clone

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

chore/regenerate-vhs-gifs
puzzletea / hashiwokakero / hashiwokakero_test.go
hashiwokakero_test.go 37.1K
1package hashiwokakero23import (4 "encoding/json"5 "image"6 "image/color"7 "strings"8 "testing"910 tea "charm.land/bubbletea/v2"11 "charm.land/lipgloss/v2"12 "github.com/FelineStateMachine/puzzletea/game"13 "github.com/FelineStateMachine/puzzletea/theme"14 "github.com/charmbracelet/x/ansi"15)1617// --- helpers ---1819// fourCornerPuzzle builds a 4x4 puzzle with islands at the four corners:20//21// 0 . . 122// . . . .23// . . . .24// 2 . . 325func fourCornerPuzzle() *Puzzle {26 return &Puzzle{27 Width: 4,28 Height: 4,29 Islands: []Island{30 {ID: 0, X: 0, Y: 0, Required: 2},31 {ID: 1, X: 3, Y: 0, Required: 2},32 {ID: 2, X: 0, Y: 3, Required: 2},33 {ID: 3, X: 3, Y: 3, Required: 2},34 },35 }36}3738// linePuzzle builds a horizontal line of islands: 0 -- 1 -- 2 -- 339func linePuzzle() *Puzzle {40 return &Puzzle{41 Width: 7,42 Height: 1,43 Islands: []Island{44 {ID: 0, X: 0, Y: 0, Required: 1},45 {ID: 1, X: 2, Y: 0, Required: 2},46 {ID: 2, X: 4, Y: 0, Required: 2},47 {ID: 3, X: 6, Y: 0, Required: 1},48 },49 }50}5152// --- GetBridge (P0) ---5354func TestGetBridge(t *testing.T) {55 t.Run("bridge exists", func(t *testing.T) {56 p := fourCornerPuzzle()57 p.SetBridge(0, 1, 1)5859 b := p.GetBridge(0, 1)60 if b == nil {61 t.Fatal("expected bridge, got nil")62 }63 if b.Count != 1 {64 t.Errorf("Count = %d, want 1", b.Count)65 }66 })6768 t.Run("order independence", func(t *testing.T) {69 p := fourCornerPuzzle()70 p.SetBridge(0, 1, 1)7172 b := p.GetBridge(1, 0)73 if b == nil {74 t.Fatal("GetBridge(1,0) should find bridge set as (0,1)")75 }76 if b.Count != 1 {77 t.Errorf("Count = %d, want 1", b.Count)78 }79 })8081 t.Run("no bridge", func(t *testing.T) {82 p := fourCornerPuzzle()83 p.SetBridge(0, 1, 1)8485 if b := p.GetBridge(0, 2); b != nil {86 t.Errorf("expected nil, got bridge with Count=%d", b.Count)87 }88 })89}9091// --- SetBridge (P0) ---9293func TestSetBridge(t *testing.T) {94 t.Run("create single bridge", func(t *testing.T) {95 p := fourCornerPuzzle()96 p.SetBridge(0, 1, 1)9798 b := p.GetBridge(0, 1)99 if b == nil || b.Count != 1 {100 t.Fatalf("expected Count=1, got %v", b)101 }102 })103104 t.Run("create double bridge", func(t *testing.T) {105 p := fourCornerPuzzle()106 p.SetBridge(0, 1, 2)107108 b := p.GetBridge(0, 1)109 if b == nil || b.Count != 2 {110 t.Fatalf("expected Count=2, got %v", b)111 }112 })113114 t.Run("update bridge", func(t *testing.T) {115 p := fourCornerPuzzle()116 p.SetBridge(0, 1, 1)117 p.SetBridge(0, 1, 2)118119 b := p.GetBridge(0, 1)120 if b == nil || b.Count != 2 {121 t.Fatalf("expected Count=2 after update, got %v", b)122 }123 })124125 t.Run("remove bridge", func(t *testing.T) {126 p := fourCornerPuzzle()127 p.SetBridge(0, 1, 1)128 p.SetBridge(0, 1, 0)129130 if b := p.GetBridge(0, 1); b != nil {131 t.Errorf("expected bridge removed, got Count=%d", b.Count)132 }133 })134135 t.Run("remove nonexistent", func(t *testing.T) {136 p := fourCornerPuzzle()137 p.SetBridge(5, 6, 0) // must not panic138 })139140 t.Run("normalizes order", func(t *testing.T) {141 p := fourCornerPuzzle()142 p.SetBridge(3, 1, 1)143144 if len(p.Bridges) != 1 {145 t.Fatalf("expected 1 bridge, got %d", len(p.Bridges))146 }147 if p.Bridges[0].Island1 != 1 || p.Bridges[0].Island2 != 3 {148 t.Errorf("bridge stored as (%d,%d), want (1,3)", p.Bridges[0].Island1, p.Bridges[0].Island2)149 }150 })151152 t.Run("invalidates bridge-derived caches only", func(t *testing.T) {153 p := fourCornerPuzzle()154 // Trigger cache builds.155 _ = p.BridgeCount(0)156 _ = p.FindIslandAt(0, 0)157 _ = p.CellContent(0, 0)158159 p.SetBridge(0, 1, 1)160161 if p.cellCache != nil {162 t.Error("cellCache should be nil after SetBridge")163 }164 if p.posIndex == nil {165 t.Error("posIndex should be preserved after SetBridge")166 }167 if p.bridgeCounts != nil {168 t.Error("bridgeCounts should be nil after SetBridge")169 }170 if p.bridgeIndex == nil {171 t.Error("bridgeIndex should remain available after SetBridge")172 }173 })174}175176func TestSelectTogglesBridgeMode(t *testing.T) {177 p := linePuzzle()178 m := Model{179 puzzle: *p,180 cursorIsland: 0,181 keys: DefaultKeyMap,182 }183184 next, _ := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter})185 selected := next.(Model)186 if selected.selectedIsland == nil || *selected.selectedIsland != 0 {187 t.Fatal("expected enter to select the current island")188 }189190 next, _ = selected.Update(tea.KeyPressMsg{Code: tea.KeyEnter})191 deselected := next.(Model)192 if deselected.selectedIsland != nil {193 t.Fatal("expected enter to deselect the current island")194 }195}196197// --- BridgeCount (P0) ---198199func TestBridgeCount(t *testing.T) {200 t.Run("single bridge", func(t *testing.T) {201 p := fourCornerPuzzle()202 p.SetBridge(0, 1, 1)203204 if got := p.BridgeCount(0); got != 1 {205 t.Errorf("BridgeCount(0) = %d, want 1", got)206 }207 })208209 t.Run("double bridge", func(t *testing.T) {210 p := fourCornerPuzzle()211 p.SetBridge(0, 1, 2)212213 if got := p.BridgeCount(0); got != 2 {214 t.Errorf("BridgeCount(0) = %d, want 2", got)215 }216 })217218 t.Run("multiple bridges", func(t *testing.T) {219 p := fourCornerPuzzle()220 p.SetBridge(0, 1, 1)221 p.SetBridge(0, 2, 2)222223 if got := p.BridgeCount(0); got != 3 {224 t.Errorf("BridgeCount(0) = %d, want 3", got)225 }226 })227228 t.Run("no bridges", func(t *testing.T) {229 p := fourCornerPuzzle()230231 if got := p.BridgeCount(0); got != 0 {232 t.Errorf("BridgeCount(0) = %d, want 0", got)233 }234 })235236 t.Run("invalid ID", func(t *testing.T) {237 p := fourCornerPuzzle()238239 if got := p.BridgeCount(999); got != 0 {240 t.Errorf("BridgeCount(999) = %d, want 0", got)241 }242 })243244 t.Run("counts both endpoints", func(t *testing.T) {245 p := fourCornerPuzzle()246 p.SetBridge(0, 1, 1)247248 if got := p.BridgeCount(1); got != 1 {249 t.Errorf("BridgeCount(1) = %d, want 1", got)250 }251 })252}253254// --- WouldCross (P0) ---255256func TestWouldCross(t *testing.T) {257 t.Run("no crossing parallel horizontal", func(t *testing.T) {258 // Two horizontal bridges on different rows can't cross.259 // 0 --- 1260 // 2 --- 3261 p := fourCornerPuzzle()262 p.SetBridge(0, 1, 1) // horizontal top263 if p.WouldCross(2, 3) {264 t.Error("parallel horizontal bridges should not cross")265 }266 })267268 t.Run("no crossing parallel vertical", func(t *testing.T) {269 // Two vertical bridges on different columns.270 p := fourCornerPuzzle()271 p.SetBridge(0, 2, 1) // vertical left272 if p.WouldCross(1, 3) {273 t.Error("parallel vertical bridges should not cross")274 }275 })276277 t.Run("horizontal crosses vertical", func(t *testing.T) {278 // Cross pattern: vertical bridge 0-2, then check horizontal 1-? crossing it.279 // Need a puzzle with an island in the middle area.280 p := &Puzzle{281 Width: 5,282 Height: 5,283 Islands: []Island{284 {ID: 0, X: 2, Y: 0, Required: 2},285 {ID: 1, X: 0, Y: 2, Required: 2},286 {ID: 2, X: 2, Y: 4, Required: 2},287 {ID: 3, X: 4, Y: 2, Required: 2},288 },289 }290 p.SetBridge(0, 2, 1) // vertical bridge through (2,1),(2,2),(2,3)291 if !p.WouldCross(1, 3) {292 t.Error("horizontal bridge should cross existing vertical bridge")293 }294 })295296 t.Run("vertical crosses horizontal", func(t *testing.T) {297 p := &Puzzle{298 Width: 5,299 Height: 5,300 Islands: []Island{301 {ID: 0, X: 2, Y: 0, Required: 2},302 {ID: 1, X: 0, Y: 2, Required: 2},303 {ID: 2, X: 2, Y: 4, Required: 2},304 {ID: 3, X: 4, Y: 2, Required: 2},305 },306 }307 p.SetBridge(1, 3, 1) // horizontal bridge through (1,2),(2,2),(3,2)308 if !p.WouldCross(0, 2) {309 t.Error("vertical bridge should cross existing horizontal bridge")310 }311 })312313 t.Run("island in path", func(t *testing.T) {314 // Island sitting between two endpoints blocks the bridge.315 p := &Puzzle{316 Width: 5,317 Height: 1,318 Islands: []Island{319 {ID: 0, X: 0, Y: 0, Required: 2},320 {ID: 1, X: 2, Y: 0, Required: 2},321 {ID: 2, X: 4, Y: 0, Required: 2},322 },323 }324 if !p.WouldCross(0, 2) {325 t.Error("bridge should be blocked by island in the middle")326 }327 })328329 t.Run("endpoints dont count as crossing", func(t *testing.T) {330 p := fourCornerPuzzle()331 // No bridges set — the endpoints themselves are not obstacles.332 if p.WouldCross(0, 1) {333 t.Error("empty path between endpoints should not cross")334 }335 })336337 t.Run("non-aligned islands", func(t *testing.T) {338 p := &Puzzle{339 Width: 4,340 Height: 4,341 Islands: []Island{342 {ID: 0, X: 0, Y: 0, Required: 1},343 {ID: 1, X: 1, Y: 1, Required: 1},344 },345 }346 if !p.WouldCross(0, 1) {347 t.Error("diagonal (non-aligned) islands should return true")348 }349 })350351 t.Run("nil island", func(t *testing.T) {352 p := fourCornerPuzzle()353 if !p.WouldCross(0, 99) {354 t.Error("bad island ID should return true")355 }356 })357358 t.Run("adjacent islands no obstacles", func(t *testing.T) {359 p := &Puzzle{360 Width: 3,361 Height: 1,362 Islands: []Island{363 {ID: 0, X: 0, Y: 0, Required: 1},364 {ID: 1, X: 1, Y: 0, Required: 1},365 },366 }367 if p.WouldCross(0, 1) {368 t.Error("adjacent islands with nothing between should not cross")369 }370 })371}372373func TestViewHeightStableAcrossStatusModes(t *testing.T) {374 selected := 0375 views := []Model{376 {puzzle: *fourCornerPuzzle(), cursorIsland: 0, keys: DefaultKeyMap, modeTitle: "Test"},377 {puzzle: *fourCornerPuzzle(), cursorIsland: 0, keys: DefaultKeyMap, modeTitle: "Test", showFullHelp: true},378 {puzzle: *fourCornerPuzzle(), cursorIsland: 0, keys: DefaultKeyMap, modeTitle: "Test", selectedIsland: &selected},379 {puzzle: *fourCornerPuzzle(), cursorIsland: 0, keys: DefaultKeyMap, modeTitle: "Test", selectedIsland: &selected, showFullHelp: true},380 }381382 wantHeight := lipgloss.Height(views[0].View())383 for i, m := range views[1:] {384 if got := lipgloss.Height(m.View()); got != wantHeight {385 t.Fatalf("view %d height = %d, want %d", i+1, got, wantHeight)386 }387 }388}389390func TestScreenToGridUsesHashiCellWidth(t *testing.T) {391 m := Model{puzzle: *fourCornerPuzzle(), cursorIsland: 0, keys: DefaultKeyMap, modeTitle: "Test"}392393 next, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 40})394 got := next.(Model)395 x, y := got.cachedGridOrigin()396397 if col, row, ok := got.screenToGrid(x, y); !ok || col != 0 || row != 0 {398 t.Fatalf("screenToGrid(origin) = (%d,%d,%v), want (0,0,true)", col, row, ok)399 }400 if _, _, ok := got.screenToGrid(x+cellWidth, y); ok {401 t.Fatal("screenToGrid(separator) = ok, want false")402 }403}404405func TestMouseClickMovesCursorToClickedIsland(t *testing.T) {406 m := Model{puzzle: *fourCornerPuzzle(), cursorIsland: 0, keys: DefaultKeyMap, modeTitle: "Test"}407408 next, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 40})409 got := next.(Model)410 ox, oy := got.cachedGridOrigin()411412 clickX := ox + (cellWidth+1)*3413 clickY := oy414 next, _ = got.Update(tea.MouseClickMsg{X: clickX, Y: clickY, Button: tea.MouseLeft})415 clicked := next.(Model)416417 if clicked.cursorIsland != 1 {418 t.Fatalf("cursorIsland = %d, want %d", clicked.cursorIsland, 1)419 }420}421422func TestMouseClickIgnoresEmptyCells(t *testing.T) {423 m := Model{puzzle: *fourCornerPuzzle(), cursorIsland: 0, keys: DefaultKeyMap, modeTitle: "Test"}424425 next, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 40})426 got := next.(Model)427 ox, oy := got.cachedGridOrigin()428429 clickX := ox + (cellWidth + 1)430 clickY := oy431 next, _ = got.Update(tea.MouseClickMsg{X: clickX, Y: clickY, Button: tea.MouseLeft})432 clicked := next.(Model)433434 if clicked.cursorIsland != 0 {435 t.Fatalf("cursorIsland = %d, want unchanged 0", clicked.cursorIsland)436 }437}438439func TestGridViewUsesOnlyOuterBorders(t *testing.T) {440 m := Model{puzzle: *fourCornerPuzzle(), cursorIsland: 0, keys: DefaultKeyMap}441442 lines := strings.Split(ansi.Strip(gridView(m, false)), "\n")443 if got, want := len(lines), 9; got != want {444 t.Fatalf("line count = %d, want %d", got, want)445 }446447 if !strings.HasPrefix(lines[0], "┌") || !strings.HasSuffix(lines[0], "┐") {448 t.Fatalf("top border = %q, want outer corners", lines[0])449 }450 if !strings.HasPrefix(lines[len(lines)-1], "└") || !strings.HasSuffix(lines[len(lines)-1], "┘") {451 t.Fatalf("bottom border = %q, want outer corners", lines[len(lines)-1])452 }453454 for _, idx := range []int{1, 3, 5, 7} {455 if got := strings.Count(lines[idx], "│"); got != 2 {456 t.Fatalf("content row %d = %q, want only outer vertical borders", idx, lines[idx])457 }458 }459 for _, idx := range []int{2, 4, 6} {460 if strings.Contains(lines[idx], "─") {461 t.Fatalf("separator row %d = %q, want no interior horizontal border glyphs", idx, lines[idx])462 }463 }464}465466func TestGridViewRendersSingleAndDoubleHorizontalBridgesAcrossSeparators(t *testing.T) {467 makePuzzle := func(count int) Puzzle {468 p := Puzzle{469 Width: 3,470 Height: 1,471 Islands: []Island{472 {ID: 0, X: 0, Y: 0, Required: count},473 {ID: 1, X: 2, Y: 0, Required: count},474 },475 }476 p.SetBridge(0, 1, count)477 return p478 }479480 single := Model{puzzle: makePuzzle(1), cursorIsland: 0, keys: DefaultKeyMap}481 singleRow := strings.Split(ansi.Strip(gridView(single, false)), "\n")[1]482 if got := strings.Count(singleRow, "━"); got != 7 {483 t.Fatalf("single bridge row = %q, want 7 heavy horizontal bridge glyphs including separators", singleRow)484 }485486 double := Model{puzzle: makePuzzle(2), cursorIsland: 0, keys: DefaultKeyMap}487 doubleRow := strings.Split(ansi.Strip(gridView(double, false)), "\n")[1]488 if got := strings.Count(doubleRow, "═"); got != 7 {489 t.Fatalf("double bridge row = %q, want 7 double-bridge glyphs including separators", doubleRow)490 }491}492493func TestGridViewRendersVerticalBridgeInSeparatorRows(t *testing.T) {494 p := Puzzle{495 Width: 1,496 Height: 3,497 Islands: []Island{498 {ID: 0, X: 0, Y: 0, Required: 1},499 {ID: 1, X: 0, Y: 2, Required: 1},500 },501 }502 p.SetBridge(0, 1, 1)503504 m := Model{puzzle: p, cursorIsland: 0, keys: DefaultKeyMap}505 lines := strings.Split(ansi.Strip(gridView(m, false)), "\n")506507 if !strings.Contains(lines[2], " ┃ ") {508 t.Fatalf("upper separator row = %q, want vertical bridge glyph", lines[2])509 }510 if !strings.Contains(lines[3], " ┃ ") {511 t.Fatalf("bridge cell row = %q, want vertical bridge glyph", lines[3])512 }513 if !strings.Contains(lines[4], " ┃ ") {514 t.Fatalf("lower separator row = %q, want vertical bridge glyph", lines[4])515 }516}517518func TestResolveCellVisualCursorIsland(t *testing.T) {519 m := Model{puzzle: *fourCornerPuzzle(), cursorIsland: 0, keys: DefaultKeyMap}520521 got := renderCellVisual(resolveCellVisual(m, 0, 0, false))522 want := renderExpectedIslandCell(game.CursorLeft+"2"+game.CursorRight, game.CursorFG(), game.CursorBG(), true)523 if got != want {524 t.Fatalf("cursor island render = %q, want %q", got, want)525 }526}527528func TestResolveCellVisualSelectedIsland(t *testing.T) {529 selected := 0530 m := Model{puzzle: *fourCornerPuzzle(), cursorIsland: 1, selectedIsland: &selected, keys: DefaultKeyMap}531532 got := renderCellVisual(resolveCellVisual(m, 0, 0, false))533 want := renderExpectedIslandCell(" 2 ", theme.TextOnBG(theme.Current().SelectionBG), theme.Current().SelectionBG, true)534 if got != want {535 t.Fatalf("selected island render = %q, want %q", got, want)536 }537}538539func TestResolveCellVisualSatisfiedAndOverfilledIslands(t *testing.T) {540 satisfied := Puzzle{541 Width: 3,542 Height: 1,543 Islands: []Island{544 {ID: 0, X: 0, Y: 0, Required: 1},545 {ID: 1, X: 2, Y: 0, Required: 1},546 },547 }548 satisfied.SetBridge(0, 1, 1)549 satisfiedModel := Model{puzzle: satisfied, cursorIsland: 1, keys: DefaultKeyMap}550551 gotSatisfied := renderCellVisual(resolveCellVisual(satisfiedModel, 0, 0, false))552 wantSatisfied := renderExpectedIslandCell(" 1 ", theme.TextOnBG(theme.Current().SuccessBG), theme.Current().SuccessBG, true)553 if gotSatisfied != wantSatisfied {554 t.Fatalf("satisfied island render = %q, want %q", gotSatisfied, wantSatisfied)555 }556557 overfilled := Puzzle{558 Width: 5,559 Height: 3,560 Islands: []Island{561 {ID: 0, X: 0, Y: 1, Required: 2},562 {ID: 1, X: 2, Y: 1, Required: 2},563 {ID: 2, X: 4, Y: 1, Required: 2},564 {ID: 3, X: 2, Y: 0, Required: 1},565 },566 }567 overfilled.SetBridge(0, 1, 1)568 overfilled.SetBridge(1, 2, 1)569 overfilled.SetBridge(1, 3, 1)570 overfilledModel := Model{puzzle: overfilled, cursorIsland: 0, keys: DefaultKeyMap}571572 gotOverfilled := renderCellVisual(resolveCellVisual(overfilledModel, 2, 1, false))573 wantOverfilled := renderExpectedIslandCell(" 2 ", theme.TextOnBG(theme.Current().ErrorBG), theme.Current().ErrorBG, true)574 if gotOverfilled != wantOverfilled {575 t.Fatalf("overfilled island render = %q, want %q", gotOverfilled, wantOverfilled)576 }577}578579func TestResolveCellVisualSolvedIsland(t *testing.T) {580 p := Puzzle{581 Width: 1,582 Height: 1,583 Islands: []Island{584 {ID: 0, X: 0, Y: 0, Required: 1},585 },586 }587 m := Model{puzzle: p, cursorIsland: 0, keys: DefaultKeyMap}588589 unsolved := resolveCellVisual(m, 0, 0, false)590 solved := resolveCellVisual(m, 0, 0, true)591592 if sameColor(unsolved.bg, solved.bg) {593 t.Fatal("expected solved island background to differ from unsolved island background")594 }595596 got := renderCellVisual(solved)597 want := renderExpectedIslandCellWithOuterBG(598 " 1 ",599 theme.Current().SolvedFG,600 solvedIslandBackground(),601 solvedBoardBackground(),602 true,603 )604 if got != want {605 t.Fatalf("solved island render = %q, want %q", got, want)606 }607}608609func TestSolvedBridgeVisualStaysLegibleOnBoardBackground(t *testing.T) {610 p := Puzzle{611 Width: 3,612 Height: 1,613 Islands: []Island{614 {ID: 0, X: 0, Y: 0, Required: 1},615 {ID: 1, X: 2, Y: 0, Required: 1},616 },617 }618 p.SetBridge(0, 1, 1)619 m := Model{puzzle: p, cursorIsland: 0, keys: DefaultKeyMap}620621 island := resolveCellVisual(m, 0, 0, true)622 if !sameColor(island.outerBG, solvedBoardBackground()) {623 t.Fatal("expected solved island outer background to use the solved board color")624 }625626 bridge := resolveCellVisual(m, 1, 0, true)627 if !sameColor(bridge.bg, solvedBoardBackground()) {628 t.Fatal("expected solved bridge cell to use the solved board color")629 }630 if !sameColor(bridge.fg, bridgeColor(0)) {631 t.Fatal("expected solved bridge cell to use the bridge palette color")632 }633}634635func TestResolveCellVisualSolvedEmptyCellUsesSolvedBoardBackground(t *testing.T) {636 m := Model{puzzle: *linePuzzle(), cursorIsland: 0, keys: DefaultKeyMap}637638 empty := resolveCellVisual(m, 1, 0, true)639 if !sameColor(empty.bg, solvedBoardBackground()) {640 t.Fatal("expected solved empty cell to use the solved board color")641 }642}643644func TestBridgeForegroundUsesThemePalettePerBridge(t *testing.T) {645 p := Puzzle{646 Width: 5,647 Height: 1,648 Islands: []Island{649 {ID: 0, X: 0, Y: 0, Required: 1},650 {ID: 1, X: 2, Y: 0, Required: 2},651 {ID: 2, X: 4, Y: 0, Required: 1},652 },653 }654 p.SetBridge(0, 1, 1)655 p.SetBridge(1, 2, 1)656 m := Model{puzzle: p, cursorIsland: 0, keys: DefaultKeyMap}657658 leftBridge := game.DynamicGridBridge{659 Kind: game.DynamicGridBridgeVertical,660 X: 1,661 Y: 0,662 Count: 2,663 Cells: [4]image.Point{{X: 0, Y: 0}, {X: 1, Y: 0}},664 }665 rightBridge := game.DynamicGridBridge{666 Kind: game.DynamicGridBridgeVertical,667 X: 3,668 Y: 0,669 Count: 2,670 Cells: [4]image.Point{{X: 2, Y: 0}, {X: 3, Y: 0}},671 }672673 if !sameColor(bridgeForeground(m, leftBridge), bridgeColor(0)) {674 t.Fatal("expected first bridge to use the first palette color")675 }676 if !sameColor(bridgeForeground(m, rightBridge), bridgeColor(1)) {677 t.Fatal("expected second bridge to use the second palette color")678 }679}680681func sameColor(left, right color.Color) bool {682 if left == nil || right == nil {683 return left == nil && right == nil684 }685 lr, lg, lb, la := left.RGBA()686 rr, rg, rb, ra := right.RGBA()687 return lr == rr && lg == rg && lb == rb && la == ra688}689690func renderExpectedIslandCell(text string, fg, bg color.Color, bold bool) string {691 return renderExpectedIslandCellWithOuterBG(text, fg, bg, boardBackground(), bold)692}693694func renderExpectedIslandCellWithOuterBG(text string, fg, bg, outerBG color.Color, bold bool) string {695 side := lipgloss.NewStyle().Width((cellWidth - islandPillWidth) / 2).Background(outerBG).Render("")696 center := lipgloss.NewStyle().697 Width(islandPillWidth).698 AlignHorizontal(lipgloss.Center).699 Foreground(fg).700 Background(bg)701 if bold {702 center = center.Bold(true)703 }704 return side + center.Render(text) + side705}706707// --- IsConnected (P0) ---708709func TestIsConnected(t *testing.T) {710 t.Run("all connected chain", func(t *testing.T) {711 p := linePuzzle()712 p.SetBridge(0, 1, 1)713 p.SetBridge(1, 2, 1)714 p.SetBridge(2, 3, 1)715716 if !p.IsConnected() {717 t.Error("chain of bridges should be connected")718 }719 })720721 t.Run("disconnected pair", func(t *testing.T) {722 p := fourCornerPuzzle()723 p.SetBridge(0, 1, 1) // only top two connected724725 if p.IsConnected() {726 t.Error("only two of four islands connected should not be connected")727 }728 })729730 t.Run("single island", func(t *testing.T) {731 p := &Puzzle{732 Width: 3,733 Height: 3,734 Islands: []Island{{ID: 0, X: 1, Y: 1, Required: 0}},735 }736 if !p.IsConnected() {737 t.Error("single island should be connected")738 }739 })740741 t.Run("no bridges multiple islands", func(t *testing.T) {742 p := fourCornerPuzzle()743 if p.IsConnected() {744 t.Error("multiple islands with no bridges should not be connected")745 }746 })747748 t.Run("multiple components", func(t *testing.T) {749 p := fourCornerPuzzle()750 p.SetBridge(0, 1, 1) // top pair751 p.SetBridge(2, 3, 1) // bottom pair752753 if p.IsConnected() {754 t.Error("two separate pairs should not be connected")755 }756 })757758 t.Run("star topology", func(t *testing.T) {759 // Center island connected to 4 spokes.760 p := &Puzzle{761 Width: 5,762 Height: 5,763 Islands: []Island{764 {ID: 0, X: 2, Y: 2, Required: 4},765 {ID: 1, X: 2, Y: 0, Required: 1},766 {ID: 2, X: 4, Y: 2, Required: 1},767 {ID: 3, X: 2, Y: 4, Required: 1},768 {ID: 4, X: 0, Y: 2, Required: 1},769 },770 }771 p.SetBridge(0, 1, 1)772 p.SetBridge(0, 2, 1)773 p.SetBridge(0, 3, 1)774 p.SetBridge(0, 4, 1)775776 if !p.IsConnected() {777 t.Error("star topology should be connected")778 }779 })780}781782// --- IsSolved (P0) ---783784func TestIsSolved(t *testing.T) {785 t.Run("fully solved", func(t *testing.T) {786 // Simple: two islands requiring 1 bridge each.787 p := &Puzzle{788 Width: 3,789 Height: 1,790 Islands: []Island{791 {ID: 0, X: 0, Y: 0, Required: 1},792 {ID: 1, X: 2, Y: 0, Required: 1},793 },794 }795 p.SetBridge(0, 1, 1)796797 if !p.IsSolved() {798 t.Error("puzzle should be solved")799 }800 })801802 t.Run("satisfied but disconnected", func(t *testing.T) {803 // Four islands: each requires 1 bridge. Connect as two pairs.804 p := &Puzzle{805 Width: 5,806 Height: 5,807 Islands: []Island{808 {ID: 0, X: 0, Y: 0, Required: 1},809 {ID: 1, X: 2, Y: 0, Required: 1},810 {ID: 2, X: 0, Y: 4, Required: 1},811 {ID: 3, X: 2, Y: 4, Required: 1},812 },813 }814 p.SetBridge(0, 1, 1)815 p.SetBridge(2, 3, 1)816817 if p.IsSolved() {818 t.Error("satisfied but disconnected should not be solved")819 }820 })821822 t.Run("connected but unsatisfied", func(t *testing.T) {823 p := &Puzzle{824 Width: 3,825 Height: 1,826 Islands: []Island{827 {ID: 0, X: 0, Y: 0, Required: 2},828 {ID: 1, X: 2, Y: 0, Required: 2},829 },830 }831 p.SetBridge(0, 1, 1) // only 1, need 2832833 if p.IsSolved() {834 t.Error("connected but unsatisfied should not be solved")835 }836 })837838 t.Run("over-satisfied", func(t *testing.T) {839 p := &Puzzle{840 Width: 3,841 Height: 1,842 Islands: []Island{843 {ID: 0, X: 0, Y: 0, Required: 1},844 {ID: 1, X: 2, Y: 0, Required: 1},845 },846 }847 p.SetBridge(0, 1, 2)848849 if p.IsSolved() {850 t.Error("over-satisfied should not be solved")851 }852 })853854 t.Run("empty puzzle no bridges", func(t *testing.T) {855 p := fourCornerPuzzle()856 if p.IsSolved() {857 t.Error("no bridges placed should not be solved")858 }859 })860}861862// --- FindIslandAt (P1) ---863864func TestFindIslandAt(t *testing.T) {865 t.Run("island exists", func(t *testing.T) {866 p := fourCornerPuzzle()867 isl := p.FindIslandAt(3, 0)868 if isl == nil {869 t.Fatal("expected island at (3,0)")870 }871 if isl.ID != 1 {872 t.Errorf("ID = %d, want 1", isl.ID)873 }874 })875876 t.Run("no island", func(t *testing.T) {877 p := fourCornerPuzzle()878 if isl := p.FindIslandAt(1, 1); isl != nil {879 t.Errorf("expected nil at (1,1), got island %d", isl.ID)880 }881 })882883 t.Run("lazy index build", func(t *testing.T) {884 p := fourCornerPuzzle()885 if p.posIndex != nil {886 t.Fatal("posIndex should be nil before first call")887 }888 _ = p.FindIslandAt(0, 0)889 if p.posIndex == nil {890 t.Error("posIndex should be built after first call")891 }892 // Subsequent call should use the same index.893 _ = p.FindIslandAt(3, 3)894 if p.posIndex == nil {895 t.Error("posIndex should persist across calls")896 }897 })898}899900// --- FindIslandByID (P1) ---901902func TestFindIslandByID(t *testing.T) {903 t.Run("sequential IDs", func(t *testing.T) {904 p := fourCornerPuzzle()905 isl := p.FindIslandByID(2)906 if isl == nil {907 t.Fatal("expected island with ID=2")908 }909 if isl.X != 0 || isl.Y != 3 {910 t.Errorf("island 2 at (%d,%d), want (0,3)", isl.X, isl.Y)911 }912 })913914 t.Run("invalid ID", func(t *testing.T) {915 p := fourCornerPuzzle()916 if isl := p.FindIslandByID(99); isl != nil {917 t.Errorf("expected nil, got island %d", isl.ID)918 }919 })920921 t.Run("non-sequential fallback", func(t *testing.T) {922 p := &Puzzle{923 Width: 3,924 Height: 3,925 Islands: []Island{926 {ID: 10, X: 0, Y: 0, Required: 1},927 {ID: 20, X: 2, Y: 0, Required: 1},928 {ID: 30, X: 0, Y: 2, Required: 1},929 },930 }931 isl := p.FindIslandByID(20)932 if isl == nil {933 t.Fatal("expected island with ID=20")934 }935 if isl.X != 2 || isl.Y != 0 {936 t.Errorf("island 20 at (%d,%d), want (2,0)", isl.X, isl.Y)937 }938 })939}940941// --- FindAdjacentIsland (P1) ---942943func TestFindAdjacentIsland(t *testing.T) {944 t.Run("finds island to the right", func(t *testing.T) {945 p := fourCornerPuzzle()946 adj := p.FindAdjacentIsland(0, 1, 0)947 if adj == nil || adj.ID != 1 {948 t.Errorf("expected island 1, got %v", adj)949 }950 })951952 t.Run("finds island below", func(t *testing.T) {953 p := fourCornerPuzzle()954 adj := p.FindAdjacentIsland(0, 0, 1)955 if adj == nil || adj.ID != 2 {956 t.Errorf("expected island 2, got %v", adj)957 }958 })959960 t.Run("no island in direction", func(t *testing.T) {961 p := fourCornerPuzzle()962 adj := p.FindAdjacentIsland(0, -1, 0) // left from leftmost963 if adj != nil {964 t.Errorf("expected nil, got island %d", adj.ID)965 }966 })967968 t.Run("stops at grid edge", func(t *testing.T) {969 p := fourCornerPuzzle()970 adj := p.FindAdjacentIsland(0, 0, -1) // up from top971 if adj != nil {972 t.Errorf("expected nil, got island %d", adj.ID)973 }974 })975976 t.Run("blocked by perpendicular bridge", func(t *testing.T) {977 // Cross puzzle:978 // . 0 .979 // 1 . 2980 // . 3 .981 p := &Puzzle{982 Width: 3,983 Height: 3,984 Islands: []Island{985 {ID: 0, X: 1, Y: 0, Required: 1},986 {ID: 1, X: 0, Y: 1, Required: 1},987 {ID: 2, X: 2, Y: 1, Required: 1},988 {ID: 3, X: 1, Y: 2, Required: 1},989 },990 }991 p.SetBridge(1, 2, 1) // horizontal bridge through (1,1)992993 // Vertical ray from 0 downward should be blocked at (1,1).994 adj := p.FindAdjacentIsland(0, 0, 1)995 if adj != nil {996 t.Errorf("expected nil (blocked by H bridge), got island %d", adj.ID)997 }998 })9991000 t.Run("not blocked by parallel bridge", func(t *testing.T) {1001 // Two islands vertically aligned with a vertical bridge between another pair1002 // in the same column. The ray should pass through a parallel bridge.1003 p := &Puzzle{1004 Width: 3,1005 Height: 5,1006 Islands: []Island{1007 {ID: 0, X: 0, Y: 0, Required: 1},1008 {ID: 1, X: 0, Y: 2, Required: 2},1009 {ID: 2, X: 0, Y: 4, Required: 1},1010 },1011 }1012 // Bridge 1-2 creates vertical bridge segments at (0,3).1013 p.SetBridge(1, 2, 1)10141015 // Ray from 0 downward should find island 1 (not blocked by vertical bridge beyond).1016 adj := p.FindAdjacentIsland(0, 0, 1)1017 if adj == nil || adj.ID != 1 {1018 t.Errorf("expected island 1, got %v", adj)1019 }1020 })1021}10221023// --- CellContent (P1) ---10241025func TestCellContent(t *testing.T) {1026 t.Run("island cell", func(t *testing.T) {1027 p := fourCornerPuzzle()1028 ci := p.CellContent(0, 0)1029 if ci.Kind != cellIsland {1030 t.Errorf("Kind = %d, want cellIsland(%d)", ci.Kind, cellIsland)1031 }1032 if ci.IslandID != 0 {1033 t.Errorf("IslandID = %d, want 0", ci.IslandID)1034 }1035 })10361037 t.Run("horizontal bridge cell", func(t *testing.T) {1038 p := fourCornerPuzzle()1039 p.SetBridge(0, 1, 1)10401041 ci := p.CellContent(1, 0)1042 if ci.Kind != cellBridgeH {1043 t.Errorf("Kind = %d, want cellBridgeH(%d)", ci.Kind, cellBridgeH)1044 }1045 })10461047 t.Run("vertical bridge cell", func(t *testing.T) {1048 p := fourCornerPuzzle()1049 p.SetBridge(0, 2, 1)10501051 ci := p.CellContent(0, 1)1052 if ci.Kind != cellBridgeV {1053 t.Errorf("Kind = %d, want cellBridgeV(%d)", ci.Kind, cellBridgeV)1054 }1055 })10561057 t.Run("empty cell", func(t *testing.T) {1058 p := fourCornerPuzzle()1059 ci := p.CellContent(1, 1)1060 if ci.Kind != cellEmpty {1061 t.Errorf("Kind = %d, want cellEmpty(%d)", ci.Kind, cellEmpty)1062 }1063 })10641065 t.Run("bridge count", func(t *testing.T) {1066 p := fourCornerPuzzle()1067 p.SetBridge(0, 1, 2)10681069 ci := p.CellContent(1, 0) // bridge segment between (0,0) and (3,0)1070 if ci.BridgeCount != 2 {1071 t.Errorf("BridgeCount = %d, want 2", ci.BridgeCount)1072 }1073 })10741075 t.Run("lazy rebuild after modification", func(t *testing.T) {1076 p := fourCornerPuzzle()1077 _ = p.CellContent(1, 0) // build cache (empty)10781079 p.SetBridge(0, 1, 1) // invalidates cache10801081 ci := p.CellContent(1, 0) // should rebuild1082 if ci.Kind != cellBridgeH {1083 t.Errorf("Kind = %d after rebuild, want cellBridgeH(%d)", ci.Kind, cellBridgeH)1084 }1085 })1086}10871088// --- Save/Load round-trip (P1) ---10891090func TestSaveLoadRoundTrip(t *testing.T) {1091 t.Run("full round trip", func(t *testing.T) {1092 original := &Model{1093 puzzle: Puzzle{1094 Width: 7,1095 Height: 7,1096 Islands: []Island{1097 {ID: 0, X: 0, Y: 0, Required: 2},1098 {ID: 1, X: 3, Y: 0, Required: 3},1099 {ID: 2, X: 6, Y: 0, Required: 1},1100 {ID: 3, X: 0, Y: 3, Required: 2},1101 },1102 Bridges: []Bridge{1103 {Island1: 0, Island2: 1, Count: 1},1104 {Island1: 1, Island2: 2, Count: 2},1105 {Island1: 0, Island2: 3, Count: 1},1106 },1107 },1108 cursorIsland: 1,1109 keys: DefaultKeyMap,1110 }11111112 data, err := original.GetSave()1113 if err != nil {1114 t.Fatal(err)1115 }11161117 got, err := ImportModel(data)1118 if err != nil {1119 t.Fatal(err)1120 }11211122 // Dimensions.1123 if got.puzzle.Width != 7 || got.puzzle.Height != 7 {1124 t.Errorf("dimensions = %dx%d, want 7x7", got.puzzle.Width, got.puzzle.Height)1125 }11261127 // Islands.1128 if len(got.puzzle.Islands) != 4 {1129 t.Fatalf("island count = %d, want 4", len(got.puzzle.Islands))1130 }1131 for i, isl := range original.puzzle.Islands {1132 g := got.puzzle.Islands[i]1133 if g.ID != isl.ID || g.X != isl.X || g.Y != isl.Y || g.Required != isl.Required {1134 t.Errorf("island[%d] = %+v, want %+v", i, g, isl)1135 }1136 }11371138 // Bridges.1139 if len(got.puzzle.Bridges) != 3 {1140 t.Fatalf("bridge count = %d, want 3", len(got.puzzle.Bridges))1141 }1142 for i, b := range original.puzzle.Bridges {1143 g := got.puzzle.Bridges[i]1144 if g.Island1 != b.Island1 || g.Island2 != b.Island2 || g.Count != b.Count {1145 t.Errorf("bridge[%d] = %+v, want %+v", i, g, b)1146 }1147 }1148 })11491150 t.Run("caches rebuilt on access", func(t *testing.T) {1151 m := &Model{1152 puzzle: Puzzle{1153 Width: 3,1154 Height: 1,1155 Islands: []Island{1156 {ID: 0, X: 0, Y: 0, Required: 1},1157 {ID: 1, X: 2, Y: 0, Required: 1},1158 },1159 Bridges: []Bridge{1160 {Island1: 0, Island2: 1, Count: 1},1161 },1162 },1163 keys: DefaultKeyMap,1164 }11651166 data, err := m.GetSave()1167 if err != nil {1168 t.Fatal(err)1169 }11701171 got, err := ImportModel(data)1172 if err != nil {1173 t.Fatal(err)1174 }11751176 // Caches should be nil after import; accessing BridgeCount triggers rebuild.1177 if got.puzzle.bridgeCounts != nil {1178 t.Error("bridgeCounts should be nil immediately after import")1179 }1180 if cnt := got.puzzle.BridgeCount(0); cnt != 1 {1181 t.Errorf("BridgeCount(0) = %d, want 1", cnt)1182 }1183 })11841185 t.Run("invalid JSON", func(t *testing.T) {1186 _, err := ImportModel([]byte("not json"))1187 if err == nil {1188 t.Fatal("expected error for invalid JSON")1189 }1190 })1191}11921193// --- placeIslands (P2) ---11941195func TestPlaceIslands(t *testing.T) {1196 t.Run("places correct count", func(t *testing.T) {1197 for range 10 {1198 islands := placeIslands(7, 7, 6)1199 if islands == nil {1200 continue // may fail on rare occasions1201 }1202 if len(islands) != 6 {1203 t.Errorf("len(islands) = %d, want 6", len(islands))1204 }1205 }1206 })12071208 t.Run("no diagonal adjacency", func(t *testing.T) {1209 for range 20 {1210 islands := placeIslands(7, 7, 6)1211 if islands == nil {1212 continue1213 }1214 for i := range len(islands) {1215 for j := i + 1; j < len(islands); j++ {1216 dx := abs(islands[i].X - islands[j].X)1217 dy := abs(islands[i].Y - islands[j].Y)1218 if dx <= 1 && dy <= 1 && dx > 0 && dy > 0 {1219 t.Errorf("islands %d(%d,%d) and %d(%d,%d) are diagonally adjacent",1220 islands[i].ID, islands[i].X, islands[i].Y,1221 islands[j].ID, islands[j].X, islands[j].Y)1222 }1223 }1224 }1225 }1226 })12271228 t.Run("returns nil when impossible", func(t *testing.T) {1229 islands := placeIslands(3, 3, 100)1230 if islands != nil {1231 t.Errorf("expected nil for 100 islands on 3x3, got %d islands", len(islands))1232 }1233 })12341235 t.Run("all within bounds", func(t *testing.T) {1236 for range 20 {1237 islands := placeIslands(7, 7, 6)1238 if islands == nil {1239 continue1240 }1241 for _, isl := range islands {1242 if isl.X < 0 || isl.X >= 7 || isl.Y < 0 || isl.Y >= 7 {1243 t.Errorf("island %d at (%d,%d) out of bounds", isl.ID, isl.X, isl.Y)1244 }1245 }1246 }1247 })1248}12491250// --- isDirectlyConnectable (P2) ---12511252func TestIsDirectlyConnectable(t *testing.T) {1253 t.Run("aligned no obstacle", func(t *testing.T) {1254 p := &Puzzle{1255 Width: 5,1256 Height: 1,1257 Islands: []Island{1258 {ID: 0, X: 0, Y: 0, Required: 1},1259 {ID: 1, X: 4, Y: 0, Required: 1},1260 },1261 }1262 if !isDirectlyConnectable(p, p.Islands[0], p.Islands[1]) {1263 t.Error("aligned islands with no obstacle should be connectable")1264 }1265 })12661267 t.Run("aligned island between", func(t *testing.T) {1268 p := linePuzzle()1269 // Islands 0,1,2,3 at x=0,2,4,6. Check if 0 and 2 are directly connectable1270 // (island 1 at x=2 sits between them... actually 1 is at the endpoint).1271 // Instead: 0 at x=0 and 3 at x=6 — islands 1(x=2) and 2(x=4) are between.1272 if isDirectlyConnectable(p, p.Islands[0], p.Islands[3]) {1273 t.Error("island between should block direct connection")1274 }1275 })12761277 t.Run("not aligned", func(t *testing.T) {1278 p := fourCornerPuzzle()1279 // Islands 0(0,0) and 3(3,3) are diagonal — not aligned.1280 if isDirectlyConnectable(p, p.Islands[0], p.Islands[3]) {1281 t.Error("diagonal pair should not be connectable")1282 }1283 })12841285 t.Run("adjacent distance 1", func(t *testing.T) {1286 p := &Puzzle{1287 Width: 2,1288 Height: 1,1289 Islands: []Island{1290 {ID: 0, X: 0, Y: 0, Required: 1},1291 {ID: 1, X: 1, Y: 0, Required: 1},1292 },1293 }1294 if !isDirectlyConnectable(p, p.Islands[0], p.Islands[1]) {1295 t.Error("adjacent islands should be connectable")1296 }1297 })1298}12991300// --- GeneratePuzzle (P2) ---13011302func TestGeneratePuzzle(t *testing.T) {1303 mode := NewMode("Test", "test mode", 7, 7, 8, 10)13041305 t.Run("produces valid puzzle", func(t *testing.T) {1306 puzzle, err := GeneratePuzzle(mode)1307 if err != nil {1308 t.Fatal(err)1309 }1310 for _, isl := range puzzle.Islands {1311 if isl.Required < 1 {1312 t.Errorf("island %d has Required=%d, want >= 1", isl.ID, isl.Required)1313 }1314 }1315 })13161317 t.Run("bridges cleared", func(t *testing.T) {1318 puzzle, err := GeneratePuzzle(mode)1319 if err != nil {1320 t.Fatal(err)1321 }1322 if len(puzzle.Bridges) != 0 {1323 t.Errorf("len(Bridges) = %d, want 0", len(puzzle.Bridges))1324 }1325 })13261327 t.Run("correct dimensions", func(t *testing.T) {1328 puzzle, err := GeneratePuzzle(mode)1329 if err != nil {1330 t.Fatal(err)1331 }1332 if puzzle.Width != 7 || puzzle.Height != 7 {1333 t.Errorf("dimensions = %dx%d, want 7x7", puzzle.Width, puzzle.Height)1334 }1335 })1336}13371338// --- Save JSON structure (P1) ---13391340func TestSaveJSON(t *testing.T) {1341 m := Model{1342 puzzle: Puzzle{1343 Width: 4,1344 Height: 4,1345 Islands: []Island{1346 {ID: 0, X: 0, Y: 0, Required: 2},1347 },1348 Bridges: []Bridge{1349 {Island1: 0, Island2: 1, Count: 1},1350 },1351 },1352 keys: DefaultKeyMap,1353 }13541355 data, err := m.GetSave()1356 if err != nil {1357 t.Fatal(err)1358 }13591360 var raw map[string]json.RawMessage1361 if err := json.Unmarshal(data, &raw); err != nil {1362 t.Fatal(err)1363 }13641365 for _, key := range []string{"width", "height", "islands", "bridges"} {1366 if _, ok := raw[key]; !ok {1367 t.Errorf("missing key %q in save JSON", key)1368 }1369 }1370}13711372// --- Reset (P0) ---13731374func TestReset(t *testing.T) {1375 t.Run("invalidates caches", func(t *testing.T) {1376 m := Model{1377 puzzle: Puzzle{1378 Width: 7,1379 Height: 7,1380 Islands: []Island{1381 {ID: 0, X: 0, Y: 0, Required: 2},1382 {ID: 1, X: 3, Y: 0, Required: 2},1383 {ID: 2, X: 0, Y: 3, Required: 2},1384 {ID: 3, X: 3, Y: 3, Required: 2},1385 },1386 },1387 keys: DefaultKeyMap,1388 }1389 m.puzzle.SetBridge(0, 1, 1)1390 m.puzzle.SetBridge(2, 3, 1)13911392 // Populate caches.1393 _ = m.puzzle.BridgeCount(0)1394 _ = m.puzzle.CellContent(1, 0)1395 _ = m.puzzle.FindIslandAt(0, 0)13961397 got := m.Reset().(Model)13981399 if got.puzzle.bridgeCounts != nil {1400 t.Error("bridgeCounts should be nil after Reset")1401 }1402 if got.puzzle.cellCache != nil {1403 t.Error("cellCache should be nil after Reset")1404 }1405 if got.puzzle.posIndex != nil {1406 t.Error("posIndex should be nil after Reset")1407 }1408 })14091410 t.Run("bridge counts reflect cleared state", func(t *testing.T) {1411 m := Model{1412 puzzle: Puzzle{1413 Width: 7,1414 Height: 7,1415 Islands: []Island{1416 {ID: 0, X: 0, Y: 0, Required: 2},1417 {ID: 1, X: 3, Y: 0, Required: 2},1418 {ID: 2, X: 0, Y: 3, Required: 2},1419 {ID: 3, X: 3, Y: 3, Required: 2},1420 },1421 },1422 keys: DefaultKeyMap,1423 }1424 m.puzzle.SetBridge(0, 1, 2)14251426 // Populate the bridge count cache with stale data.1427 if cnt := m.puzzle.BridgeCount(0); cnt != 2 {1428 t.Fatalf("BridgeCount(0) before reset = %d, want 2", cnt)1429 }14301431 got := m.Reset().(Model)14321433 if cnt := got.puzzle.BridgeCount(0); cnt != 0 {1434 t.Errorf("BridgeCount(0) after reset = %d, want 0", cnt)1435 }1436 })14371438 t.Run("cell cache reflects cleared state", func(t *testing.T) {1439 m := Model{1440 puzzle: Puzzle{1441 Width: 7,1442 Height: 7,1443 Islands: []Island{1444 {ID: 0, X: 0, Y: 0, Required: 2},1445 {ID: 1, X: 3, Y: 0, Required: 2},1446 },1447 },1448 keys: DefaultKeyMap,1449 }1450 m.puzzle.SetBridge(0, 1, 1)14511452 // Populate cell cache with a bridge cell.1453 ci := m.puzzle.CellContent(1, 0)1454 if ci.Kind != cellBridgeH {1455 t.Fatalf("CellContent(1,0) before reset = %d, want cellBridgeH(%d)", ci.Kind, cellBridgeH)1456 }14571458 got := m.Reset().(Model)14591460 ci = got.puzzle.CellContent(1, 0)1461 if ci.Kind != cellEmpty {1462 t.Errorf("CellContent(1,0) after reset = %d, want cellEmpty(%d)", ci.Kind, cellEmpty)1463 }1464 })1465}