clone

an offline sliding-block puzzle game in gleam and lustre, with an endless ladder of puzzles ranked by search

main
klot.ski / src / klotski / domain.gleam
domain.gleam 6.2K
1import gleam/int2import gleam/list3import gleam/result4import gleam/set56pub type Shape {7 Single8 Upright9 Recumbent10 Goal11}1213pub type Direction {14 Up15 Down16 Left17 Right18}1920pub type Board {21 Board(width: Int, height: Int, goal_x: Int, goal_y: Int)22}2324pub type Piece {25 Piece(id: Int, shape: Shape, x: Int, y: Int, width: Int, height: Int)26}2728pub type Move {29 Move(piece_id: Int, direction: Direction, cells: Int)30}3132pub fn pieces_from_grid(board: Board, grid: List(Int)) -> List(Piece) {33 let cells = indices(board.width * board.height)34 let #(pieces, _, _) =35 list.fold(cells, #([], set.new(), 0), fn(state, index) {36 let #(pieces, covered, next_id) = state37 let code = grid |> list.drop(index) |> list.first |> result.unwrap(0)3839 case code == 0 || set.contains(covered, index) {40 True -> state41 False -> {42 let #(shape, width, height) = dimensions(code)43 let x = index % board.width44 let y = index / board.width45 let occupied = occupied_indices(board, x, y, width, height)46 let covered = list.fold(occupied, covered, set.insert)47 #(48 [Piece(next_id, shape, x, y, width, height), ..pieces],49 covered,50 next_id + 1,51 )52 }53 }54 })5556 list.reverse(pieces)57}5859fn dimensions(code: Int) -> #(Shape, Int, Int) {60 case code {61 1 -> #(Single, 1, 1)62 2 -> #(Upright, 1, 2)63 3 -> #(Recumbent, 2, 1)64 _ -> #(Goal, 2, 2)65 }66}6768fn occupied_indices(69 board: Board,70 x: Int,71 y: Int,72 width: Int,73 height: Int,74) -> List(Int) {75 list.flat_map(indices(height), fn(dy) {76 list.map(indices(width), fn(dx) { { y + dy } * board.width + x + dx })77 })78}7980fn indices(count: Int) -> List(Int) {81 int.range(from: 0, to: count, with: [], run: fn(items, index) {82 [index, ..items]83 })84 |> list.reverse85}8687pub fn direction_delta(direction: Direction) -> #(Int, Int) {88 case direction {89 Up -> #(0, -1)90 Down -> #(0, 1)91 Left -> #(-1, 0)92 Right -> #(1, 0)93 }94}9596pub fn opposite(direction: Direction) -> Direction {97 case direction {98 Up -> Down99 Down -> Up100 Left -> Right101 Right -> Left102 }103}104105pub fn piece_by_id(pieces: List(Piece), id: Int) -> Result(Piece, Nil) {106 list.find(pieces, fn(piece) { piece.id == id })107}108109pub fn can_move(110 board: Board,111 pieces: List(Piece),112 piece_id: Int,113 direction: Direction,114) -> Bool {115 case piece_by_id(pieces, piece_id) {116 Error(_) -> False117 Ok(piece) -> {118 let #(dx, dy) = direction_delta(direction)119 let moved = Piece(..piece, x: piece.x + dx, y: piece.y + dy)120 let in_bounds =121 moved.x >= 0122 && moved.y >= 0123 && moved.x + moved.width <= board.width124 && moved.y + moved.height <= board.height125126 in_bounds127 && !list.any(pieces, fn(other) {128 other.id != moved.id && overlaps(moved, other)129 })130 }131 }132}133134fn overlaps(a: Piece, b: Piece) -> Bool {135 a.x < b.x + b.width136 && a.x + a.width > b.x137 && a.y < b.y + b.height138 && a.y + a.height > b.y139}140141pub fn move_one(142 board: Board,143 pieces: List(Piece),144 piece_id: Int,145 direction: Direction,146) -> Result(List(Piece), Nil) {147 case can_move(board, pieces, piece_id, direction) {148 False -> Error(Nil)149 True -> Ok(move_unchecked(pieces, piece_id, direction))150 }151}152153pub fn move_many(154 board: Board,155 pieces: List(Piece),156 piece_id: Int,157 direction: Direction,158 requested: Int,159) -> #(List(Piece), Int) {160 move_many_loop(board, pieces, piece_id, direction, int.max(0, requested), 0)161}162163fn move_many_loop(164 board: Board,165 pieces: List(Piece),166 piece_id: Int,167 direction: Direction,168 remaining: Int,169 moved: Int,170) -> #(List(Piece), Int) {171 case remaining <= 0 {172 True -> #(pieces, moved)173 False ->174 case move_one(board, pieces, piece_id, direction) {175 Error(_) -> #(pieces, moved)176 Ok(next) ->177 move_many_loop(178 board,179 next,180 piece_id,181 direction,182 remaining - 1,183 moved + 1,184 )185 }186 }187}188189pub fn move_unchecked(190 pieces: List(Piece),191 piece_id: Int,192 direction: Direction,193) -> List(Piece) {194 let #(dx, dy) = direction_delta(direction)195 list.map(pieces, fn(piece) {196 case piece.id == piece_id {197 True -> Piece(..piece, x: piece.x + dx, y: piece.y + dy)198 False -> piece199 }200 })201}202203pub fn move_options(204 board: Board,205 pieces: List(Piece),206 piece_id: Int,207) -> List(Direction) {208 [Up, Down, Left, Right]209 |> list.filter(can_move(board, pieces, piece_id, _))210}211212pub fn free_cells(213 board: Board,214 pieces: List(Piece),215 piece_id: Int,216 direction: Direction,217) -> Int {218 let #(_, moved) = move_many(board, pieces, piece_id, direction, 6)219 moved220}221222pub fn is_solved(board: Board, pieces: List(Piece)) -> Bool {223 list.any(pieces, fn(piece) {224 piece.shape == Goal && piece.x == board.goal_x && piece.y == board.goal_y225 })226}227228pub fn goal_gap(board: Board, pieces: List(Piece)) -> Int {229 case list.find(pieces, fn(piece) { piece.shape == Goal }) {230 Error(_) -> 0231 Ok(piece) ->232 int.absolute_value(piece.x - board.goal_x)233 + int.absolute_value(piece.y - board.goal_y)234 }235}236237/// Encode a configuration using the same base-five, identity-free key as the238/// browser survey worker. Pieces of the same shape are interchangeable.239pub fn encode_state(board: Board, pieces: List(Piece)) -> Int {240 indices(board.width * board.height)241 |> list.map(fn(index) {242 let x = index % board.width243 let y = index / board.width244 pieces245 |> list.find(fn(piece) {246 x >= piece.x247 && x < piece.x + piece.width248 && y >= piece.y249 && y < piece.y + piece.height250 })251 |> result.map(fn(piece) { shape_code(piece.shape) })252 |> result.unwrap(0)253 })254 |> list.reverse255 |> list.fold(0, fn(key, code) { key * 5 + code })256}257258fn shape_code(shape: Shape) -> Int {259 case shape {260 Single -> 1261 Upright -> 2262 Recumbent -> 3263 Goal -> 4264 }265}266267pub fn shape_name(shape: Shape) -> String {268 case shape {269 Single -> "single"270 Upright -> "upright"271 Recumbent -> "recumbent"272 Goal -> "goal"273 }274}275276/// The piece class the renderer and the survey key both speak in: 1 single,277/// 2 upright, 3 recumbent, 4 goal.278pub fn shape_class(shape: Shape) -> Int {279 shape_code(shape)280}