- Rename modes to creative names (Mini, Pocket, Teaser, Standard, Classic, Tricky, Large, Grand, Epic, Massive) - Add spawn performance benchmark test (20 runs per mode)
- commit
- 3200ad947aa4cd9f8ff7711a770c2cf1cdc95537
- parent
- 3e324dfa1402d5943c48b20caa19c36415c49f58
- author
- Dami <dami.etoile@gmail.com>
| M | nonogram/ | 22 | ++++----- |
| M | nonogram/ | 77 | +++++++++++++++++++++++++----- |
| M | nonogram/ | 47 | ++++++++++++++++++ |
@@ -38,19 +38,17 @@ func (n NonogramMode) Spawn() (game.Gamer, error) {
3838 3939 var Modes = []list.Item{4040 // 5x541- NewMode("Easy 5x5", "5x5 grid, ~65% filled. Dense, constraining hints.", 5, 5, 0.65),42- NewMode("Medium 5x5", "5x5 grid, ~50% filled. Balanced challenge.", 5, 5, 0.50),43- NewMode("Hard 5x5", "5x5 grid, ~35% filled. Sparse, ambiguous hints.", 5, 5, 0.35),41+ NewMode("Mini", "5x5 grid, ~65% filled. Quick puzzle, straightforward hints.", 5, 5, 0.65),42+ NewMode("Pocket", "5x5 grid, ~50% filled. Compact but balanced.", 5, 5, 0.50),43+ NewMode("Teaser", "5x5 grid, ~35% filled. Small but tricky.", 5, 5, 0.35),4444 // 10x1045- NewMode("Easy 10x10", "10x10 grid, ~65% filled. Dense, constraining hints.", 10, 10, 0.65),46- NewMode("Medium 10x10", "10x10 grid, ~50% filled. Balanced challenge.", 10, 10, 0.50),47- NewMode("Hard 10x10", "10x10 grid, ~35% filled. Sparse, ambiguous hints.", 10, 10, 0.35),45+ NewMode("Standard", "10x10 grid, ~67% filled. Classic size, dense hints.", 10, 10, 0.67),46+ NewMode("Classic", "10x10 grid, ~52% filled. The typical nonogram experience.", 10, 10, 0.52),47+ NewMode("Tricky", "10x10 grid, ~37% filled. Sparse hints require reasoning.", 10, 10, 0.37),4848 // 15x1549- NewMode("Easy 15x15", "15x15 grid, ~65% filled. Dense, constraining hints.", 15, 15, 0.65),50- NewMode("Medium 15x15", "15x15 grid, ~50% filled. Balanced challenge.", 15, 15, 0.50),51- NewMode("Hard 15x15", "15x15 grid, ~35% filled. Sparse, ambiguous hints.", 15, 15, 0.35),49+ NewMode("Large", "15x15 grid, ~69% filled. Bigger grid, constraining hints.", 15, 15, 0.69),50+ NewMode("Grand", "15x15 grid, ~54% filled. A substantial challenge.", 15, 15, 0.54),5251 // 20x2053- NewMode("Easy 20x20", "20x20 grid, ~65% filled. Dense, constraining hints.", 20, 20, 0.65),54- NewMode("Medium 20x20", "20x20 grid, ~50% filled. Balanced challenge.", 20, 20, 0.50),55- NewMode("Hard 20x20", "20x20 grid, ~35% filled. Sparse, ambiguous hints.", 20, 20, 0.35),52+ NewMode("Epic", "20x20 grid, ~71% filled. A epic undertaking.", 20, 20, 0.71),53+ NewMode("Massive", "20x20 grid, ~56% filled. Truly massive puzzle.", 20, 20, 0.56),5654 }@@ -2,12 +2,26 @@ package nonogram
22 33 import (44 "context"5+ "fmt"56 "math/rand/v2"67 "strings"8+ "sync"79 "time"810 )911 10-const maxAttempts = 10012+const (13+ maxAttempts = 10014+ maxPossibilities = 1000015+)16+17+var (18+ possibilitiesCache = make(map[string][][]cellState)19+ possibilitiesMu sync.RWMutex20+)21+22+func cacheKey(length int, hint []int) string {23+ return fmt.Sprintf("%d:%v", length, hint)24+}1125 1226 func GenerateRandomTomography(mode NonogramMode) Hints {1327 maxDim := max(mode.Width, mode.Height)@@ -141,7 +155,7 @@ func countSolutionsRecursive(g solutionGrid, hints Hints, limit int, ctx context
141155 default:142156 }143157 144- propagated, valid := propagate(g, hints)158+ propagated, valid := propagate(g, hints, ctx)145159 if !valid {146160 return 0147161 }@@ -155,8 +169,9 @@ func countSolutionsRecursive(g solutionGrid, hints Hints, limit int, ctx context
155169 if propagated.cells[y][x] == cellUnknown {156170 count := 0157171 158- g1 := propagated.clone()159- if g1.set(x, y, cellFilled) {172+ if propagated.set(x, y, cellFilled) {173+ g1 := propagated.clone()174+ propagated.cells[y][x] = cellUnknown160175 n := countSolutionsRecursive(g1, hints, limit-count, ctx)161176 if n < 0 {162177 return n@@ -167,8 +182,9 @@ func countSolutionsRecursive(g solutionGrid, hints Hints, limit int, ctx context
167182 }168183 }169184 170- g2 := propagated.clone()171- if g2.set(x, y, cellEmpty) {185+ if propagated.set(x, y, cellEmpty) {186+ g2 := propagated.clone()187+ propagated.cells[y][x] = cellUnknown172188 n := countSolutionsRecursive(g2, hints, limit-count, ctx)173189 if n < 0 {174190 return n@@ -187,18 +203,25 @@ func countSolutionsRecursive(g solutionGrid, hints Hints, limit int, ctx context
187203 return 0188204 }189205 190-func propagate(g solutionGrid, hints Hints) (solutionGrid, bool) {206+func propagate(g solutionGrid, hints Hints, ctx context.Context) (solutionGrid, bool) {207+ rowBuf := make([]cellState, g.w)208+ colBuf := make([]cellState, g.h)209+191210 changed := true192211 for changed {212+ select {213+ case <-ctx.Done():214+ return g, false215+ default:216+ }193217 changed = false194218 195219 for y := range g.h {196- row := make([]cellState, g.w)197220 for x := range g.w {198- row[x] = g.cells[y][x]221+ rowBuf[x] = g.cells[y][x]199222 }200223 201- newRow, valid := propagateLine(row, hints.rows[y])224+ newRow, valid := propagateLine(rowBuf[:g.w], hints.rows[y])202225 if !valid {203226 return g, false204227 }@@ -212,12 +235,11 @@ func propagate(g solutionGrid, hints Hints) (solutionGrid, bool) {
212235 }213236 214237 for x := range g.w {215- col := make([]cellState, g.h)216238 for y := range g.h {217- col[y] = g.cells[y][x]239+ colBuf[y] = g.cells[y][x]218240 }219241 220- newCol, valid := propagateLine(col, hints.cols[x])242+ newCol, valid := propagateLine(colBuf[:g.h], hints.cols[x])221243 if !valid {222244 return g, false223245 }@@ -236,7 +258,11 @@ func propagate(g solutionGrid, hints Hints) (solutionGrid, bool) {
236258 237259 func propagateLine(line []cellState, hint []int) ([]cellState, bool) {238260 n := len(line)239- possibilities := generateLinePossibilities(n, hint)261+ possibilities := getCachedPossibilities(n, hint)262+263+ if len(possibilities) > maxPossibilities {264+ return line, true265+ }240266 241267 filtered := make([][]cellState, 0, len(possibilities))242268 for _, poss := range possibilities {@@ -273,6 +299,29 @@ func propagateLine(line []cellState, hint []int) ([]cellState, bool) {
273299 return result, true274300 }275301 302+func getCachedPossibilities(length int, hint []int) [][]cellState {303+ key := cacheKey(length, hint)304+305+ possibilitiesMu.RLock()306+ if cached, ok := possibilitiesCache[key]; ok {307+ possibilitiesMu.RUnlock()308+ return cached309+ }310+ possibilitiesMu.RUnlock()311+312+ possibilities := generateLinePossibilities(length, hint)313+314+ possibilitiesMu.Lock()315+ if cached, ok := possibilitiesCache[key]; ok {316+ possibilitiesMu.Unlock()317+ return cached318+ }319+ possibilitiesCache[key] = possibilities320+ possibilitiesMu.Unlock()321+322+ return possibilities323+}324+276325 func generateLinePossibilities(length int, hint []int) [][]cellState {277326 if len(hint) == 0 || (len(hint) == 1 && hint[0] == 0) {278327 result := make([]cellState, length)@@ -894,3 +894,50 @@ func TestGenerateRandomTomography_5x5_VerifyUnique(t *testing.T) {
894894 // Ensure the game import is used (it's needed for the init() registration side effect895895 // and the NewMode constructor uses game.NewBaseMode internally).896896 var _ game.Gamer = Model{}897+898+// --- Spawn Performance Benchmark (P2) ---899+900+func TestSpawnPerformance(t *testing.T) {901+ const numRuns = 20902+903+ modeConfigs := []struct {904+ name string905+ mode NonogramMode906+ }{907+ {"Mini", NewMode("Mini", "test", 5, 5, 0.65)},908+ {"Pocket", NewMode("Pocket", "test", 5, 5, 0.50)},909+ {"Teaser", NewMode("Teaser", "test", 5, 5, 0.35)},910+ {"Standard", NewMode("Standard", "test", 10, 10, 0.67)},911+ {"Classic", NewMode("Classic", "test", 10, 10, 0.52)},912+ {"Tricky", NewMode("Tricky", "test", 10, 10, 0.37)},913+ {"Large", NewMode("Large", "test", 15, 15, 0.69)},914+ {"Grand", NewMode("Grand", "test", 15, 15, 0.54)},915+ {"Epic", NewMode("Epic", "test", 20, 20, 0.71)},916+ {"Massive", NewMode("Massive", "test", 20, 20, 0.56)},917+ }918+919+ t.Run("average spawn time across all modes", func(t *testing.T) {920+ for _, cfg := range modeConfigs {921+ cfg := cfg922+ t.Run(cfg.name, func(t *testing.T) {923+ if cfg.mode.Width >= 15 && testing.Short() {924+ t.Skip("skipping large grids in short mode")925+ }926+927+ var totalDuration time.Duration928+ for i := 0; i < numRuns; i++ {929+ start := time.Now()930+ _, err := cfg.mode.Spawn()931+ if err != nil {932+ t.Fatalf("Spawn failed: %v", err)933+ }934+ totalDuration += time.Since(start)935+ }936+937+ avgNs := totalDuration.Nanoseconds() / numRuns938+ avgµs := float64(avgNs) / 1000939+ t.Logf("Average spawn time for %s: %.2fµs", cfg.name, avgµs)940+ })941+ }942+ })943+}