- commit
- 4242efd6cc85182bcfbe9f8cf55579309e61f575
- parent
- 504785ea074afcc37e053973bbdecb9eb5471480
- author
- Dami <dami.etoile@gmail.com>
| M | fillomino/ | 25 | +++++-- |
| M | game/ | 67 | ------------------ |
| M | game/ | 70 | ------------------- |
| A | gamereg/ | 99 | +++++++++++++++++++++++++++ |
| A | gamereg/ | 72 | +++++++++++++++++++ |
| M | hashiwokakero/ | 25 | +++++-- |
| M | hitori/ | 23 | ++++-- |
| M | hitori/ | 2 | +- |
| M | lightsout/ | 25 | +++++-- |
| M | nonogram/ | 23 | ++++-- |
| M | nurikabe/ | 25 | +++++-- |
| M | puzzle/ | 55 | +++++++++++++++ |
| M | puzzle/ | 53 | ++++++++++++++ |
| M | registry/ | 111 | ++++++------------------------ |
| M | rippleeffect/ | 25 | +++++-- |
| M | shikaku/ | 25 | +++++-- |
| M | spellpuzzle/ | 25 | +++++-- |
| M | sudoku/ | 23 | ++++-- |
| M | sudokurgb/ | 25 | +++++-- |
| M | takuzu/ | 25 | +++++-- |
| M | takuzuplus/ | 25 | +++++-- |
| M | wordsearch/ | 25 | +++++-- |
@@ -5,6 +5,8 @@ import (
55 "math/rand/v2"66 77 "github.com/FelineStateMachine/puzzletea/game"8+ "github.com/FelineStateMachine/puzzletea/gamereg"9+ "github.com/FelineStateMachine/puzzletea/puzzle"810 )911 1012 //go:embed help.md@@ -56,12 +58,19 @@ var Modes = []game.Mode{
5658 NewMode("Expert 12x12", "Wide board with long deduction chains.", 12, 9, 0.52),5759 }5860 59-var Definition = game.NewDefinition(game.DefinitionSpec{60- Name: "Fillomino",61- Description: "Grow the numbered regions to their exact sizes.",62- Aliases: []string{"polyomino", "regions"},63- Modes: Modes,64- DailyModeIndexes: []int{1, 2, 3},65- Help: HelpContent,66- Import: game.AdaptImport(ImportModel),61+var ModeDefinitions = gamereg.BuildModeDefs(Modes)62+63+var Definition = puzzle.NewDefinition(puzzle.DefinitionSpec{64+ Name: "Fillomino",65+ Description: "Grow the numbered regions to their exact sizes.",66+ Aliases: []string{"polyomino", "regions"},67+ Modes: ModeDefinitions,68+ DailyModeIDs: puzzle.SelectModeIDsByIndex(ModeDefinitions, 1, 2, 3),69+})70+71+var Entry = gamereg.NewEntry(gamereg.EntrySpec{72+ Definition: Definition,73+ Help: HelpContent,74+ Import: game.AdaptImport(ImportModel),75+ Modes: Modes,6776 })@@ -202,73 +202,6 @@ func TestBaseMode(t *testing.T) {
202202 })203203 }204204 205-// --- Category (P3) ---206-207-func TestCategory(t *testing.T) {208- c := Category{Name: "Sudoku", Desc: "Classic"}209-210- if got := c.Title(); got != "Sudoku" {211- t.Errorf("Title() = %q, want %q", got, "Sudoku")212- }213- if got := c.Description(); got != "Classic" {214- t.Errorf("Description() = %q, want %q", got, "Classic")215- }216- if got := c.FilterValue(); got != "Sudoku" {217- t.Errorf("FilterValue() = %q, want %q", got, "Sudoku")218- }219-}220-221-// --- Definition (P1) ---222-223-func TestDefinitionCategory(t *testing.T) {224- def := Definition{225- Name: "Sudoku",226- Description: "Classic grid logic",227- Help: "Rules",228- }229-230- cat := def.Category()231- if cat.Name != def.Name {232- t.Fatalf("Category().Name = %q, want %q", cat.Name, def.Name)233- }234- if cat.Desc != def.Description {235- t.Fatalf("Category().Desc = %q, want %q", cat.Desc, def.Description)236- }237- if cat.Help != def.Help {238- t.Fatalf("Category().Help = %q, want %q", cat.Help, def.Help)239- }240-}241-242-func TestNewDefinitionSelectsDailyModesByIndex(t *testing.T) {243- modes := []Mode{244- NewBaseMode("Beginner", "Easy board"),245- NewBaseMode("Medium", "Balanced board"),246- NewBaseMode("Expert", "Hard board"),247- }248-249- def := NewDefinition(DefinitionSpec{250- Name: "Sudoku",251- Description: "Classic grid logic",252- Aliases: []string{"classic"},253- Modes: modes,254- DailyModeIndexes: []int{1, 2, 99, -1},255- Help: "Rules",256- })257-258- if got, want := len(def.DailyModes), 2; got != want {259- t.Fatalf("len(DailyModes) = %d, want %d", got, want)260- }261- if got, want := def.DailyModes[0].Title(), "Medium"; got != want {262- t.Fatalf("DailyModes[0] = %q, want %q", got, want)263- }264- if got, want := def.DailyModes[1].Title(), "Expert"; got != want {265- t.Fatalf("DailyModes[1] = %q, want %q", got, want)266- }267- if got, want := len(def.Aliases), 1; got != want {268- t.Fatalf("len(Aliases) = %d, want %d", got, want)269- }270-}271-272205 func TestNormalizeName(t *testing.T) {273206 tests := []struct {274207 input string@@ -6,7 +6,6 @@ import (
66 "math/rand/v2"77 88 "charm.land/bubbles/v2/key"9- "charm.land/bubbles/v2/list"109 tea "charm.land/bubbletea/v2"1110 "github.com/FelineStateMachine/puzzletea/puzzle"1211 )@@ -76,81 +75,12 @@ func (b BaseMode) Title() string { return b.title }
7675 func (b BaseMode) Description() string { return b.description }7776 func (b BaseMode) FilterValue() string { return b.title + " " + b.description }7877 79-// Category groups related game modes under a heading in the menu.80-type Category struct {81- Name string82- Desc string83- Modes []list.Item84- Help string // embedded help.md content rendered in "How to Play"85-}86-87-func (c Category) Title() string { return c.Name }88-func (c Category) Description() string { return c.Desc }89-func (c Category) FilterValue() string { return c.Name }90-91-// Definition is the package-level metadata for a puzzle game.92-type Definition struct {93- Name string94- Description string95- Aliases []string96- Modes []Mode97- DailyModes []Mode98- Help string99- Import func([]byte) (Gamer, error)100-}101-102-type DefinitionSpec struct {103- Name string104- Description string105- Aliases []string106- Modes []Mode107- DailyModeIndexes []int108- Help string109- Import func([]byte) (Gamer, error)110-}111-112-func NewDefinition(spec DefinitionSpec) Definition {113- return Definition{114- Name: spec.Name,115- Description: spec.Description,116- Aliases: append([]string(nil), spec.Aliases...),117- Modes: append([]Mode(nil), spec.Modes...),118- DailyModes: SelectModesByIndex(spec.Modes, spec.DailyModeIndexes...),119- Help: spec.Help,120- Import: spec.Import,121- }122-}123-124-func SelectModesByIndex(modes []Mode, indexes ...int) []Mode {125- selected := make([]Mode, 0, len(indexes))126- for _, idx := range indexes {127- if idx < 0 || idx >= len(modes) {128- continue129- }130- selected = append(selected, modes[idx])131- }132- return selected133-}134-13578 func AdaptImport[T Gamer](fn func([]byte) (T, error)) func([]byte) (Gamer, error) {13679 return func(data []byte) (Gamer, error) {13780 return fn(data)13881 }13982 }14083 141-func (d Definition) Category() Category {142- modes := make([]list.Item, len(d.Modes))143- for i, m := range d.Modes {144- modes[i] = m145- }146- return Category{147- Name: d.Name,148- Desc: d.Description,149- Modes: modes,150- Help: d.Help,151- }152-}153-15484 // NormalizeName lowercases and replaces hyphens/underscores with spaces for15585 // fuzzy matching of CLI arguments to game names and aliases.15686 func NormalizeName(s string) string {@@ -0,0 +1,99 @@
1+package gamereg2+3+import (4+ "fmt"5+6+ "github.com/FelineStateMachine/puzzletea/game"7+ "github.com/FelineStateMachine/puzzletea/puzzle"8+)9+10+type ModeEntry struct {11+ Definition puzzle.ModeDef12+ Spawner game.Spawner13+ Seeded game.SeededSpawner14+}15+16+type Entry struct {17+ Definition puzzle.Definition18+ Help string19+ Import func([]byte) (game.Gamer, error)20+ Modes []ModeEntry21+}22+23+type EntrySpec struct {24+ Definition puzzle.Definition25+ Help string26+ Import func([]byte) (game.Gamer, error)27+ Modes []game.Mode28+}29+30+func BuildModeDefs(modes []game.Mode) []puzzle.ModeDef {31+ defs := make([]puzzle.ModeDef, 0, len(modes))32+ for _, mode := range modes {33+ def := puzzle.NewModeDef(puzzle.ModeSpec{34+ Title: mode.Title(),35+ Description: mode.Description(),36+ })37+ if _, ok := mode.(game.SeededSpawner); ok {38+ def.Seeded = true39+ }40+ defs = append(defs, def)41+ }42+ return defs43+}44+45+func NewEntry(spec EntrySpec) Entry {46+ if len(spec.Definition.Modes) != len(spec.Modes) {47+ panic(fmt.Sprintf(48+ "gamereg: definition %q has %d mode definitions but %d runtime modes",49+ spec.Definition.Name,50+ len(spec.Definition.Modes),51+ len(spec.Modes),52+ ))53+ }54+55+ entries := make([]ModeEntry, 0, len(spec.Modes))56+ for i, mode := range spec.Modes {57+ spawner, ok := mode.(game.Spawner)58+ if !ok {59+ panic(fmt.Sprintf(60+ "gamereg: definition %q mode %q does not implement game.Spawner",61+ spec.Definition.Name,62+ mode.Title(),63+ ))64+ }65+66+ modeDef := spec.Definition.Modes[i]67+ if modeDef.Title != mode.Title() || modeDef.Description != mode.Description() {68+ panic(fmt.Sprintf(69+ "gamereg: definition %q mode %d metadata does not match runtime mode %q",70+ spec.Definition.Name,71+ i,72+ mode.Title(),73+ ))74+ }75+76+ entry := ModeEntry{77+ Definition: modeDef,78+ Spawner: spawner,79+ }80+ if seeded, ok := mode.(game.SeededSpawner); ok {81+ entry.Seeded = seeded82+ }83+ if entry.Definition.Seeded != (entry.Seeded != nil) {84+ panic(fmt.Sprintf(85+ "gamereg: definition %q mode %q seeded flag does not match runtime mode",86+ spec.Definition.Name,87+ modeDef.Title,88+ ))89+ }90+ entries = append(entries, entry)91+ }92+93+ return Entry{94+ Definition: spec.Definition,95+ Help: spec.Help,96+ Import: spec.Import,97+ Modes: entries,98+ }99+}@@ -0,0 +1,72 @@
1+package gamereg2+3+import (4+ "errors"5+ "math/rand/v2"6+ "testing"7+8+ "github.com/FelineStateMachine/puzzletea/game"9+ "github.com/FelineStateMachine/puzzletea/puzzle"10+)11+12+type stubMode struct {13+ game.BaseMode14+}15+16+func (m stubMode) Spawn() (game.Gamer, error) {17+ return nil, errors.New("not implemented")18+}19+20+type seededStubMode struct {21+ stubMode22+}23+24+func (m seededStubMode) SpawnSeeded(rng *rand.Rand) (game.Gamer, error) {25+ return nil, errors.New("not implemented")26+}27+28+func TestBuildModeDefsMarksSeededModes(t *testing.T) {29+ modes := []game.Mode{30+ stubMode{BaseMode: game.NewBaseMode("Beginner", "Easy board")},31+ seededStubMode{stubMode{BaseMode: game.NewBaseMode("Expert", "Hard board")}},32+ }33+34+ defs := BuildModeDefs(modes)35+ if len(defs) != 2 {36+ t.Fatalf("len(BuildModeDefs) = %d, want 2", len(defs))37+ }38+ if defs[0].Seeded {39+ t.Fatal("defs[0].Seeded = true, want false")40+ }41+ if !defs[1].Seeded {42+ t.Fatal("defs[1].Seeded = false, want true")43+ }44+}45+46+func TestNewEntryKeepsMetadataAndRuntimeModesAligned(t *testing.T) {47+ modes := []game.Mode{48+ seededStubMode{stubMode{BaseMode: game.NewBaseMode("Medium", "Balanced board")}},49+ }50+ definition := puzzle.NewDefinition(puzzle.DefinitionSpec{51+ Name: "Sudoku",52+ Modes: BuildModeDefs(modes),53+ DailyModeIDs: []puzzle.ModeID{"medium"},54+ })55+56+ entry := NewEntry(EntrySpec{57+ Definition: definition,58+ Help: "Rules",59+ Import: func([]byte) (game.Gamer, error) { return nil, nil },60+ Modes: modes,61+ })62+63+ if got, want := entry.Definition.Name, "Sudoku"; got != want {64+ t.Fatalf("Definition.Name = %q, want %q", got, want)65+ }66+ if got, want := len(entry.Modes), 1; got != want {67+ t.Fatalf("len(Modes) = %d, want %d", got, want)68+ }69+ if entry.Modes[0].Seeded == nil {70+ t.Fatal("Modes[0].Seeded = nil, want seeded spawner")71+ }72+}@@ -5,6 +5,8 @@ import (
55 "math/rand/v2"66 77 "github.com/FelineStateMachine/puzzletea/game"8+ "github.com/FelineStateMachine/puzzletea/gamereg"9+ "github.com/FelineStateMachine/puzzletea/puzzle"810 )911 1012 //go:embed help.md@@ -65,12 +67,19 @@ var Modes = []game.Mode{
6567 NewMode("Hard 13x13", "13x13 grid with 59-68 islands.", 13, 13, 59, 68),6668 }6769 68-var Definition = game.NewDefinition(game.DefinitionSpec{69- Name: "Hashiwokakero",70- Description: "Connect the islands with bridges.",71- Aliases: []string{"hashi", "bridges"},72- Modes: Modes,73- DailyModeIndexes: []int{3, 1},74- Help: HelpContent,75- Import: game.AdaptImport(ImportModel),70+var ModeDefinitions = gamereg.BuildModeDefs(Modes)71+72+var Definition = puzzle.NewDefinition(puzzle.DefinitionSpec{73+ Name: "Hashiwokakero",74+ Description: "Connect the islands with bridges.",75+ Aliases: []string{"hashi", "bridges"},76+ Modes: ModeDefinitions,77+ DailyModeIDs: puzzle.SelectModeIDsByIndex(ModeDefinitions, 3, 1),78+})79+80+var Entry = gamereg.NewEntry(gamereg.EntrySpec{81+ Definition: Definition,82+ Help: HelpContent,83+ Import: game.AdaptImport(ImportModel),84+ Modes: Modes,7685 })@@ -5,6 +5,8 @@ import (
55 "math/rand/v2"66 77 "github.com/FelineStateMachine/puzzletea/game"8+ "github.com/FelineStateMachine/puzzletea/gamereg"9+ "github.com/FelineStateMachine/puzzletea/puzzle"810 )911 1012 //go:embed help.md@@ -55,11 +57,18 @@ var Modes = []game.Mode{
5557 NewMode("Expert", "12\u00d712 grid, maximum challenge.", 12, 0.28),5658 }5759 58-var Definition = game.NewDefinition(game.DefinitionSpec{59- Name: "Hitori",60- Description: "Shade the cells to eliminate duplicates.",61- Modes: Modes,62- DailyModeIndexes: []int{1, 2},63- Help: HelpContent,64- Import: game.AdaptImport(ImportModel),60+var ModeDefinitions = gamereg.BuildModeDefs(Modes)61+62+var Definition = puzzle.NewDefinition(puzzle.DefinitionSpec{63+ Name: "Hitori",64+ Description: "Shade the cells to eliminate duplicates.",65+ Modes: ModeDefinitions,66+ DailyModeIDs: puzzle.SelectModeIDsByIndex(ModeDefinitions, 1, 2),67+})68+69+var Entry = gamereg.NewEntry(gamereg.EntrySpec{70+ Definition: Definition,71+ Help: HelpContent,72+ Import: game.AdaptImport(ImportModel),73+ Modes: Modes,6574 })@@ -1385,7 +1385,7 @@ func TestRegistration(t *testing.T) {
13851385 g, _ := New(mode, numbers)13861386 data, _ := g.GetSave()13871387 1388- loaded, err := Definition.Import(data)1388+ loaded, err := Entry.Import(data)13891389 if err != nil {13901390 t.Fatalf("definition import: %v", err)13911391 }@@ -5,6 +5,8 @@ import (
55 "math/rand/v2"66 77 "github.com/FelineStateMachine/puzzletea/game"8+ "github.com/FelineStateMachine/puzzletea/gamereg"9+ "github.com/FelineStateMachine/puzzletea/puzzle"810 )911 1012 //go:embed help.md@@ -44,12 +46,19 @@ var Modes = []game.Mode{
4446 NewMode("Extreme", "9x9 grid", 9, 9),4547 }4648 47-var Definition = game.NewDefinition(game.DefinitionSpec{48- Name: "Lights Out",49- Description: "Turn the lights off.",50- Aliases: []string{"lights"},51- Modes: Modes,52- DailyModeIndexes: []int{1, 2},53- Help: HelpContent,54- Import: game.AdaptImport(ImportModel),49+var ModeDefinitions = gamereg.BuildModeDefs(Modes)50+51+var Definition = puzzle.NewDefinition(puzzle.DefinitionSpec{52+ Name: "Lights Out",53+ Description: "Turn the lights off.",54+ Aliases: []string{"lights"},55+ Modes: ModeDefinitions,56+ DailyModeIDs: puzzle.SelectModeIDsByIndex(ModeDefinitions, 1, 2),57+})58+59+var Entry = gamereg.NewEntry(gamereg.EntrySpec{60+ Definition: Definition,61+ Help: HelpContent,62+ Import: game.AdaptImport(ImportModel),63+ Modes: Modes,5564 })@@ -5,6 +5,8 @@ import (
55 "math/rand/v2"66 77 "github.com/FelineStateMachine/puzzletea/game"8+ "github.com/FelineStateMachine/puzzletea/gamereg"9+ "github.com/FelineStateMachine/puzzletea/puzzle"810 )911 1012 //go:embed help.md@@ -58,11 +60,18 @@ var Modes = []game.Mode{
5860 NewMode("Massive", "20x20 grid, ~56% filled. Truly massive puzzle.", 20, 20, 0.56),5961 }6062 61-var Definition = game.NewDefinition(game.DefinitionSpec{62- Name: "Nonogram",63- Description: "Fill the cells to match tomographic hints.",64- Modes: Modes,65- DailyModeIndexes: []int{3, 4},66- Help: HelpContent,67- Import: game.AdaptImport(ImportModel),63+var ModeDefinitions = gamereg.BuildModeDefs(Modes)64+65+var Definition = puzzle.NewDefinition(puzzle.DefinitionSpec{66+ Name: "Nonogram",67+ Description: "Fill the cells to match tomographic hints.",68+ Modes: ModeDefinitions,69+ DailyModeIDs: puzzle.SelectModeIDsByIndex(ModeDefinitions, 3, 4),70+})71+72+var Entry = gamereg.NewEntry(gamereg.EntrySpec{73+ Definition: Definition,74+ Help: HelpContent,75+ Import: game.AdaptImport(ImportModel),76+ Modes: Modes,6877 })@@ -7,6 +7,8 @@ import (
77 "math/rand/v2"88 99 "github.com/FelineStateMachine/puzzletea/game"10+ "github.com/FelineStateMachine/puzzletea/gamereg"11+ "github.com/FelineStateMachine/puzzletea/puzzle"1012 )1113 1214 //go:embed help.md@@ -73,12 +75,19 @@ var Modes = []game.Mode{
7375 NewMode("Expert", "12x12 grid, sparse clues and long chains.", 12, 12, 0.14, 12),7476 }7577 76-var Definition = game.NewDefinition(game.DefinitionSpec{77- Name: "Nurikabe",78- Description: "Split the land while keeping one connected sea.",79- Aliases: []string{"islands", "sea"},80- Modes: Modes,81- DailyModeIndexes: []int{1, 2},82- Help: HelpContent,83- Import: game.AdaptImport(ImportModel),78+var ModeDefinitions = gamereg.BuildModeDefs(Modes)79+80+var Definition = puzzle.NewDefinition(puzzle.DefinitionSpec{81+ Name: "Nurikabe",82+ Description: "Split the land while keeping one connected sea.",83+ Aliases: []string{"islands", "sea"},84+ Modes: ModeDefinitions,85+ DailyModeIDs: puzzle.SelectModeIDsByIndex(ModeDefinitions, 1, 2),86+})87+88+var Entry = gamereg.NewEntry(gamereg.EntrySpec{89+ Definition: Definition,90+ Help: HelpContent,91+ Import: game.AdaptImport(ImportModel),92+ Modes: Modes,8493 })@@ -13,6 +13,13 @@ type ModeDef struct {
1313 Seeded bool1414 }1515 16+type ModeSpec struct {17+ ID ModeID18+ Title string19+ Description string20+ Seeded bool21+}22+1623 type Definition struct {1724 ID GameID1825 Name string@@ -22,6 +29,54 @@ type Definition struct {
2229 DailyModeIDs []ModeID2330 }2431 32+type DefinitionSpec struct {33+ ID GameID34+ Name string35+ Description string36+ Aliases []string37+ Modes []ModeDef38+ DailyModeIDs []ModeID39+}40+41+func NewModeDef(spec ModeSpec) ModeDef {42+ id := spec.ID43+ if id == "" {44+ id = CanonicalModeID(spec.Title)45+ }46+ return ModeDef{47+ ID: id,48+ Title: spec.Title,49+ Description: spec.Description,50+ Seeded: spec.Seeded,51+ }52+}53+54+func NewDefinition(spec DefinitionSpec) Definition {55+ id := spec.ID56+ if id == "" {57+ id = CanonicalGameID(spec.Name)58+ }59+ return Definition{60+ ID: id,61+ Name: spec.Name,62+ Description: spec.Description,63+ Aliases: append([]string(nil), spec.Aliases...),64+ Modes: append([]ModeDef(nil), spec.Modes...),65+ DailyModeIDs: append([]ModeID(nil), spec.DailyModeIDs...),66+ }67+}68+69+func SelectModeIDsByIndex(modes []ModeDef, indexes ...int) []ModeID {70+ selected := make([]ModeID, 0, len(indexes))71+ for _, idx := range indexes {72+ if idx < 0 || idx >= len(modes) {73+ continue74+ }75+ selected = append(selected, modes[idx].ID)76+ }77+ return selected78+}79+2580 func NormalizeName(s string) string {2681 s = strings.ToLower(strings.TrimSpace(s))2782 s = strings.ReplaceAll(s, "-", " ")@@ -29,3 +29,56 @@ func TestCanonicalIDsUseNormalizedNames(t *testing.T) {
2929 t.Fatalf("CanonicalModeID() = %q, want %q", got, want)3030 }3131 }32+33+func TestNewModeDefAndDefinitionCloneInput(t *testing.T) {34+ mode := NewModeDef(ModeSpec{35+ Title: "Medium",36+ Description: "Balanced board",37+ Seeded: true,38+ })39+ if got, want := mode.ID, ModeID("medium"); got != want {40+ t.Fatalf("ModeDef.ID = %q, want %q", got, want)41+ }42+43+ spec := DefinitionSpec{44+ Name: "Sudoku",45+ Description: "Classic grid logic",46+ Aliases: []string{"classic"},47+ Modes: []ModeDef{mode},48+ DailyModeIDs: []ModeID{mode.ID},49+ }50+ def := NewDefinition(spec)51+52+ spec.Aliases[0] = "changed"53+ spec.Modes[0].Title = "Changed"54+ spec.DailyModeIDs[0] = "changed"55+56+ if got, want := def.ID, GameID("sudoku"); got != want {57+ t.Fatalf("Definition.ID = %q, want %q", got, want)58+ }59+ if got, want := def.Aliases[0], "classic"; got != want {60+ t.Fatalf("Aliases[0] = %q, want %q", got, want)61+ }62+ if got, want := def.Modes[0].Title, "Medium"; got != want {63+ t.Fatalf("Modes[0].Title = %q, want %q", got, want)64+ }65+ if got, want := def.DailyModeIDs[0], ModeID("medium"); got != want {66+ t.Fatalf("DailyModeIDs[0] = %q, want %q", got, want)67+ }68+}69+70+func TestSelectModeIDsByIndex(t *testing.T) {71+ modes := []ModeDef{72+ NewModeDef(ModeSpec{Title: "Beginner"}),73+ NewModeDef(ModeSpec{Title: "Medium"}),74+ NewModeDef(ModeSpec{Title: "Expert"}),75+ }76+77+ got := SelectModeIDsByIndex(modes, 1, 2, 99, -1)78+ if len(got) != 2 {79+ t.Fatalf("len(SelectModeIDsByIndex) = %d, want 2", len(got))80+ }81+ if got[0] != ModeID("medium") || got[1] != ModeID("expert") {82+ t.Fatalf("SelectModeIDsByIndex = %v, want [medium expert]", got)83+ }84+}@@ -6,6 +6,7 @@ import (
66 "github.com/FelineStateMachine/puzzletea/catalog"77 "github.com/FelineStateMachine/puzzletea/fillomino"88 "github.com/FelineStateMachine/puzzletea/game"9+ "github.com/FelineStateMachine/puzzletea/gamereg"910 "github.com/FelineStateMachine/puzzletea/hashiwokakero"1011 "github.com/FelineStateMachine/puzzletea/hitori"1112 "github.com/FelineStateMachine/puzzletea/lightsout"@@ -22,18 +23,10 @@ import (
2223 "github.com/FelineStateMachine/puzzletea/wordsearch"2324 )2425 25-type ModeEntry struct {26- Definition puzzle.ModeDef27- Spawner game.Spawner28- Seeded game.SeededSpawner29-}30-31-type Entry struct {32- Definition puzzle.Definition33- Help string34- Import func([]byte) (game.Gamer, error)35- Modes []ModeEntry36-}26+type (27+ ModeEntry = gamereg.ModeEntry28+ Entry = gamereg.Entry29+)3730 3831 type DailyEntry struct {3932 Spawner game.SeededSpawner@@ -43,22 +36,22 @@ type DailyEntry struct {
4336 Mode string4437 }4538 46-var all = buildEntries(47- fillomino.Definition,48- hashiwokakero.Definition,49- hitori.Definition,50- lightsout.Definition,51- nonogram.Definition,52- nurikabe.Definition,53- rippleeffect.Definition,54- shikaku.Definition,55- spellpuzzle.Definition,56- sudoku.Definition,57- sudokurgb.Definition,58- takuzu.Definition,59- takuzuplus.Definition,60- wordsearch.Definition,61-)39+var all = []Entry{40+ fillomino.Entry,41+ hashiwokakero.Entry,42+ hitori.Entry,43+ lightsout.Entry,44+ nonogram.Entry,45+ nurikabe.Entry,46+ rippleeffect.Entry,47+ shikaku.Entry,48+ spellpuzzle.Entry,49+ sudoku.Entry,50+ sudokurgb.Entry,51+ takuzu.Entry,52+ takuzuplus.Entry,53+ wordsearch.Entry,54+}6255 6356 var (6457 definitions = buildDefinitions(all)@@ -66,68 +59,6 @@ var (
6659 entriesByID = buildEntriesByID(all)6760 )6861 69-func adaptLegacy(def game.Definition) Entry {70- gameID := puzzle.CanonicalGameID(def.Name)71- modes := make([]ModeEntry, 0, len(def.Modes))72- for _, mode := range def.Modes {73- spawner, ok := mode.(game.Spawner)74- if !ok {75- continue76- }77- modeID := puzzle.CanonicalModeID(mode.Title())78- modeDef := puzzle.ModeDef{79- ID: modeID,80- Title: mode.Title(),81- Description: mode.Description(),82- }83- var seeded game.SeededSpawner84- if s, ok := mode.(game.SeededSpawner); ok {85- modeDef.Seeded = true86- seeded = s87- }88- modes = append(modes, ModeEntry{89- Definition: modeDef,90- Spawner: spawner,91- Seeded: seeded,92- })93- }94-95- dailyIDs := make([]puzzle.ModeID, 0, len(def.DailyModes))96- for _, mode := range def.DailyModes {97- dailyIDs = append(dailyIDs, puzzle.CanonicalModeID(mode.Title()))98- }99-100- return Entry{101- Definition: puzzle.Definition{102- ID: gameID,103- Name: def.Name,104- Description: def.Description,105- Aliases: append([]string(nil), def.Aliases...),106- Modes: extractModeDefs(modes),107- DailyModeIDs: dailyIDs,108- },109- Help: def.Help,110- Import: def.Import,111- Modes: modes,112- }113-}114-115-func buildEntries(defs ...game.Definition) []Entry {116- entries := make([]Entry, 0, len(defs))117- for _, def := range defs {118- entries = append(entries, adaptLegacy(def))119- }120- return entries121-}122-123-func extractModeDefs(modes []ModeEntry) []puzzle.ModeDef {124- defs := make([]puzzle.ModeDef, 0, len(modes))125- for _, mode := range modes {126- defs = append(defs, mode.Definition)127- }128- return defs129-}130-13162 func buildDefinitions(entries []Entry) []puzzle.Definition {13263 defs := make([]puzzle.Definition, 0, len(entries))13364 for _, entry := range entries {@@ -5,6 +5,8 @@ import (
55 "math/rand/v2"66 77 "github.com/FelineStateMachine/puzzletea/game"8+ "github.com/FelineStateMachine/puzzletea/gamereg"9+ "github.com/FelineStateMachine/puzzletea/puzzle"810 )911 1012 //go:embed help.md@@ -122,14 +124,21 @@ var Modes = []game.Mode{
122124 ),123125 }124126 125-var Definition = game.NewDefinition(game.DefinitionSpec{126- Name: "Ripple Effect",127- Description: "Fill the cages with sequential numbers without violating ripple distance.",128- Aliases: []string{"ripple"},129- Modes: Modes,130- DailyModeIndexes: []int{1, 2, 3},131- Help: HelpContent,132- Import: game.AdaptImport(ImportModel),127+var ModeDefinitions = gamereg.BuildModeDefs(Modes)128+129+var Definition = puzzle.NewDefinition(puzzle.DefinitionSpec{130+ Name: "Ripple Effect",131+ Description: "Fill the cages with sequential numbers without violating ripple distance.",132+ Aliases: []string{"ripple"},133+ Modes: ModeDefinitions,134+ DailyModeIDs: puzzle.SelectModeIDsByIndex(ModeDefinitions, 1, 2, 3),135+})136+137+var Entry = gamereg.NewEntry(gamereg.EntrySpec{138+ Definition: Definition,139+ Help: HelpContent,140+ Import: game.AdaptImport(ImportModel),141+ Modes: Modes,133142 })134143 135144 func (m Mode) generatePuzzle() (Puzzle, error) {@@ -5,6 +5,8 @@ import (
55 "math/rand/v2"66 77 "github.com/FelineStateMachine/puzzletea/game"8+ "github.com/FelineStateMachine/puzzletea/gamereg"9+ "github.com/FelineStateMachine/puzzletea/puzzle"810 )911 1012 //go:embed help.md@@ -56,12 +58,19 @@ var Modes = []game.Mode{
5658 NewMode("Expert 12x12", "12x12 grid, maximum challenge.", 12, 12, 20),5759 }5860 59-var Definition = game.NewDefinition(game.DefinitionSpec{60- Name: "Shikaku",61- Description: "Divide the grid into rectangles with set sizes.",62- Aliases: []string{"rectangles"},63- Modes: Modes,64- DailyModeIndexes: []int{1, 2},65- Help: HelpContent,66- Import: game.AdaptImport(ImportModel),61+var ModeDefinitions = gamereg.BuildModeDefs(Modes)62+63+var Definition = puzzle.NewDefinition(puzzle.DefinitionSpec{64+ Name: "Shikaku",65+ Description: "Divide the grid into rectangles with set sizes.",66+ Aliases: []string{"rectangles"},67+ Modes: ModeDefinitions,68+ DailyModeIDs: puzzle.SelectModeIDsByIndex(ModeDefinitions, 1, 2),69+})70+71+var Entry = gamereg.NewEntry(gamereg.EntrySpec{72+ Definition: Definition,73+ Help: HelpContent,74+ Import: game.AdaptImport(ImportModel),75+ Modes: Modes,6776 })@@ -5,6 +5,8 @@ import (
55 "math/rand/v2"66 77 "github.com/FelineStateMachine/puzzletea/game"8+ "github.com/FelineStateMachine/puzzletea/gamereg"9+ "github.com/FelineStateMachine/puzzletea/puzzle"810 )911 1012 //go:embed help.md@@ -55,12 +57,19 @@ var Modes = []game.Mode{
5557 NewMode("Hard", "9 letters, 9 board words, largest launch board.", 9, 9, 8),5658 }5759 58-var Definition = game.NewDefinition(game.DefinitionSpec{59- Name: "Spell Puzzle",60- Description: "Connect letters to fill a crossword with bonus anagrams.",61- Aliases: []string{"spell", "spellpuzzle"},62- Modes: Modes,63- DailyModeIndexes: []int{0},64- Help: HelpContent,65- Import: game.AdaptImport(ImportModel),60+var ModeDefinitions = gamereg.BuildModeDefs(Modes)61+62+var Definition = puzzle.NewDefinition(puzzle.DefinitionSpec{63+ Name: "Spell Puzzle",64+ Description: "Connect letters to fill a crossword with bonus anagrams.",65+ Aliases: []string{"spell", "spellpuzzle"},66+ Modes: ModeDefinitions,67+ DailyModeIDs: puzzle.SelectModeIDsByIndex(ModeDefinitions, 0),68+})69+70+var Entry = gamereg.NewEntry(gamereg.EntrySpec{71+ Definition: Definition,72+ Help: HelpContent,73+ Import: game.AdaptImport(ImportModel),74+ Modes: Modes,6675 })@@ -5,6 +5,8 @@ import (
55 "math/rand/v2"66 77 "github.com/FelineStateMachine/puzzletea/game"8+ "github.com/FelineStateMachine/puzzletea/gamereg"9+ "github.com/FelineStateMachine/puzzletea/puzzle"810 )911 1012 //go:embed help.md@@ -45,11 +47,18 @@ var Modes = []game.Mode{
4547 NewMode("Diabolical", "17–21 clues. Swordfish / XY-Chains.", 17),4648 }4749 48-var Definition = game.NewDefinition(game.DefinitionSpec{49- Name: "Sudoku",50- Description: "Fill the 9x9 grid following sudoku rules.",51- Modes: Modes,52- DailyModeIndexes: []int{1, 2},53- Help: HelpContent,54- Import: game.AdaptImport(ImportModel),50+var ModeDefinitions = gamereg.BuildModeDefs(Modes)51+52+var Definition = puzzle.NewDefinition(puzzle.DefinitionSpec{53+ Name: "Sudoku",54+ Description: "Fill the 9x9 grid following sudoku rules.",55+ Modes: ModeDefinitions,56+ DailyModeIDs: puzzle.SelectModeIDsByIndex(ModeDefinitions, 1, 2),57+})58+59+var Entry = gamereg.NewEntry(gamereg.EntrySpec{60+ Definition: Definition,61+ Help: HelpContent,62+ Import: game.AdaptImport(ImportModel),63+ Modes: Modes,5564 })@@ -5,6 +5,8 @@ import (
55 "math/rand/v2"66 77 "github.com/FelineStateMachine/puzzletea/game"8+ "github.com/FelineStateMachine/puzzletea/gamereg"9+ "github.com/FelineStateMachine/puzzletea/puzzle"810 )911 1012 //go:embed help.md@@ -45,12 +47,19 @@ var Modes = []game.Mode{
4547 NewMode("Diabolical", "30 clues. Tightest clue budget in the launch set.", 30),4648 }4749 48-var Definition = game.NewDefinition(game.DefinitionSpec{49- Name: "Sudoku RGB",50- Description: "Fill the board with RGB symbols so each row, column, and 3x3 box contains {1,1,1,2,2,2,3,3,3}. [1,2,3] maps to [▲,■,●]. Inspired by Sudoku Ripeto.",51- Aliases: []string{"rgb sudoku", "ripeto", "sudoku ripeto"},52- Modes: Modes,53- DailyModeIndexes: []int{1, 2},54- Help: HelpContent,55- Import: game.AdaptImport(ImportModel),50+var ModeDefinitions = gamereg.BuildModeDefs(Modes)51+52+var Definition = puzzle.NewDefinition(puzzle.DefinitionSpec{53+ Name: "Sudoku RGB",54+ Description: "Fill the board with RGB symbols so each row, column, and 3x3 box contains {1,1,1,2,2,2,3,3,3}. [1,2,3] maps to [▲,■,●]. Inspired by Sudoku Ripeto.",55+ Aliases: []string{"rgb sudoku", "ripeto", "sudoku ripeto"},56+ Modes: ModeDefinitions,57+ DailyModeIDs: puzzle.SelectModeIDsByIndex(ModeDefinitions, 1, 2),58+})59+60+var Entry = gamereg.NewEntry(gamereg.EntrySpec{61+ Definition: Definition,62+ Help: HelpContent,63+ Import: game.AdaptImport(ImportModel),64+ Modes: Modes,5665 })@@ -5,6 +5,8 @@ import (
55 "math/rand/v2"66 77 "github.com/FelineStateMachine/puzzletea/game"8+ "github.com/FelineStateMachine/puzzletea/gamereg"9+ "github.com/FelineStateMachine/puzzletea/puzzle"810 )911 1012 //go:embed help.md@@ -52,12 +54,19 @@ var Modes = []game.Mode{
5254 NewMode("Extreme", "14×14 grid, ~28% clues. Maximum challenge.", 14, 0.28),5355 }5456 55-var Definition = game.NewDefinition(game.DefinitionSpec{56- Name: "Takuzu",57- Description: "Fill the grid with ● and ○. No 3 in a row.",58- Aliases: []string{"binairo", "binary"},59- Modes: Modes,60- DailyModeIndexes: []int{2, 3},61- Help: HelpContent,62- Import: game.AdaptImport(ImportModel),57+var ModeDefinitions = gamereg.BuildModeDefs(Modes)58+59+var Definition = puzzle.NewDefinition(puzzle.DefinitionSpec{60+ Name: "Takuzu",61+ Description: "Fill the grid with ● and ○. No 3 in a row.",62+ Aliases: []string{"binairo", "binary"},63+ Modes: ModeDefinitions,64+ DailyModeIDs: puzzle.SelectModeIDsByIndex(ModeDefinitions, 2, 3),65+})66+67+var Entry = gamereg.NewEntry(gamereg.EntrySpec{68+ Definition: Definition,69+ Help: HelpContent,70+ Import: game.AdaptImport(ImportModel),71+ Modes: Modes,6372 })@@ -5,6 +5,8 @@ import (
55 "math/rand/v2"66 77 "github.com/FelineStateMachine/puzzletea/game"8+ "github.com/FelineStateMachine/puzzletea/gamereg"9+ "github.com/FelineStateMachine/puzzletea/puzzle"810 )911 1012 //go:embed help.md@@ -56,12 +58,19 @@ var Modes = []game.Mode{
5658 NewMode("Extreme", "14×14 grid, maximum size with additive relation clues.", 14, 0.28, modeProfiles[6]),5759 }5860 59-var Definition = game.NewDefinition(game.DefinitionSpec{60- Name: "Takuzu+",61- Description: "Fill the grid with ● and ○ using some relational clues. No 3 in a row.",62- Aliases: []string{"takuzu plus", "binario+", "binario plus"},63- Modes: Modes,64- DailyModeIndexes: []int{2, 3},65- Help: HelpContent,66- Import: game.AdaptImport(ImportModel),61+var ModeDefinitions = gamereg.BuildModeDefs(Modes)62+63+var Definition = puzzle.NewDefinition(puzzle.DefinitionSpec{64+ Name: "Takuzu+",65+ Description: "Fill the grid with ● and ○ using some relational clues. No 3 in a row.",66+ Aliases: []string{"takuzu plus", "binario+", "binario plus"},67+ Modes: ModeDefinitions,68+ DailyModeIDs: puzzle.SelectModeIDsByIndex(ModeDefinitions, 2, 3),69+})70+71+var Entry = gamereg.NewEntry(gamereg.EntrySpec{72+ Definition: Definition,73+ Help: HelpContent,74+ Import: game.AdaptImport(ImportModel),75+ Modes: Modes,6776 })@@ -5,6 +5,8 @@ import (
55 "math/rand/v2"66 77 "github.com/FelineStateMachine/puzzletea/game"8+ "github.com/FelineStateMachine/puzzletea/gamereg"9+ "github.com/FelineStateMachine/puzzletea/puzzle"810 )911 1012 //go:embed help.md@@ -54,12 +56,19 @@ var Modes = []game.Mode{
5456 NewMode("Hard 20x20", "Find 15 words in a 20x20 grid.", 20, 20, 15, 5, 10, []Direction{Right, Down, DownRight, DownLeft, Left, Up, UpRight, UpLeft}),5557 }5658 57-var Definition = game.NewDefinition(game.DefinitionSpec{58- Name: "Word Search",59- Description: "Find the hidden words in a letter grid.",60- Aliases: []string{"words", "wordsearch", "ws"},61- Modes: Modes,62- DailyModeIndexes: []int{0},63- Help: HelpContent,64- Import: game.AdaptImport(ImportModel),59+var ModeDefinitions = gamereg.BuildModeDefs(Modes)60+61+var Definition = puzzle.NewDefinition(puzzle.DefinitionSpec{62+ Name: "Word Search",63+ Description: "Find the hidden words in a letter grid.",64+ Aliases: []string{"words", "wordsearch", "ws"},65+ Modes: ModeDefinitions,66+ DailyModeIDs: puzzle.SelectModeIDsByIndex(ModeDefinitions, 0),67+})68+69+var Entry = gamereg.NewEntry(gamereg.EntrySpec{70+ Definition: Definition,71+ Help: HelpContent,72+ Import: game.AdaptImport(ImportModel),73+ Modes: Modes,6574 })