// Package recipe 配方注册表与匹配(场景/工作台与合成表.md §2–§4)。 // // 索引化匹配:启动时按输入物品建索引,运行时不做全表扫描。 package recipe import ( "encoding/json" "fmt" "os" "sort" "strings" ) // Result 配方产物。 type Result struct { Item string `json:"item"` Count int `json:"count"` } // Recipe 配方条目(assets/config/recipes.json)。 type Recipe struct { Type string `json:"type"` // crafting_shaped / crafting_shapeless / smelting Pattern []string `json:"pattern"` Key map[string]string `json:"key"` Ingredients []string `json:"ingredients"` Input string `json:"input"` Result Result `json:"result"` } // Registry 配方注册表(含索引)。 type Registry struct { shaped []*Recipe shapeless []*Recipe smelting map[string]*Recipe index map[string][]*Recipe // 输入物品 → 候选配方(§3 索引) } // Load 解析 recipes.json 并构建索引。 func Load(path string) (*Registry, error) { b, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("recipe.Load %s: %w", path, err) } var doc struct { Recipes []Recipe `json:"recipes"` } if err := json.Unmarshal(b, &doc); err != nil { return nil, fmt.Errorf("recipe.Load %s: %w", path, err) } r := &Registry{smelting: make(map[string]*Recipe), index: make(map[string][]*Recipe)} for i := range doc.Recipes { rc := &doc.Recipes[i] switch rc.Type { case "crafting_shaped": r.shaped = append(r.shaped, rc) for _, k := range rc.Key { r.index[k] = append(r.index[k], rc) } case "crafting_shapeless": r.shapeless = append(r.shapeless, rc) for _, ing := range rc.Ingredients { r.index[ing] = append(r.index[ing], rc) } case "smelting": r.smelting[rc.Input] = rc default: return nil, fmt.Errorf("recipe.Load: 未知配方类型 %q", rc.Type) } } return r, nil } // MatchSmelting 熔炼匹配(单输入键)。 func (r *Registry) MatchSmelting(input string) (*Recipe, bool) { rc, ok := r.smelting[input] return rc, ok } // MatchShapeless 无序匹配:多重集比对(工作台与合成表.md §4.2 计数不能只判包含)。 func (r *Registry) MatchShapeless(counts map[string]int) (*Recipe, bool) { for _, rc := range r.shapeless { if multisetEq(counts, rc.Ingredients) { return rc, true } } return nil, false } // multisetEq 输入多重集与配方配料多重集是否完全一致。 func multisetEq(counts map[string]int, ingredients []string) bool { if len(counts) != len(ingredients) { return false } want := make(map[string]int, len(ingredients)) for _, ing := range ingredients { want[ing]++ } for k, v := range want { if counts[k] != v { return false } } return true } // MatchShaped 有序匹配:pattern 归一化后与 3×3 网格逐偏移比对(工作台与合成表.md §4.1)。 // grid 按行主序:grid[y][x](y=0 为最上行)。 func (r *Registry) MatchShaped(grid [3][3]string) (*Recipe, bool) { // 网格归一化:去掉空行/空列 g := normalizeGrid(grid) for _, rc := range r.shaped { if shapedMatches(rc, g) { return rc, true } } return nil, false } // gridT 归一化后的网格(行 × 列,均为非空)。 type gridT struct { w, h int cell [][]string } // normalizeGrid 去除全空行与全空列。 func normalizeGrid(g [3][3]string) gridT { var out gridT for y := 0; y < 3; y++ { var row []string for x := 0; x < 3; x++ { row = append(row, g[y][x]) } out.cell = append(out.cell, row) } // 裁空行 var rows [][]string for _, row := range out.cell { nonEmpty := false for _, c := range row { if c != "" { nonEmpty = true } } if nonEmpty { rows = append(rows, row) } } if len(rows) == 0 { return gridT{} } out.cell = rows out.h = len(rows) // 裁空列 minCol, maxCol := 3, -1 for x := 0; x < 3; x++ { for y := 0; y < out.h; y++ { if out.cell[y][x] != "" { if x < minCol { minCol = x } if x > maxCol { maxCol = x } } } } if minCol > maxCol { return gridT{} } var trimmed [][]string for y := 0; y < out.h; y++ { trimmed = append(trimmed, out.cell[y][minCol:maxCol+1]) } out.cell = trimmed out.w = maxCol - minCol + 1 return out } // shapedMatches 配方 pattern(同样归一化)与网格全等。 func shapedMatches(rc *Recipe, g gridT) bool { if rc == nil || len(rc.Pattern) == 0 { return false } if len(rc.Pattern) != g.h { return false } for y, row := range rc.Pattern { if len(row) != g.w { return false } for x := 0; x < g.w; x++ { sym := string(row[x]) if sym == " " { if g.cell[y][x] != "" { return false } continue } if g.cell[y][x] != rc.Key[sym] { return false } } } return true } // GridFromStrings 把行主序字符串切片转网格(" X " 表示一行)。 func GridFromStrings(rows []string) [3][3]string { var g [3][3]string for y := 0; y < 3 && y < len(rows); y++ { row := rows[y] for x := 0; x < 3 && x < len(row); x++ { if row[x] != ' ' { g[y][x] = string(row[x]) } } } return g } // KeysSorted 返回已排序键列表(工具函数)。 func KeysSorted(m map[string]int) []string { ks := make([]string, 0, len(m)) for k := range m { ks = append(ks, k) } sort.Strings(ks) return ks } // TrimNS 去掉命名空间前缀(minecraft:stick → stick)。 func TrimNS(s string) string { if i := strings.IndexByte(s, ':'); i >= 0 { return s[i+1:] } return s }