238 lines
5.5 KiB
Go
238 lines
5.5 KiB
Go
// 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
|
||
}
|