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 / nonogram / dynamic_grid_test.go
dynamic_grid_test.go 8.3K
1package nonogram23import (4 "image/color"5 "strings"6 "testing"78 "charm.land/lipgloss/v2"9 "github.com/FelineStateMachine/puzzletea/game"10 "github.com/FelineStateMachine/puzzletea/theme"11 "github.com/charmbracelet/x/ansi"12)1314func TestGridViewUsesOnlyOuterBordersAndFiveCellSeparators(t *testing.T) {15 g := newGrid(createEmptyState(10, 10))1617 lines := strings.Split(ansi.Strip(gridView(g, game.Cursor{X: 4, Y: 4}, false)), "\n")18 if got, want := len(lines), 13; got != want {19 t.Fatalf("line count = %d, want %d", got, want)20 }21 if got, want := lipgloss.Width(lines[0]), 33; got != want {22 t.Fatalf("top row width = %d, want %d", got, want)23 }24 wantTop := "┌" + strings.Repeat("─", cellWidth*5) + "┬" + strings.Repeat("─", cellWidth*5) + "┐"25 if got := lines[0]; got != wantTop {26 t.Fatalf("top row = %q, want %q", got, wantTop)27 }2829 contentRow := []rune(lines[1])30 if got := contentRow[0]; got != '│' {31 t.Fatalf("left border = %q, want %q", got, '│')32 }33 if got := contentRow[16]; got != '│' {34 t.Fatalf("five-cell separator = %q, want %q", got, '│')35 }36 if got := contentRow[32]; got != '│' {37 t.Fatalf("right border = %q, want %q", got, '│')38 }39 if got := strings.Count(lines[1], "│"); got != 3 {40 t.Fatalf("content row border count = %d, want 3", got)41 }4243 wantDivider := "├" + strings.Repeat("─", cellWidth*5) + "┼" + strings.Repeat("─", cellWidth*5) + "┤"44 if got := lines[6]; got != wantDivider {45 t.Fatalf("major divider = %q, want %q", got, wantDivider)46 }47}4849func TestSeparatorBridgeFillHighlightsOnlySpacerEdgesOnAxis(t *testing.T) {50 cursor := game.Cursor{X: 4, Y: 4}51 surface := theme.Current().Surface5253 tests := []struct {54 name string55 bridge game.DynamicGridBridge56 want color.Color57 }{58 {59 name: "vertical spacer on cursor row",60 bridge: game.DynamicGridBridge{61 Kind: game.DynamicGridBridgeVertical,62 X: 5,63 Y: 4,64 },65 want: surface,66 },67 {68 name: "horizontal spacer on cursor column",69 bridge: game.DynamicGridBridge{70 Kind: game.DynamicGridBridgeHorizontal,71 X: 4,72 Y: 5,73 },74 want: surface,75 },76 {77 name: "normal interior bridge stays empty",78 bridge: game.DynamicGridBridge{79 Kind: game.DynamicGridBridgeVertical,80 X: 1,81 Y: 4,82 },83 want: nil,84 },85 {86 name: "outer border stays empty",87 bridge: game.DynamicGridBridge{88 Kind: game.DynamicGridBridgeHorizontal,89 X: 4,90 Y: 0,91 },92 want: nil,93 },94 }9596 for _, tt := range tests {97 t.Run(tt.name, func(t *testing.T) {98 if got := separatorBridgeFill(cursor, false, 10, 10, tt.bridge); !game.SameColor(got, tt.want) {99 t.Fatalf("separatorBridgeFill() = %v, want %v", got, tt.want)100 }101 })102 }103104 if got := separatorBridgeFill(cursor, true, 10, 10, game.DynamicGridBridge{105 Kind: game.DynamicGridBridgeVertical,106 X: 5,107 Y: 4,108 }); got != nil {109 t.Fatalf("solved separatorBridgeFill() = %v, want nil", got)110 }111}112113func TestTileViewUsesSharedCrosshairSurfaceTint(t *testing.T) {114 p := theme.Current()115116 gotFilled := tileView(filledTile, false, true, false, false)117 wantFilled := filledStyle().118 Background(p.Surface).119 Width(cellWidth).120 AlignHorizontal(lipgloss.Center).121 Render(renderRuneMap[filledTile])122 if gotFilled != wantFilled {123 t.Fatalf("filled crosshair tileView() = %q, want %q", gotFilled, wantFilled)124 }125126 gotEmpty := tileView(emptyTile, false, false, true, false)127 wantEmpty := emptyStyle().128 Background(p.Surface).129 Width(cellWidth).130 AlignHorizontal(lipgloss.Center).131 Render(renderRuneMap[emptyTile])132 if gotEmpty != wantEmpty {133 t.Fatalf("empty crosshair tileView() = %q, want %q", gotEmpty, wantEmpty)134 }135136 gotCursor := tileView(filledTile, true, true, true, false)137 wantCursor := game.CursorStyle().138 Width(cellWidth).139 AlignHorizontal(lipgloss.Center).140 Render(game.CursorLeft + string([]rune(renderRuneMap[filledTile])[1]) + game.CursorRight)141 if gotCursor != wantCursor {142 t.Fatalf("cursor tileView() = %q, want %q", gotCursor, wantCursor)143 }144}145146func TestBuildBoardBlockPreservesHintBandLayout(t *testing.T) {147 m := Model{148 width: 5,149 height: 5,150 rowHints: TomographyDefinition{{1}, {2}, {3}, {4}, {5}},151 colHints: TomographyDefinition{{1}, {2}, {3}, {4}, {5}},152 grid: newGrid(createEmptyState(5, 5)),153 cursor: game.Cursor{X: 0, Y: 0},154 }155156 layout := buildBoardBlock(m)157 lines := strings.Split(ansi.Strip(layout.Block), "\n")158 if len(lines) <= layout.HintHeight {159 t.Fatalf("block height = %d, want > hint height %d", len(lines), layout.HintHeight)160 }161 if got := lipgloss.Width(lines[layout.HintHeight]); got <= layout.HintWidth {162 t.Fatalf("content row width = %d, want > hint width %d", got, layout.HintWidth)163 }164 if got := []rune(lines[layout.HintHeight])[0]; got != ' ' {165 t.Fatalf("row hint should render outside the grid border, got %q", got)166 }167 if got := []rune(lines[layout.HintHeight])[layout.HintWidth]; got != '┌' {168 t.Fatalf("grid top border should start after hint band, got %q", got)169 }170 if strings.TrimSpace(lines[layout.HintHeight][:layout.HintWidth]) != "" {171 t.Fatalf("row hints should not occupy the grid border row, got %q", lines[layout.HintHeight][:layout.HintWidth])172 }173 if strings.TrimSpace(lines[layout.HintHeight+1][:layout.HintWidth]) == "" {174 t.Fatalf("row hints should begin on the first grid content row, got %q", lines[layout.HintHeight+1][:layout.HintWidth])175 }176 if got := []rune(lines[layout.HintHeight+1])[layout.HintWidth]; got != '│' {177 t.Fatalf("grid content should remain offset after hint band, got %q", got)178 }179}180181func TestBuildBoardBlockAlignsHintSeparatorsWithGrid(t *testing.T) {182 m := Model{183 width: 10,184 height: 10,185 rowHints: repeatedHints(10, []int{1}),186 colHints: repeatedHints(10, []int{1}),187 grid: newGrid(createEmptyState(10, 10)),188 currentHints: Hints{rows: repeatedHints(10, []int{1}), cols: repeatedHints(10, []int{1})},189 }190191 layout := buildBoardBlock(m)192 lines := strings.Split(ansi.Strip(layout.Block), "\n")193 separatorColumn := layout.HintWidth + 1 + cellWidth*5194 if got := []rune(lines[layout.HintHeight-1])[separatorColumn]; got != '│' {195 t.Fatalf("column hint separator = %q, want %q", got, '│')196 }197 if got := []rune(lines[layout.HintHeight])[separatorColumn]; got != '┬' {198 t.Fatalf("top border separator = %q, want %q", got, '┬')199 }200 if got := []rune(lines[layout.HintHeight+6])[separatorColumn]; got != '┼' {201 t.Fatalf("row divider separator = %q, want %q", got, '┼')202 }203}204205func TestBuildBoardBlockDimensionsAcrossModes(t *testing.T) {206 tests := []struct {207 name string208 size int209 }{210 {name: "5x5", size: 5},211 {name: "10x10", size: 10},212 {name: "15x15", size: 15},213 {name: "20x20", size: 20},214 }215216 for _, tt := range tests {217 t.Run(tt.name, func(t *testing.T) {218 hints := repeatedHints(tt.size, []int{1})219 m := Model{220 width: tt.size,221 height: tt.size,222 rowHints: hints,223 colHints: hints,224 grid: newGrid(createEmptyState(tt.size, tt.size)),225 currentHints: Hints{rows: hints, cols: hints},226 }227228 layout := buildBoardBlock(m)229 if layout.HintWidth <= 0 || layout.HintHeight <= 0 {230 t.Fatalf("hint bands should be non-zero, got (%d,%d)", layout.HintWidth, layout.HintHeight)231 }232 if lipgloss.Width(layout.Block) < layout.HintWidth+lipgloss.Width(layout.Grid) {233 t.Fatalf("block width = %d, want at least %d", lipgloss.Width(layout.Block), layout.HintWidth+lipgloss.Width(layout.Grid))234 }235 if lipgloss.Height(layout.Block) != layout.HintHeight+lipgloss.Height(layout.Grid) {236 t.Fatalf("block height = %d, want %d", lipgloss.Height(layout.Block), layout.HintHeight+lipgloss.Height(layout.Grid))237 }238 })239 }240}241242func TestHintViewsUseSatisfiedStyle(t *testing.T) {243 rowHints := TomographyDefinition{{3}, {1, 1}}244 rowView := rowHintView(rowHints, 5, rowHints)245 wantRow := hintSatisfiedStyle().Width(5).Align(lipgloss.Right).Render(" 3")246 if !strings.Contains(rowView, wantRow) {247 t.Fatalf("rowHintView() = %q, want satisfied row %q", rowView, wantRow)248 }249250 colHints := TomographyDefinition{{2}, {1}}251 colView := colHintView(colHints, 1, colHints)252 wantCol := hintSatisfiedStyle().Width(cellWidth).Align(lipgloss.Center).Render("2")253 if !strings.Contains(colView, wantCol) {254 t.Fatalf("colHintView() = %q, want satisfied column %q", colView, wantCol)255 }256}257258func repeatedHints(size int, hint []int) TomographyDefinition {259 hints := make(TomographyDefinition, size)260 for i := range size {261 hints[i] = append([]int(nil), hint...)262 }263 return hints264}