From 842ef0413bd175fa51b54152a5697f48669f3e0d Mon Sep 17 00:00:00 2001 From: Sebastian Bugge Date: Sun, 27 Apr 2025 17:02:57 +0200 Subject: [PATCH] Add basic game interactions. --- .github/workflows/test.yml | 23 ++++ .gitignore | 4 + README.md | 24 +++++ gleam.toml | 19 ++++ manifest.toml | 11 ++ mise.toml | 2 + src/game.gleam | 212 +++++++++++++++++++++++++++++++++++++ 7 files changed, 295 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 gleam.toml create mode 100644 manifest.toml create mode 100644 mise.toml create mode 100644 src/game.gleam diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6f6924a --- /dev/null +++ b/.github/workflows/test.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/README.md b/README.md new file mode 100644 index 0000000..4ffb2dc --- /dev/null +++ b/README.md @@ -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 . + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` diff --git a/gleam.toml b/gleam.toml new file mode 100644 index 0000000..493839b --- /dev/null +++ b/gleam.toml @@ -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" diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..22f5a58 --- /dev/null +++ b/manifest.toml @@ -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" } diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..e279079 --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +gleam = "1.9" diff --git a/src/game.gleam b/src/game.gleam new file mode 100644 index 0000000..b164dec --- /dev/null +++ b/src/game.gleam @@ -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) +}