This commit is contained in:
commit
842ef0413b
7 changed files with 295 additions and 0 deletions
23
.github/workflows/test.yml
vendored
Normal file
23
.github/workflows/test.yml
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
name: test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: erlef/setup-beam@v1
|
||||||
|
with:
|
||||||
|
otp-version: "27.1.2"
|
||||||
|
gleam-version: "1.10.0"
|
||||||
|
rebar3-version: "3"
|
||||||
|
# elixir-version: "1"
|
||||||
|
- run: gleam deps download
|
||||||
|
- run: gleam test
|
||||||
|
- run: gleam format --check src test
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
*.beam
|
||||||
|
*.ez
|
||||||
|
/build
|
||||||
|
erl_crash.dump
|
24
README.md
Normal file
24
README.md
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# minesweeper_engine
|
||||||
|
|
||||||
|
[](https://hex.pm/packages/minesweeper_engine)
|
||||||
|
[](https://hexdocs.pm/minesweeper_engine/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gleam add minesweeper_engine@1
|
||||||
|
```
|
||||||
|
```gleam
|
||||||
|
import minesweeper_engine
|
||||||
|
|
||||||
|
pub fn main() -> Nil {
|
||||||
|
// TODO: An example of the project in use
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Further documentation can be found at <https://hexdocs.pm/minesweeper_engine>.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gleam run # Run the project
|
||||||
|
gleam test # Run the tests
|
||||||
|
```
|
19
gleam.toml
Normal file
19
gleam.toml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
name = "minesweeper_engine"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
# Fill out these fields if you intend to generate HTML documentation or publish
|
||||||
|
# your project to the Hex package manager.
|
||||||
|
#
|
||||||
|
# description = ""
|
||||||
|
# licences = ["Apache-2.0"]
|
||||||
|
# repository = { type = "github", user = "", repo = "" }
|
||||||
|
# links = [{ title = "Website", href = "" }]
|
||||||
|
#
|
||||||
|
# For a full reference of all the available options, you can have a look at
|
||||||
|
# https://gleam.run/writing-gleam/gleam-toml/.
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
gleeunit = ">= 1.0.0 and < 2.0.0"
|
11
manifest.toml
Normal file
11
manifest.toml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# This file was generated by Gleam
|
||||||
|
# You typically do not need to edit this file
|
||||||
|
|
||||||
|
packages = [
|
||||||
|
{ name = "gleam_stdlib", version = "0.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" },
|
||||||
|
{ name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[requirements]
|
||||||
|
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
|
||||||
|
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
|
2
mise.toml
Normal file
2
mise.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[tools]
|
||||||
|
gleam = "1.9"
|
212
src/game.gleam
Normal file
212
src/game.gleam
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
import gleam/dict.{type Dict}
|
||||||
|
import gleam/int
|
||||||
|
import gleam/list
|
||||||
|
|
||||||
|
type Position {
|
||||||
|
Position(x: Int, y: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Offset {
|
||||||
|
Offset(x: Int, y: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn offset_position(position: Position, offset: Offset) -> Position {
|
||||||
|
Position(position.x + offset.x, position.y + offset.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub opaque type Board {
|
||||||
|
Board(width: Int, height: Int, bombs: Int, cells: Dict(Position, Cell))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub opaque type Cell {
|
||||||
|
Cell(state: CellState, is_bomb: Bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type CellState {
|
||||||
|
Revealed(adjacent_bombs: Int)
|
||||||
|
Flagged
|
||||||
|
Unflagged
|
||||||
|
Exploded
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_board(width, height, bombs, x, y) {
|
||||||
|
let offsets = [
|
||||||
|
Offset(-1, -1),
|
||||||
|
Offset(0, -1),
|
||||||
|
Offset(1, -1),
|
||||||
|
Offset(-1, 0),
|
||||||
|
Offset(0, 0),
|
||||||
|
Offset(1, 0),
|
||||||
|
Offset(-1, 1),
|
||||||
|
Offset(0, 1),
|
||||||
|
Offset(1, 1),
|
||||||
|
]
|
||||||
|
let click = Position(x, y)
|
||||||
|
let safe_cells =
|
||||||
|
offsets
|
||||||
|
|> list.map(fn(o) { click |> offset_position(o) })
|
||||||
|
|> list.filter(fn(p) { p.x >= 0 && p.x < width && p.y >= 0 && p.y < height })
|
||||||
|
|
||||||
|
safe_cells |> create_board_from_safe_cells(width, height, bombs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_board_from_safe_cells(safe_cells, width, height, bombs) {
|
||||||
|
let bomb_list = [] |> create_board_inner(width, height, bombs, safe_cells)
|
||||||
|
let cells = dict.new() |> create_cells(bomb_list, 0, 0, width, height)
|
||||||
|
Board(width, height, bombs, cells)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_board_inner(
|
||||||
|
bomb_list: List(Position),
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
bombs: Int,
|
||||||
|
safe_cells: List(Position),
|
||||||
|
) -> List(Position) {
|
||||||
|
use <-
|
||||||
|
fn(c) {
|
||||||
|
case bomb_list |> list.length() >= bombs {
|
||||||
|
True -> bomb_list
|
||||||
|
False -> c()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = int.random(width)
|
||||||
|
let y = int.random(height)
|
||||||
|
let cell_position = Position(x, y)
|
||||||
|
|
||||||
|
let is_not_safe_cell = !{ safe_cells |> list.contains(cell_position) }
|
||||||
|
let is_not_in_bomb_list = !{ bomb_list |> list.contains(cell_position) }
|
||||||
|
|
||||||
|
let bomb_list = case is_not_safe_cell && is_not_in_bomb_list {
|
||||||
|
False -> bomb_list
|
||||||
|
True -> [cell_position, ..bomb_list]
|
||||||
|
}
|
||||||
|
|
||||||
|
bomb_list |> create_board_inner(width, height, bombs, safe_cells)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_cells(
|
||||||
|
cells: Dict(Position, Cell),
|
||||||
|
bomb_list: List(Position),
|
||||||
|
x: Int,
|
||||||
|
y: Int,
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
) -> Dict(Position, Cell) {
|
||||||
|
let position = Position(x, y)
|
||||||
|
let cell = Cell(Unflagged, bomb_list |> list.contains(position))
|
||||||
|
let cells = cells |> dict.insert(position, cell)
|
||||||
|
|
||||||
|
let x = x + 1
|
||||||
|
let #(x, y) = case x >= width {
|
||||||
|
True -> #(0, y + 1)
|
||||||
|
False -> #(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
case y >= height {
|
||||||
|
True -> cells
|
||||||
|
False -> cells |> create_cells(bomb_list, x, y, width, height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cell(board: Board, x: Int, y: Int) -> Cell {
|
||||||
|
case board.cells |> dict.get(Position(x, y)) {
|
||||||
|
Error(_) -> panic as "tried to get a non-existent cell"
|
||||||
|
Ok(cell) -> cell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_adjacent(board: Board, x, y) {
|
||||||
|
let offsets = [
|
||||||
|
Offset(-1, -1),
|
||||||
|
Offset(0, -1),
|
||||||
|
Offset(1, -1),
|
||||||
|
Offset(-1, 0),
|
||||||
|
Offset(1, 0),
|
||||||
|
Offset(-1, 1),
|
||||||
|
Offset(0, 1),
|
||||||
|
Offset(1, 1),
|
||||||
|
]
|
||||||
|
|
||||||
|
let position = Position(x, y)
|
||||||
|
offsets
|
||||||
|
|> list.map(fn(offset) { position |> offset_position(offset) })
|
||||||
|
|> list.filter(fn(pos) {
|
||||||
|
pos.x >= 0 && pos.x < board.width && pos.y >= 0 && pos.y < board.height
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_cell(board: Board, x: Int, y: Int, cell: Cell) -> Board {
|
||||||
|
let cells = board.cells |> dict.insert(Position(x, y), cell)
|
||||||
|
Board(..board, cells: cells)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reveal(board: Board, x: Int, y: Int) -> #(Board, CellState) {
|
||||||
|
use #(board, state) <-
|
||||||
|
fn(callback) {
|
||||||
|
let state = { board |> get_cell(x, y) }.state
|
||||||
|
case state {
|
||||||
|
Revealed(_) -> #(board, state)
|
||||||
|
_ -> {
|
||||||
|
let #(board, state) = board |> reveal_non_recursive(x, y)
|
||||||
|
callback(#(board, state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case state {
|
||||||
|
Unflagged -> panic as "cell should not be unflagged after reveal"
|
||||||
|
Flagged -> #(board, state)
|
||||||
|
Exploded -> #(board, state)
|
||||||
|
Revealed(0) -> {
|
||||||
|
let board =
|
||||||
|
board
|
||||||
|
|> get_adjacent(x, y)
|
||||||
|
|> list.fold(board, fn(board, pos) {
|
||||||
|
let #(board, _) = board |> reveal(pos.x, pos.y)
|
||||||
|
board
|
||||||
|
})
|
||||||
|
#(board, state)
|
||||||
|
}
|
||||||
|
Revealed(_) -> #(board, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reveal_non_recursive(board: Board, x: Int, y: Int) -> #(Board, CellState) {
|
||||||
|
let cell = board |> get_cell(x, y)
|
||||||
|
case cell.state {
|
||||||
|
Revealed(_) -> #(board, cell.state)
|
||||||
|
Flagged -> #(board, cell.state)
|
||||||
|
Exploded -> #(board, cell.state)
|
||||||
|
Unflagged -> {
|
||||||
|
let new_cell = case cell.is_bomb {
|
||||||
|
True -> Cell(..cell, state: Exploded)
|
||||||
|
False -> {
|
||||||
|
let adjacent_bombs =
|
||||||
|
board
|
||||||
|
|> get_adjacent(x, y)
|
||||||
|
|> list.fold(0, fn(bombs, pos) {
|
||||||
|
case { board |> get_cell(pos.x, pos.y) }.is_bomb {
|
||||||
|
True -> bombs + 1
|
||||||
|
False -> bombs
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Cell(..cell, state: Revealed(adjacent_bombs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#(board |> update_cell(x, y, new_cell), new_cell.state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flag(board: Board, x: Int, y: Int) -> #(Board, CellState) {
|
||||||
|
let cell = board |> get_cell(x, y)
|
||||||
|
let new_state = case cell.state {
|
||||||
|
Revealed(_) -> cell.state
|
||||||
|
Exploded -> cell.state
|
||||||
|
Unflagged -> Flagged
|
||||||
|
Flagged -> Unflagged
|
||||||
|
}
|
||||||
|
#(board |> update_cell(x, y, Cell(..cell, state: new_state)), new_state)
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue