Add basic game interactions.
Some checks are pending
test / test (push) Waiting to run

This commit is contained in:
Sebastian Bugge 2025-04-27 17:02:57 +02:00
commit 842ef0413b
Signed by: kaholaz
GPG key ID: 2EFFEDEE03519691
7 changed files with 295 additions and 0 deletions

23
.github/workflows/test.yml vendored Normal file
View 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
View file

@ -0,0 +1,4 @@
*.beam
*.ez
/build
erl_crash.dump

24
README.md Normal file
View file

@ -0,0 +1,24 @@
# minesweeper_engine
[![Package Version](https://img.shields.io/hexpm/v/minesweeper_engine)](https://hex.pm/packages/minesweeper_engine)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](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
View 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
View 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
View file

@ -0,0 +1,2 @@
[tools]
gleam = "1.9"

212
src/game.gleam Normal file
View 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)
}