clone

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

cleanup

Dami committed 7df3e87
commit
7df3e87262b51cb402cb85b23ca307f5f28d4369
parent
f65b7c4853af69f63940ac5c592a8ebf56d21fb9
author
Dami <dami.etoile@gmail.com>
17 files changed, +92 -629
Mfillomino/fillomino_test.go87+++-----------
Mfillomino/style.go188------------------------------
Mnonogram/generator.go5-
Mnonogram/nonogram_test.go18+-
Mnurikabe/generator.go4-
Mnurikabe/nurikabe_test.go10+-
Mnurikabe/solver.go8-
Mpdfexport/cover_art.go5-
Mpdfexport/cover_art_test.go3+-
Mrippleeffect/rippleeffect_test.go84++++---------
Mrippleeffect/style.go36------
Mshikaku/shikaku_test.go65+++-------
Mshikaku/style.go97---------------
Mtakuzu/generator.go18---
Mtakuzu/takuzu_test.go54+--------
Mwordsearch/generator.go15--
Mwordsearch/wordsearch_test.go24++--
M fillomino/fillomino_test.go +16 -71
@@ -193,67 +193,30 @@ func TestCellViewConflictedCursorIsDistinct(t *testing.T) {
193193 }194194 }195195 196-func TestDynamicGridEdges(t *testing.T) {197- g := grid{198- {2, 2, 1},199- {3, 3, 1},200- }201- h := grid{202- {4, 1},203- {4, 2},204- }205-206- if hasVerticalEdge(g, 1, 0) {207- t.Fatal("expected open interior edge inside a region")208- }209- if !hasVerticalEdge(g, 2, 0) {210- t.Fatal("expected border between different regions")211- }212- if horizontalEdge(h, 0, 1) {213- t.Fatal("expected open horizontal edge inside a region")214- }215- if !horizontalEdge(g, 0, 0) {216- t.Fatal("expected top outer border")217- }218-}219-220-func TestCompletedRegionGapBackgrounds(t *testing.T) {221- conflicts := validateGridState(grid{{4, 4}, {4, 4}}).conflicts196+func TestGridViewRendersRegionBoundaries(t *testing.T) {222197 m := Model{223- width: 2,198+ width: 3,224199 height: 2,225- grid: grid{{4, 4}, {4, 4}},226- conflicts: conflicts,200+ grid: grid{{2, 2, 1}, {2, 3, 1}},201+ provided: newProvidedMask(grid{{2, 2, 1}, {2, 3, 1}}),202+ conflicts: validateGridState(grid{{2, 2, 1}, {2, 3, 1}}).conflicts,227203 }228- completed := completedRegionBackgrounds(m.grid, m.conflicts)229- regionSet := map[point]struct{}{}230204 231- if got := verticalGapBackground(m, regionSet, completed, 1, 0); got == nil {232- t.Fatal("expected completed region to color vertical interior gap")233- }234- if got := horizontalGapBackground(m, regionSet, completed, 0, 1); got == nil {235- t.Fatal("expected completed region to color horizontal interior gap")205+ lines := strings.Split(ansi.Strip(gridView(m)), "\n")206+ content := []rune(lines[1])207+ if got := content[4]; got != ' ' {208+ t.Fatalf("interior separator inside region = %q, want space", got)236209 }237- if got := junctionGapBackground(m, regionSet, completed, 1, 1); got == nil {238- t.Fatal("expected completed region to color interior junction")210+ if got := content[8]; got != '│' {211+ t.Fatalf("separator between regions = %q, want vertical wall", got)239212 }240-}241213 242-func TestConflictGapBackgroundUsesConflictColor(t *testing.T) {243- conflicts := validateGridState(grid{{4, 4}, {4, 4}}).conflicts244- m := Model{245- width: 2,246- height: 2,247- grid: grid{{4, 4}, {4, 4}},248- conflicts: conflicts,214+ boundary := []rune(lines[2])215+ if got := boundary[1]; got != ' ' {216+ t.Fatalf("row separator inside region = %q, want space", got)249217 }250- m.conflicts[0][0] = true251-252- if got := verticalGapBackground(m, nil, nil, 1, 0); !sameColor(got, game.ConflictBG()) {253- t.Fatal("expected conflict color to bridge vertical interior gap")254- }255- if got := junctionGapBackground(m, nil, nil, 1, 1); !sameColor(got, game.ConflictBG()) {256- t.Fatal("expected conflict color to bridge interior junction")218+ if got := boundary[5]; got != '─' {219+ t.Fatalf("row separator between regions = %q, want horizontal wall", got)257220 }258221 }259222
@@ -531,24 +494,6 @@ func TestViewKeepsStableHeightAcrossCursorRegionInfoChanges(t *testing.T) {
531494 }532495 }533496 534-func TestSolvedGapBackgroundUsesSolvedColor(t *testing.T) {535- conflicts := validateGridState(grid{{2, 2}}).conflicts536- m := Model{537- width: 2,538- height: 1,539- grid: grid{{2, 2}},540- conflicts: conflicts,541- solved: true,542- }543- got := verticalGapBackground(m, nil, nil, 1, 0)544- if got == nil {545- t.Fatal("expected solved gap background")546- }547- if !sameColor(got, theme.Current().SuccessBG) {548- t.Fatal("expected solved gap background to use solved color")549- }550-}551-552497 func TestScreenToGrid(t *testing.T) {553498 m := Model{554499 width: 3,
M fillomino/style.go +0 -188
@@ -305,194 +305,6 @@ func buildRenderGridState(m Model) renderGridState {
305305 }306306 }307307 308-func horizontalEdge(g grid, x, y int) bool {309- switch {310- case y <= 0, y >= len(g):311- return true312- default:313- top := point{x: x, y: y - 1}314- bottom := point{x: x, y: y}315- return !sameRegion(g, top, bottom)316- }317-}318-319-func hasVerticalEdge(g grid, x, y int) bool {320- width := len(g[0])321- switch {322- case x <= 0, x >= width:323- return true324- default:325- left := point{x: x - 1, y: y}326- right := point{x: x, y: y}327- return !sameRegion(g, left, right)328- }329-}330-331-func junctionRune(g grid, x, y int) rune {332- height := len(g)333- width := len(g[0])334- north := y > 0 && hasVerticalEdge(g, x, y-1)335- south := y < height && hasVerticalEdge(g, x, y)336- west := x > 0 && horizontalEdge(g, x-1, y)337- east := x < width && horizontalEdge(g, x, y)338-339- switch {340- case north && south && west && east:341- return '┼'342- case north && south && west:343- return '┤'344- case north && south && east:345- return '├'346- case west && east && north:347- return '┴'348- case west && east && south:349- return '┬'350- case south && east:351- return '┌'352- case south && west:353- return '┐'354- case north && east:355- return '└'356- case north && west:357- return '┘'358- case north || south:359- return '│'360- case west || east:361- return '─'362- default:363- return ' '364- }365-}366-367-func verticalGapBackground(m Model, regionSet map[point]struct{}, completed map[point]color.Color, x, y int) color.Color {368- if hasVerticalEdge(m.grid, x, y) || x <= 0 || x >= m.width {369- return nil370- }371- left := point{x: x - 1, y: y}372- right := point{x: x, y: y}373- return gapBackground(m, regionSet, completed, left, right)374-}375-376-func horizontalGapBackground(m Model, regionSet map[point]struct{}, completed map[point]color.Color, x, y int) color.Color {377- if horizontalEdge(m.grid, x, y) || y <= 0 || y >= m.height {378- return nil379- }380- top := point{x: x, y: y - 1}381- bottom := point{x: x, y: y}382- return gapBackground(m, regionSet, completed, top, bottom)383-}384-385-func junctionGapBackground(m Model, regionSet map[point]struct{}, completed map[point]color.Color, x, y int) color.Color {386- if junctionRune(m.grid, x, y) != ' ' {387- return nil388- }389-390- cells := make([]point, 0, 4)391- if x > 0 && y > 0 {392- cells = append(cells, point{x: x - 1, y: y - 1})393- }394- if x < m.width && y > 0 {395- cells = append(cells, point{x: x, y: y - 1})396- }397- if x > 0 && y < m.height {398- cells = append(cells, point{x: x - 1, y: y})399- }400- if x < m.width && y < m.height {401- cells = append(cells, point{x: x, y: y})402- }403- if len(cells) != 4 {404- return nil405- }406- for i := 1; i < len(cells); i++ {407- if !sameRegion(m.grid, cells[0], cells[i]) {408- return nil409- }410- }411- return gapBackground(m, regionSet, completed, cells...)412-}413-414-func gapBackground(m Model, regionSet map[point]struct{}, completed map[point]color.Color, cells ...point) color.Color {415- p := theme.Current()416- if len(cells) == 0 {417- return nil418- }419- if anyCellConflict(m.conflicts, cells...) {420- return game.ConflictBG()421- }422- if m.solved {423- return p.SuccessBG424- }425- if bg := completed[cells[0]]; bg != nil {426- for _, cell := range cells[1:] {427- if completed[cell] == nil {428- return nil429- }430- }431- return bg432- }433- for _, cell := range cells {434- if _, ok := regionSet[cell]; !ok {435- return nil436- }437- }438- if len(regionSet) == 0 {439- return nil440- }441- return p.HighlightBG442-}443-444-func anyCellConflict(conflicts [][]bool, cells ...point) bool {445- for _, cell := range cells {446- if conflicts[cell.y][cell.x] {447- return true448- }449- }450- return false451-}452-453-func sameRegion(g grid, a, b point) bool {454- return g[a.y][a.x] != 0 && g[a.y][a.x] == g[b.y][b.x]455-}456-457-func completedRegionBackgrounds(g grid, conflicts [][]bool) map[point]color.Color {458- height := len(g)459- if height == 0 {460- return nil461- }462- width := len(g[0])463- visited := make([][]bool, height)464- for y := range height {465- visited[y] = make([]bool, width)466- }467-468- palette := theme.Current()469- colors := palette.ThemeColors()470- if len(colors) == 0 {471- return nil472- }473-474- backgrounds := make(map[point]color.Color)475- for y := range height {476- for x := range width {477- if g[y][x] == 0 || visited[y][x] {478- continue479- }480-481- comp := buildComponent(g, point{x: x, y: y}, visited)482- if len(comp.cells) != comp.value || componentHasConflict(comp, conflicts) {483- continue484- }485-486- bg := completedRegionColor(comp, colors, palette.Surface)487- for _, cell := range comp.cells {488- backgrounds[cell] = bg489- }490- }491- }492-493- return backgrounds494-}495-496308 func componentHasConflict(comp component, conflicts [][]bool) bool {497309 for _, cell := range comp.cells {498310 if conflicts[cell.y][cell.x] {
M nonogram/generator.go +0 -5
M nonogram/nonogram_test.go +9 -9
@@ -296,8 +296,8 @@ func TestGenerateRandomState(t *testing.T) {
296296 t.Run("density clamping low", func(t *testing.T) {297297 // Use a larger grid and multiple runs to avoid flakiness from per-row jitter.298298 totalFilled := 0299- for range 10 {300- s := generateRandomState(10, 10, 0.0)299+ for i := range 10 {300+ s := generateRandomStateSeeded(10, 10, 0.0, rand.New(rand.NewPCG(uint64(i+1), uint64(i+101))))301301 g := newGrid(s)302302 for _, row := range g {303303 for _, r := range row {
@@ -314,8 +314,8 @@ func TestGenerateRandomState(t *testing.T) {
314314 315315 t.Run("density clamping high", func(t *testing.T) {316316 totalEmpty := 0317- for range 10 {318- s := generateRandomState(10, 10, 1.0)317+ for i := range 10 {318+ s := generateRandomStateSeeded(10, 10, 1.0, rand.New(rand.NewPCG(uint64(i+21), uint64(i+121))))319319 g := newGrid(s)320320 for _, row := range g {321321 for _, r := range row {
@@ -331,7 +331,7 @@ func TestGenerateRandomState(t *testing.T) {
331331 })332332 333333 t.Run("correct dimensions", func(t *testing.T) {334- s := generateRandomState(5, 10, 0.5)334+ s := generateRandomStateSeeded(5, 10, 0.5, rand.New(rand.NewPCG(41, 141)))335335 g := newGrid(s)336336 if len(g) != 5 {337337 t.Fatalf("expected 5 rows, got %d", len(g))
@@ -344,10 +344,10 @@ func TestGenerateRandomState(t *testing.T) {
344344 })345345 346346 t.Run("0 dimensions", func(t *testing.T) {347- if s := generateRandomState(0, 5, 0.5); s != "" {347+ if s := generateRandomStateSeeded(0, 5, 0.5, rand.New(rand.NewPCG(51, 151))); s != "" {348348 t.Errorf("expected empty string for h=0, got %q", s)349349 }350- if s := generateRandomState(5, 0, 0.5); s != "" {350+ if s := generateRandomStateSeeded(5, 0, 0.5, rand.New(rand.NewPCG(52, 152))); s != "" {351351 t.Errorf("expected empty string for w=0, got %q", s)352352 }353353 })
@@ -356,8 +356,8 @@ func TestGenerateRandomState(t *testing.T) {
356356 totalFilled := 0357357 totalCells := 0358358 runs := 20359- for range runs {360- s := generateRandomState(10, 10, 0.5)359+ for i := range runs {360+ s := generateRandomStateSeeded(10, 10, 0.5, rand.New(rand.NewPCG(uint64(i+61), uint64(i+161))))361361 g := newGrid(s)362362 for _, row := range g {363363 for _, r := range row {
M nurikabe/generator.go +0 -4
M nurikabe/nurikabe_test.go +5 -5
@@ -149,7 +149,7 @@ func TestSeaConnectivity(t *testing.T) {
149149 []cellState{islandCell, seaCell, islandCell},150150 []cellState{islandCell, seaCell, seaCell},151151 }152- if !isSeaConnected(connected) {152+ if !isSeaConnectedWithScratch(connected, newSolverScratch(len(connected[0]), len(connected))) {153153 t.Fatal("expected connected sea")154154 }155155
@@ -158,7 +158,7 @@ func TestSeaConnectivity(t *testing.T) {
158158 []cellState{islandCell, islandCell, islandCell},159159 []cellState{seaCell, islandCell, seaCell},160160 }161- if isSeaConnected(disconnected) {161+ if isSeaConnectedWithScratch(disconnected, newSolverScratch(len(disconnected[0]), len(disconnected))) {162162 t.Fatal("expected disconnected sea")163163 }164164 }
@@ -895,7 +895,7 @@ func TestGenerateSeededReturnsSolvable(t *testing.T) {
895895 t.Fatalf("invalid generated puzzle clues: %v", err)896896 }897897 898- nodeLimit := max(300000, generationNodeLimit(mode)*8)898+ nodeLimit := max(300000, modeGenerationBudget(mode).solvabilityNodes*8)899899 count, _, err := CountSolutionsContext(context.Background(), p, 1, nodeLimit)900900 if err != nil {901901 if errors.Is(err, errNodeLimit) {
@@ -966,7 +966,7 @@ func TestUniqueFirstFallbackPolicy(t *testing.T) {
966966 t.Fatalf("GenerateSeeded error: %v", err)967967 }968968 969- count, _, err := CountSolutionsContext(context.Background(), p, 2, generationNodeLimit(mode))969+ count, _, err := CountSolutionsContext(context.Background(), p, 2, modeGenerationBudget(mode).solvabilityNodes)970970 if err != nil {971971 t.Fatalf("CountSolutionsContext error: %v", err)972972 }
@@ -1105,7 +1105,7 @@ func BenchmarkCountSolutions(b *testing.B) {
11051105 {11061106 name: "GeneratedEasy",11071107 puzzle: easyPuzzle,1108- nodeLimit: generationNodeLimit(easyMode) * 4,1108+ nodeLimit: modeGenerationBudget(easyMode).solvabilityNodes * 4,11091109 },11101110 }11111111
M nurikabe/solver.go +0 -8
M pdfexport/cover_art.go +0 -5
M pdfexport/cover_art_test.go +2 -1
M rippleeffect/rippleeffect_test.go +27 -57
@@ -11,6 +11,7 @@ import (
1111 "charm.land/lipgloss/v2"1212 "github.com/FelineStateMachine/puzzletea/game"1313 "github.com/FelineStateMachine/puzzletea/theme"14+ "github.com/charmbracelet/x/ansi"1415 )1516 1617 func sampleCages() []Cage {
@@ -405,35 +406,7 @@ func TestSolvedBridgeBackgroundsUseSolvedColor(t *testing.T) {
405406 }406407 }407408 408-func TestCompletedGapBackgrounds(t *testing.T) {409- geo, err := buildGeometry(3, 3, sampleCages())410- if err != nil {411- t.Fatal(err)412- }413-414- bg := color.NRGBA{R: 10, G: 20, B: 30, A: 255}415- completed := map[int]color.Color{0: bg}416-417- if got := verticalGapBackground(geo, completed, 1, 0); !sameColor(got, bg) {418- t.Fatal("expected vertical interior gap to inherit completed cage background")419- }420- if got := horizontalGapBackground(geo, completed, 1, 1); got != nil {421- t.Fatal("expected cage boundary row to keep nil background")422- }423-424- squareGeo, err := buildGeometry(2, 2, []Cage{425- {ID: 0, Size: 4, Cells: []Cell{{0, 0}, {1, 0}, {0, 1}, {1, 1}}},426- })427- if err != nil {428- t.Fatal(err)429- }430- completed = map[int]color.Color{0: bg}431- if got := junctionGapBackground(squareGeo, completed, 1, 1); !sameColor(got, bg) {432- t.Fatal("expected empty junction inside completed cage to inherit background")433- }434-}435-436-func TestConflictBridgeBackgrounds(t *testing.T) {409+func TestGridViewUsesCageBoundaries(t *testing.T) {437410 geo, err := buildGeometry(3, 3, sampleCages())438411 if err != nil {439412 t.Fatal(err)
@@ -442,42 +415,39 @@ func TestConflictBridgeBackgrounds(t *testing.T) {
442415 m := Model{443416 width: 3,444417 height: 3,445- grid: sampleSolution(),446- givens: grid{{0, 0, 0}, {0, 0, 0}, {0, 0, 0}},418+ grid: newGrid(3, 3),419+ givens: newGrid(3, 3),447420 conflicts: newConflictGrid(3, 3),421+ cursor: pointCursor(0, 0),448422 geo: geo,449423 }450- m.conflicts[0][0] = true451424 452- bridge := bridgeBackgrounds(m, nil)453- if got := verticalGapBackground(geo, bridge, 1, 0); !sameColor(got, game.ConflictBG()) {454- t.Fatal("expected conflict color to bridge vertical gap")425+ lines := strings.Split(ansi.Strip(gridView(m)), "\n")426+ content := []rune(lines[1])427+ if got := content[4]; got != ' ' {428+ t.Fatalf("interior separator inside cage = %q, want space", got)455429 }456-}457-458-func TestActiveCageBridgeBackgrounds(t *testing.T) {459- geo, err := buildGeometry(3, 3, sampleCages())460- if err != nil {461- t.Fatal(err)430+ boundary := []rune(lines[2])431+ if got := boundary[1]; got != '─' {432+ t.Fatalf("separator between cages = %q, want horizontal wall", got)462433 }434+}463435 464- m := Model{465- width: 3,466- height: 3,467- grid: newGrid(3, 3),468- givens: newGrid(3, 3),469- conflicts: newConflictGrid(3, 3),470- cursor: pointCursor(1, 0),471- geo: geo,472- }436+func TestBridgeFillDefersToUniformZoneBackground(t *testing.T) {437+ m := Model{}438+ bridgeBG := map[int]color.Color{7: theme.Current().SelectionBG}473439 474- bridge := bridgeBackgrounds(m, nil)475- want := theme.Current().SelectionBG476- if got := verticalGapBackground(geo, bridge, 1, 0); !sameColor(got, want) {477- t.Fatal("expected active cage highlight to bridge vertical gap")478- }479- if got := horizontalGapBackground(geo, bridge, 1, 1); got != nil {480- t.Fatal("expected cage boundary row to keep nil background for hover highlight")440+ if got := bridgeFill(m, bridgeBG, game.DynamicGridBridge{441+ Kind: game.DynamicGridBridgeVertical,442+ Zone: 7,443+ Uniform: true,444+ Count: 2,445+ Cells: [4]image.Point{446+ {X: 0, Y: 0},447+ {X: 1, Y: 0},448+ },449+ }); got != nil {450+ t.Fatal("expected uniform bridge to defer to zone background")481451 }482452 }483453
M rippleeffect/style.go +0 -36
M shikaku/shikaku_test.go +20 -45
@@ -2,7 +2,6 @@ package shikaku
22 33 import (44 "encoding/json"5- "image/color"65 "math/rand/v2"76 "strings"87 "testing"
@@ -10,6 +9,7 @@ import (
109 tea "charm.land/bubbletea/v2"1110 "charm.land/lipgloss/v2"1211 "github.com/FelineStateMachine/puzzletea/game"12+ "github.com/charmbracelet/x/ansi"1313 )1414 1515 // --- helpers ---
@@ -863,7 +863,7 @@ func TestMouseRightClickDeletesRectangle(t *testing.T) {
863863 }864864 }865865 866-func TestDynamicGridEdges(t *testing.T) {866+func TestGridViewRendersCommittedRectangleBoundaries(t *testing.T) {867867 m := Model{868868 puzzle: Puzzle{869869 Width: 4,
@@ -876,18 +876,22 @@ func TestDynamicGridEdges(t *testing.T) {
876876 },877877 }878878 879- if hasVerticalEdge(m, nil, 1, 0) {880- t.Fatal("expected open interior edge inside committed rectangle")879+ lines := strings.Split(ansi.Strip(gridView(m, false)), "\n")880+ content := []rune(lines[1])881+ if got := content[4]; got != ' ' {882+ t.Fatalf("interior separator inside rectangle = %q, want space", got)881883 }882- if !hasVerticalEdge(m, nil, 2, 0) {883- t.Fatal("expected wall between committed rectangles")884+ if got := content[8]; got != '│' {885+ t.Fatalf("separator between rectangles = %q, want vertical wall", got)884886 }885- if horizontalEdge(m, nil, 0, 1) {886- t.Fatal("expected open horizontal edge inside committed rectangle")887+888+ boundary := []rune(lines[2])889+ if got := boundary[1]; got != ' ' {890+ t.Fatalf("row separator inside rectangle = %q, want space", got)887891 }888892 }889893 890-func TestPreviewOverridesCommittedWalls(t *testing.T) {894+func TestGridViewPreviewRemovesCommittedWall(t *testing.T) {891895 preview := &Rectangle{X: 0, Y: 0, W: 2, H: 1}892896 m := Model{893897 puzzle: Puzzle{
@@ -902,35 +906,15 @@ func TestPreviewOverridesCommittedWalls(t *testing.T) {
902906 mousePreview: preview,903907 }904908 905- if !hasVerticalEdge(m, nil, 1, 0) {906- t.Fatal("expected committed wall before preview overlay")907- }908- if hasVerticalEdge(m, preview, 1, 0) {909- t.Fatal("expected preview to remove interior committed wall")909+ withPreview := []rune(strings.Split(ansi.Strip(gridView(m, false)), "\n")[1])910+ if got := withPreview[4]; got != ' ' {911+ t.Fatalf("preview separator = %q, want space", got)910912 }911-}912913 913-func TestDynamicGridGapBackgrounds(t *testing.T) {914- m := Model{915- puzzle: Puzzle{916- Width: 2,917- Height: 2,918- Clues: []Clue{{ID: 0, X: 0, Y: 0, Value: 4}},919- Rectangles: []Rectangle{920- {ClueID: 0, X: 0, Y: 0, W: 2, H: 2},921- },922- },923- }924- bg := rectColors()[0]925-926- if got := verticalGapBackground(m, nil, nil, false, 1, 0); !sameColor(got, bg) {927- t.Fatal("expected committed rectangle to color vertical interior gap")928- }929- if got := horizontalGapBackground(m, nil, nil, false, 0, 1); !sameColor(got, bg) {930- t.Fatal("expected committed rectangle to color horizontal interior gap")931- }932- if got := junctionGapBackground(m, nil, nil, false, 1, 1); !sameColor(got, bg) {933- t.Fatal("expected committed rectangle to color interior junction")914+ m.mousePreview = nil915+ withoutPreview := []rune(strings.Split(ansi.Strip(gridView(m, false)), "\n")[1])916+ if got := withoutPreview[4]; got != '│' {917+ t.Fatalf("committed separator = %q, want vertical wall", got)934918 }935919 }936920
@@ -1153,15 +1137,6 @@ func TestModeSpawn(t *testing.T) {
11531137 })11541138 }11551139 1156-func sameColor(a, b color.Color) bool {1157- if a == nil || b == nil {1158- return a == b1159- }1160- ar, ag, ab, aa := a.RGBA()1161- br, bg, bb, ba := b.RGBA()1162- return ar == br && ag == bg && ab == bb && aa == ba1163-}1164-11651140 func cellScreenCoords(m *Model, col, row int) (int, int) {11661141 ox, oy := m.gridOrigin()11671142 return ox + col*(cellWidth+1), oy + row*2
M shikaku/style.go +0 -97
@@ -128,103 +128,6 @@ func regionTokenAt(m Model, preview *Rectangle, x, y int) int {
128128 return unownedRegionID129129 }130130 131-func horizontalEdge(m Model, preview *Rectangle, x, y int) bool {132- switch {133- case y <= 0, y >= m.puzzle.Height:134- return true135- default:136- return regionTokenAt(m, preview, x, y-1) != regionTokenAt(m, preview, x, y)137- }138-}139-140-func hasVerticalEdge(m Model, preview *Rectangle, x, y int) bool {141- switch {142- case x <= 0, x >= m.puzzle.Width:143- return true144- default:145- return regionTokenAt(m, preview, x-1, y) != regionTokenAt(m, preview, x, y)146- }147-}148-149-func junctionRune(m Model, preview *Rectangle, x, y int) rune {150- north := y > 0 && hasVerticalEdge(m, preview, x, y-1)151- south := y < m.puzzle.Height && hasVerticalEdge(m, preview, x, y)152- west := x > 0 && horizontalEdge(m, preview, x-1, y)153- east := x < m.puzzle.Width && horizontalEdge(m, preview, x, y)154-155- switch {156- case north && south && west && east:157- return '┼'158- case north && south && west:159- return '┤'160- case north && south && east:161- return '├'162- case west && east && north:163- return '┴'164- case west && east && south:165- return '┬'166- case south && east:167- return '┌'168- case south && west:169- return '┐'170- case north && east:171- return '└'172- case north && west:173- return '┘'174- case north || south:175- return '│'176- case west || east:177- return '─'178- default:179- return ' '180- }181-}182-183-func verticalGapBackground(m Model, preview *Rectangle, previewClue *Clue, previewValid bool, x, y int) color.Color {184- if hasVerticalEdge(m, preview, x, y) || x <= 0 || x >= m.puzzle.Width {185- return nil186- }187- token := regionTokenAt(m, preview, x-1, y)188- return regionBackground(m, token, previewClue, previewValid)189-}190-191-func horizontalGapBackground(m Model, preview *Rectangle, previewClue *Clue, previewValid bool, x, y int) color.Color {192- if horizontalEdge(m, preview, x, y) || y <= 0 || y >= m.puzzle.Height {193- return nil194- }195- token := regionTokenAt(m, preview, x, y-1)196- return regionBackground(m, token, previewClue, previewValid)197-}198-199-func junctionGapBackground(m Model, preview *Rectangle, previewClue *Clue, previewValid bool, x, y int) color.Color {200- if junctionRune(m, preview, x, y) != ' ' {201- return nil202- }203-204- tokens := make([]int, 0, 4)205- if x > 0 && y > 0 {206- tokens = append(tokens, regionTokenAt(m, preview, x-1, y-1))207- }208- if x < m.puzzle.Width && y > 0 {209- tokens = append(tokens, regionTokenAt(m, preview, x, y-1))210- }211- if x > 0 && y < m.puzzle.Height {212- tokens = append(tokens, regionTokenAt(m, preview, x-1, y))213- }214- if x < m.puzzle.Width && y < m.puzzle.Height {215- tokens = append(tokens, regionTokenAt(m, preview, x, y))216- }217- if len(tokens) != 4 {218- return nil219- }220- for _, token := range tokens[1:] {221- if token != tokens[0] {222- return nil223- }224- }225- return regionBackground(m, tokens[0], previewClue, previewValid)226-}227-228131 func regionBackground(m Model, token int, previewClue *Clue, previewValid bool) color.Color {229132 p := theme.Current()230133 switch token {
M takuzu/generator.go +0 -18
M takuzu/takuzu_test.go +1 -53
@@ -314,7 +314,7 @@ func TestColEqualWith(t *testing.T) {
314314 })315315 }316316 317-// --- rowFilled / colFilled / rowFilledExcept / colFilledExcept (P1) ---317+// --- rowFilled / colFilled (P1) ---318318 319319 func TestRowFilled(t *testing.T) {320320 g := makeGrid6(
@@ -369,58 +369,6 @@ func TestColFilled(t *testing.T) {
369369 })370370 }371371 372-func TestRowFilledExcept(t *testing.T) {373- g := makeGrid6(374- "00101.",375- "0.1...",376- "......",377- "......",378- "......",379- "......",380- )381-382- t.Run("skip is only empty", func(t *testing.T) {383- if !rowFilledExcept(g, 0, 5, 6) {384- t.Error("rowFilledExcept should return true when skip is only empty cell")385- }386- })387-388- t.Run("another empty exists", func(t *testing.T) {389- if rowFilledExcept(g, 1, 1, 6) {390- t.Error("rowFilledExcept should return false when other empties exist")391- }392- })393-394- t.Run("all empty except skip", func(t *testing.T) {395- if rowFilledExcept(g, 2, 0, 6) {396- t.Error("rowFilledExcept should return false for all-empty row")397- }398- })399-}400-401-func TestColFilledExcept(t *testing.T) {402- g := makeGrid6(403- "0.....",404- "0.....",405- "......",406- "1.....",407- "0.....",408- "1.....",409- )410-411- t.Run("skip is only empty", func(t *testing.T) {412- if !colFilledExcept(g, 0, 2, 6) {413- t.Error("colFilledExcept should return true when skip is only empty")414- }415- })416-417- t.Run("another empty exists", func(t *testing.T) {418- if colFilledExcept(g, 1, 0, 6) {419- t.Error("colFilledExcept should return false when column has other empties")420- }421- })422-}423-424372 // --- checkConstraints (P0) ---425373 426374 func TestCheckConstraints(t *testing.T) {
M wordsearch/generator.go +0 -15
M wordsearch/wordsearch_test.go +12 -12
@@ -566,7 +566,7 @@ func TestReverseString(t *testing.T) {
566566 func TestTryPlaceWord(t *testing.T) {567567 t.Run("places word in empty grid", func(t *testing.T) {568568 g := createEmptyGrid(10, 10)569- w := tryPlaceWord(g, "HELLO", []Direction{Right, Down, DownRight}, 1000)569+ w := tryPlaceWordSeeded(g, "HELLO", []Direction{Right, Down, DownRight}, 1000, testRNG(1))570570 if w == nil {571571 t.Fatal("expected word to be placed")572572 }
@@ -583,7 +583,7 @@ func TestTryPlaceWord(t *testing.T) {
583583 584584 t.Run("respects allowed directions", func(t *testing.T) {585585 g := createEmptyGrid(10, 10)586- w := tryPlaceWord(g, "HELLO", []Direction{Right}, 1000)586+ w := tryPlaceWordSeeded(g, "HELLO", []Direction{Right}, 1000, testRNG(2))587587 if w == nil {588588 t.Fatal("expected word to be placed")589589 }
@@ -595,7 +595,7 @@ func TestTryPlaceWord(t *testing.T) {
595595 t.Run("allows letter overlap", func(t *testing.T) {596596 g := createEmptyGrid(1, 5)597597 g.Set(0, 0, 'H')598- w := tryPlaceWord(g, "HELLO", []Direction{Right}, 1000)598+ w := tryPlaceWordSeeded(g, "HELLO", []Direction{Right}, 1000, testRNG(3))599599 if w == nil {600600 t.Fatal("expected word to be placed with matching overlap")601601 }
@@ -606,7 +606,7 @@ func TestTryPlaceWord(t *testing.T) {
606606 for x := range 5 {607607 g.Set(x, 0, 'X')608608 }609- w := tryPlaceWord(g, "HELLO", []Direction{Right}, 100)609+ w := tryPlaceWordSeeded(g, "HELLO", []Direction{Right}, 100, testRNG(4))610610 if w != nil {611611 t.Error("expected nil for conflicting overlap")612612 }
@@ -614,7 +614,7 @@ func TestTryPlaceWord(t *testing.T) {
614614 615615 t.Run("returns nil when impossible", func(t *testing.T) {616616 g := createEmptyGrid(3, 3)617- w := tryPlaceWord(g, "TOOLONGWORD", []Direction{Right, Down}, 100)617+ w := tryPlaceWordSeeded(g, "TOOLONGWORD", []Direction{Right, Down}, 100, testRNG(5))618618 if w != nil {619619 t.Error("expected nil for word that cannot fit")620620 }
@@ -622,7 +622,7 @@ func TestTryPlaceWord(t *testing.T) {
622622 623623 t.Run("uppercases text", func(t *testing.T) {624624 g := createEmptyGrid(10, 10)625- w := tryPlaceWord(g, "hello", []Direction{Right}, 1000)625+ w := tryPlaceWordSeeded(g, "hello", []Direction{Right}, 1000, testRNG(6))626626 if w == nil {627627 t.Fatal("expected word to be placed")628628 }
@@ -658,14 +658,14 @@ func TestTryPlaceWordSeededWithFallback(t *testing.T) {
658658 659659 func TestSelectWords(t *testing.T) {660660 t.Run("respects count", func(t *testing.T) {661- words := selectWords(5, 4, 6)661+ words := selectWordsSeeded(5, 4, 6, testRNG(7))662662 if len(words) > 5 {663663 t.Errorf("len(selectWords) = %d, want <= 5", len(words))664664 }665665 })666666 667667 t.Run("respects length filter", func(t *testing.T) {668- words := selectWords(20, 4, 6)668+ words := selectWordsSeeded(20, 4, 6, testRNG(8))669669 for _, w := range words {670670 if len(w) < 4 || len(w) > 6 {671671 t.Errorf("word %q has length %d, want 4-6", w, len(w))
@@ -674,7 +674,7 @@ func TestSelectWords(t *testing.T) {
674674 })675675 676676 t.Run("clamps when too few", func(t *testing.T) {677- words := selectWords(99999, 4, 4)677+ words := selectWordsSeeded(99999, 4, 4, testRNG(9))678678 // Should return all 4-letter words, not 99999.679679 if len(words) > 99999 {680680 t.Error("returned more than requested")
@@ -715,7 +715,7 @@ func TestOrderWordsForPlacement(t *testing.T) {
715715 func TestFillEmptyCells(t *testing.T) {716716 t.Run("fills all spaces", func(t *testing.T) {717717 g := createEmptyGrid(5, 5)718- fillEmptyCells(g)718+ fillEmptyCellsSeeded(g, testRNG(10))719719 for y := range g {720720 for x := range g[y] {721721 if g[y][x] == ' ' {
@@ -728,7 +728,7 @@ func TestFillEmptyCells(t *testing.T) {
728728 t.Run("preserves existing letters", func(t *testing.T) {729729 g := createEmptyGrid(5, 5)730730 g.Set(0, 0, 'Z')731- fillEmptyCells(g)731+ fillEmptyCellsSeeded(g, testRNG(11))732732 if got := g.Get(0, 0); got != 'Z' {733733 t.Errorf("cell (0,0) = %c, want Z", got)734734 }
@@ -736,7 +736,7 @@ func TestFillEmptyCells(t *testing.T) {
736736 737737 t.Run("only uppercase letters", func(t *testing.T) {738738 g := createEmptyGrid(5, 5)739- fillEmptyCells(g)739+ fillEmptyCellsSeeded(g, testRNG(12))740740 for y := range g {741741 for x := range g[y] {742742 c := g[y][x]