clone

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

refactor/root-tui-model
puzzletea / games / spellpuzzle / board.go
board.go 4.7K
1package spellpuzzle23import (4 "sort"56 "github.com/FelineStateMachine/puzzletea/game"7)89type Orientation int1011const (12 Horizontal Orientation = iota13 Vertical14)1516type Position struct {17 X int `json:"x"`18 Y int `json:"y"`19}2021type WordPlacement struct {22 Text string `json:"text"`23 Start Position `json:"start"`24 Orientation Orientation `json:"orientation"`25 Found bool `json:"found"`26}2728func (w WordPlacement) Positions() []Position {29 positions := make([]Position, 0, len(w.Text))30 for i := range len(w.Text) {31 pos := w.Start32 if w.Orientation == Horizontal {33 pos.X += i34 } else {35 pos.Y += i36 }37 positions = append(positions, pos)38 }39 return positions40}4142type boardCell struct {43 Letter rune44 Occupied bool45 Revealed bool46 Horizontal bool47 Vertical bool48 Owners uint1649}5051type board struct {52 Width int53 Height int54 Cells [][]boardCell55}5657func buildBoard(placements []WordPlacement) board {58 if len(placements) == 0 {59 return board{}60 }6162 minX, minY := placements[0].Start.X, placements[0].Start.Y63 maxX, maxY := placements[0].Start.X, placements[0].Start.Y64 for _, placement := range placements {65 for _, pos := range placement.Positions() {66 minX = min(minX, pos.X)67 minY = min(minY, pos.Y)68 maxX = max(maxX, pos.X)69 maxY = max(maxY, pos.Y)70 }71 }7273 width := maxX - minX + 174 height := maxY - minY + 175 cells := make([][]boardCell, height)76 for y := range cells {77 cells[y] = make([]boardCell, width)78 }7980 for placementIndex, placement := range placements {81 for i, letter := range placement.Text {82 x := placement.Start.X - minX83 y := placement.Start.Y - minY84 if placement.Orientation == Horizontal {85 x += i86 } else {87 y += i88 }8990 cell := &cells[y][x]91 cell.Letter = letter92 cell.Occupied = true93 cell.Revealed = cell.Revealed || placement.Found94 if placement.Orientation == Horizontal {95 cell.Horizontal = true96 } else {97 cell.Vertical = true98 }99 cell.Owners |= 1 << placementIndex100 }101 }102103 return board{104 Width: width,105 Height: height,106 Cells: cells,107 }108}109110func (b board) cellAt(x, y int) boardCell {111 if y < 0 || y >= b.Height || x < 0 || x >= b.Width {112 return boardCell{}113 }114 return b.Cells[y][x]115}116117func (b board) hasVerticalEdge(x, y int) bool {118 left := b.cellAt(x-1, y)119 right := b.cellAt(x, y)120121 switch {122 case !left.Occupied && !right.Occupied:123 return false124 case !left.Occupied || !right.Occupied:125 return true126 default:127 return left.Owners&right.Owners == 0128 }129}130131func (b board) hasHorizontalEdge(x, y int) bool {132 top := b.cellAt(x, y-1)133 bottom := b.cellAt(x, y)134135 switch {136 case !top.Occupied && !bottom.Occupied:137 return false138 case !top.Occupied || !bottom.Occupied:139 return true140 default:141 return top.Owners&bottom.Owners == 0142 }143}144145func (b board) bridgeFillState(bridge game.DynamicGridBridge) (fill, solved bool) {146 if bridge.Kind == game.DynamicGridBridgeJunction || bridge.Count < 2 {147 return false, false148 }149150 var (151 sharedOwners uint16152 allRevealed = true153 )154 for i := 0; i < bridge.Count; i++ {155 cell := b.cellAt(bridge.Cells[i].X, bridge.Cells[i].Y)156 if !cell.Occupied {157 return false, false158 }159 if i == 0 {160 sharedOwners = cell.Owners161 } else {162 sharedOwners &= cell.Owners163 }164 allRevealed = allRevealed && cell.Revealed165 }166167 if sharedOwners == 0 {168 return false, false169 }170 return true, allRevealed171}172173func normalizePlacements(placements []WordPlacement) []WordPlacement {174 if len(placements) == 0 {175 return nil176 }177178 minX, minY := placements[0].Start.X, placements[0].Start.Y179 for _, placement := range placements {180 for _, pos := range placement.Positions() {181 minX = min(minX, pos.X)182 minY = min(minY, pos.Y)183 }184 }185186 type placementKey struct {187 Text string188 Start Position189 Orientation Orientation190 }191192 merged := make(map[placementKey]WordPlacement, len(placements))193 for _, placement := range placements {194 placement.Start.X -= minX195 placement.Start.Y -= minY196 key := placementKey{197 Text: placement.Text,198 Start: placement.Start,199 Orientation: placement.Orientation,200 }201 existing, ok := merged[key]202 if ok {203 existing.Found = existing.Found || placement.Found204 merged[key] = existing205 continue206 }207 merged[key] = placement208 }209210 normalized := make([]WordPlacement, 0, len(merged))211 for _, placement := range merged {212 normalized = append(normalized, placement)213 }214215 sort.SliceStable(normalized, func(i, j int) bool {216 if len(normalized[i].Text) != len(normalized[j].Text) {217 return len(normalized[i].Text) > len(normalized[j].Text)218 }219 if normalized[i].Start.Y != normalized[j].Start.Y {220 return normalized[i].Start.Y < normalized[j].Start.Y221 }222 return normalized[i].Start.X < normalized[j].Start.X223 })224225 return normalized226}