clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

bug/architecture-test-trim-path
puzzletea / games / shikaku / generation_invariants_test.go
generation_invariants_test.go 3.3K
1package shikaku23import (4 "math/rand/v2"5 "testing"6)78func TestPartitionGridCoverageBySize(t *testing.T) {9 for modeIndex, mode := range benchmarkShikakuModes() {10 t.Run(mode.Title(), func(t *testing.T) {11 grid := makeUncoveredGenerationGrid(mode.Width, mode.Height)12 partitions := make([]genRect, 0, mode.Width*mode.Height/2)1314 ok := partitionGrid(15 grid,16 mode.Width,17 mode.Height,18 mode.MaxRectSize,19 &partitions,20 rand.New(rand.NewPCG(uint64(modeIndex+700), uint64(modeIndex+701))),21 )22 if !ok {23 t.Fatal("partitionGrid returned false")24 }25 if len(partitions) == 0 {26 t.Fatal("expected at least one partition")27 }2829 covered := make([][]bool, mode.Height)30 for y := range mode.Height {31 covered[y] = make([]bool, mode.Width)32 }3334 totalArea := 035 for _, r := range partitions {36 if r.w <= 0 || r.h <= 0 {37 t.Fatalf("invalid partition size: %+v", r)38 }39 if r.x < 0 || r.y < 0 || r.x+r.w > mode.Width || r.y+r.h > mode.Height {40 t.Fatalf("partition out of bounds: %+v", r)41 }42 if r.w*r.h > mode.MaxRectSize {43 t.Fatalf("partition area %d exceeds max %d", r.w*r.h, mode.MaxRectSize)44 }4546 totalArea += r.w * r.h47 for dy := range r.h {48 for dx := range r.w {49 x, y := r.x+dx, r.y+dy50 if covered[y][x] {51 t.Fatalf("overlap at (%d,%d)", x, y)52 }53 covered[y][x] = true54 }55 }56 }5758 if totalArea != mode.Width*mode.Height {59 t.Fatalf("partition area sum=%d, want %d", totalArea, mode.Width*mode.Height)60 }6162 for y := range mode.Height {63 for x := range mode.Width {64 if !covered[y][x] {65 t.Fatalf("uncovered cell at (%d,%d)", x, y)66 }67 if grid[y][x] < 0 {68 t.Fatalf("grid cell (%d,%d) left uncovered in generation grid", x, y)69 }70 }71 }72 })73 }74}7576func TestGeneratePuzzleClueInvariantsBySize(t *testing.T) {77 for modeIndex, mode := range benchmarkShikakuModes() {78 t.Run(mode.Title(), func(t *testing.T) {79 puzzle, err := GeneratePuzzleSeeded(80 mode.Width,81 mode.Height,82 mode.MaxRectSize,83 rand.New(rand.NewPCG(uint64(modeIndex+800), uint64(modeIndex+801))),84 )85 if err != nil {86 t.Fatalf("GeneratePuzzleSeeded failed: %v", err)87 }88 if puzzle.Width != mode.Width || puzzle.Height != mode.Height {89 t.Fatalf("dimensions = %dx%d, want %dx%d", puzzle.Width, puzzle.Height, mode.Width, mode.Height)90 }91 if len(puzzle.Rectangles) != 0 {92 t.Fatalf("expected no pre-placed rectangles, got %d", len(puzzle.Rectangles))93 }9495 seenPos := make(map[[2]int]bool, len(puzzle.Clues))96 seenID := make(map[int]bool, len(puzzle.Clues))97 sum := 09899 for _, clue := range puzzle.Clues {100 if clue.X < 0 || clue.X >= mode.Width || clue.Y < 0 || clue.Y >= mode.Height {101 t.Fatalf("clue out of bounds: %+v", clue)102 }103 if clue.Value < 1 || clue.Value > mode.MaxRectSize {104 t.Fatalf("clue value out of range: %+v", clue)105 }106107 pos := [2]int{clue.X, clue.Y}108 if seenPos[pos] {109 t.Fatalf("duplicate clue position: (%d,%d)", clue.X, clue.Y)110 }111 seenPos[pos] = true112113 if seenID[clue.ID] {114 t.Fatalf("duplicate clue ID: %d", clue.ID)115 }116 seenID[clue.ID] = true117118 sum += clue.Value119 }120121 if sum != mode.Width*mode.Height {122 t.Fatalf("sum of clue values=%d, want %d", sum, mode.Width*mode.Height)123 }124 if len(seenID) != len(puzzle.Clues) {125 t.Fatal("clue IDs are not unique")126 }127 })128 }129}