From 123144c93bfc77883c8fb517828b47bbe13b8671 Mon Sep 17 00:00:00 2001 From: Sebastiano Tronto Date: Mon, 21 Apr 2025 11:09:56 +0200 Subject: Initial commit --- minesweeper.ha | 318 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 minesweeper.ha (limited to 'minesweeper.ha') diff --git a/minesweeper.ha b/minesweeper.ha new file mode 100644 index 0000000..4567c7b --- /dev/null +++ b/minesweeper.ha @@ -0,0 +1,318 @@ +use fmt; +use math::random; +use raylib; +use strings; +use time; + +// Types +type cell = struct { + is_mine: bool, + is_hidden: bool, + is_flag: bool, + near_mines: int, + near_flags: int +}; + +type board = struct { + cells: []cell, + last_clicked: (int | void), + width: int, + height: int, + nmines: int, + nfreed: int, + finished: bool, +}; + +// Gameplay constants +const BOARD_WIDTH = 8; +const BOARD_HEIGHT = 8; +const NMINES = 10; + +// Graphics-related constants +const WINDOW_WIDTH = 1000; +const WINDOW_HEIGHT = 800; +const WINDOW_TITLE = "Minesweeper"; + +const FLAG_SIZE = 40; +const CELL_SIZE = 80; +const HALFGAP_SIZE = 4; +const BOTTOM_BAR_THICKNESS = 40; + +const COLOR_BACKGROUND = raylib::color { r = 230, g = 230, b = 230, a = 255 }; +const COLOR_FREE = raylib::color { r = 200, g = 200, b = 200, a = 255 }; +const COLOR_HIDDEN = raylib::color { r = 100, g = 100, b = 100, a = 255 }; +const COLOR_FLAG = raylib::color { r = 30, g = 30, b = 30, a = 255 }; +const COLOR_MINE = raylib::color { r = 255, g = 0, b = 0, a = 255 }; +const COLOR_CLICKED_MINE = raylib::color { r = 255, g = 100, b = 100, a = 255 }; +const COLOR_NR: [9]raylib::color = [ + // TODO, maybe: different color for each number + raylib::color { r = 0, g = 0, b = 0, a = 255 }, // 0, unused + raylib::color { r = 0, g = 0, b = 0, a = 255 }, // 1 + raylib::color { r = 0, g = 0, b = 0, a = 255 }, // 2 + raylib::color { r = 0, g = 0, b = 0, a = 255 }, // 3 + raylib::color { r = 0, g = 0, b = 0, a = 255 }, // 4 + raylib::color { r = 0, g = 0, b = 0, a = 255 }, // 5 + raylib::color { r = 0, g = 0, b = 0, a = 255 }, // 6 + raylib::color { r = 0, g = 0, b = 0, a = 255 }, // 7 + raylib::color { r = 0, g = 0, b = 0, a = 255 }, // 8 +]; + +let bottom_left_text = "Playing..."; + +fn draw_bottom_bar() void = { + const color_text = raylib::color { r = 200, g = 0, b = 0, a = 255 }; + const text_gap = 4; + let h = raylib::get_screen_height(); + let w = raylib::get_screen_width(); + let tl = raylib::vector2 { x = 0.0, y = (h - BOTTOM_BAR_THICKNESS) : f32 }; + let sz = raylib::vector2 { x = w: f32, y = h: f32 }; + + raylib::draw_rectangle_v(tl, sz, COLOR_BACKGROUND); + raylib::draw_text(bottom_left_text, text_gap, + (h - BOTTOM_BAR_THICKNESS + text_gap), + (BOTTOM_BAR_THICKNESS - 2 * text_gap), color_text); +}; + +fn foreach_neighbor( + b: *board, + ii: int, + f: *fn(b: *board, jj: int) void +) void = { + let i = ii / b.width; + let j = ii % b.width; + + const neighbors: [8](int, int) = [ + (i-1, j-1), (i-1, j), (i-1, j+1), + (i, j-1), (i, j+1), + (i+1, j-1), (i+1, j), (i+1, j+1) + ]; + for (let p .. neighbors) + if (p.0 >= 0 && p.0 < b.height && + p.1 >= 0 && p.1 < b.width) + f(b, p.0*b.width + p.1); +}; + +fn inc_near_mines(b: *board, ii: int) void = b.cells[ii].near_mines += 1; +fn inc_near_flags(b: *board, ii: int) void = b.cells[ii].near_flags += 1; +fn dec_near_flags(b: *board, ii: int) void = b.cells[ii].near_flags -= 1; + +fn new_board(w: int, h: int, n: int) *board = { + let empty_cell = cell { + is_mine = false, + is_hidden = true, + is_flag = false, + near_mines = 0, + near_flags = 0 + }; + let b: *board = alloc(board { + cells = alloc([empty_cell ...], (w*h): size)!, + last_clicked = void, + width = w, + height = h, + nmines = n, + nfreed = 0, + finished = false + })!; + + const seed: u64 = time::now(time::clock::REALTIME).sec : u64; + const rand: math::random::random = math::random::init(seed); + for (let i = 0; i < n; i += 1) { + let j = math::random::u32n(&rand, (w*h-i): u32): int; + for (let k = 0; k <= j; k += 1) + if (b.cells[k].is_mine) + j += 1; + b.cells[j].is_mine = true; + foreach_neighbor(b, j, &inc_near_mines); + }; + return b; +}; + +fn delete_board(b: *board) void = { + free(b.cells); + free(b); +}; + +fn game_lost(b: *board) void = { + for (let c &.. b.cells) + c.is_hidden = false; + bottom_left_text = "You lost! Press n for a new game"; + b.finished = true; +}; + +fn game_won(b: *board) void = { + for (let c &.. b.cells) + c.is_hidden = false; + bottom_left_text = "You won! Press n for a new game"; + b.finished = true; +}; + +fn leftclick_if_hidden_noflag(b: *board, i: int) void = { + if (b.cells[i].is_hidden && ! b.cells[i].is_flag) + leftclick_cell(b, i); +}; + +fn leftclick_cell(b: *board, i: int) void = { + let c = &(b.cells[i]); + if (c.is_hidden) { + if (c.is_flag) + return; + b.last_clicked = i; + c.is_hidden = false; + if (c.is_mine) { + game_lost(b); + } else { + b.nfreed += 1; + if (b.nfreed == len(b.cells): int - b.nmines) + game_won(b); + }; + if (c.near_mines == 0 && c.near_flags == 0) + foreach_neighbor(b, i, &leftclick_if_hidden_noflag); + } else { + if (c.near_flags == c.near_mines) + foreach_neighbor(b, i, &leftclick_if_hidden_noflag); + }; +}; + +fn rightclick_cell(b: *board, i: int) void = { + let c = &(b.cells[i]); + if (c.is_hidden) { + c.is_flag = !c.is_flag; + const f = if (c.is_flag) &inc_near_flags else &dec_near_flags; + foreach_neighbor(b, i, f); + } else { + return; + }; +}; + +// Return true if the user started a new game +fn handle_interaction(b: *board) bool = { + const p = raylib::get_mouse_position(); + if (raylib::is_mouse_button_pressed(raylib::MOUSE_BUTTON_LEFT)) { + match (get_cell_index(b, p)) { + case void => return false; + case let i: int => leftclick_cell(b, i); + }; + }; + if (raylib::is_mouse_button_pressed(raylib::MOUSE_BUTTON_RIGHT)) { + match (get_cell_index(b, p)) { + case void => return false; + case let i: int => rightclick_cell(b, i); + }; + }; + if (raylib::is_key_pressed(raylib::KEY_N) && b.finished) + return true; + return false; +}; + +fn top_gap() int = { + let h = raylib::get_screen_height(); + return (h - BOTTOM_BAR_THICKNESS - BOARD_HEIGHT * CELL_SIZE) / 2; +}; + +fn side_gap() int = { + let w = raylib::get_screen_width(); + return (w - BOARD_WIDTH * CELL_SIZE) / 2; +}; + +fn cell_pos(b: *board, i: int) raylib::vector2 = { + return raylib::vector2 { + x = ((i % b.width) * CELL_SIZE + HALFGAP_SIZE + side_gap()) : f32, + y = ((i / b.width) * CELL_SIZE + HALFGAP_SIZE + top_gap()) : f32 + }; +}; + +fn flag_pos(b: *board, i: int) raylib::vector2 = { + const shift = (CELL_SIZE - FLAG_SIZE) / 2; + return raylib::vector2 { + x = ((i % b.width) * CELL_SIZE + shift + side_gap()) : f32, + y = ((i / b.width) * CELL_SIZE + shift + top_gap()) : f32 + }; +}; + +fn get_cell_index(b: *board, p: raylib::vector2) (int | void) = { + const i = (p.y: int - top_gap()) / CELL_SIZE; + const j: int = (p.x: int - side_gap()) / CELL_SIZE; + if (i < b.height && j < b.width) + return i * b.width + j; +}; + +fn cell_drawsize() raylib::vector2 = raylib::vector2 { + x = (CELL_SIZE - 2*HALFGAP_SIZE) : f32, + y = (CELL_SIZE - 2*HALFGAP_SIZE) : f32 +}; + +fn flag_drawsize() raylib::vector2 = raylib::vector2 { + x = FLAG_SIZE : f32, + y = FLAG_SIZE : f32 +}; + +fn draw_flag(b: *board, i: int) void = + raylib::draw_rectangle_v(flag_pos(b, i), flag_drawsize(), COLOR_FLAG); + +fn draw_cell_hidden(b: *board, i: int) void = { + raylib::draw_rectangle_v(cell_pos(b, i), cell_drawsize(), COLOR_HIDDEN); + if (b.cells[i].is_flag) + draw_flag(b, i); +}; + +fn draw_cell_mine(b: *board, i: int) void = { + const c = match (b.last_clicked) { + case void => yield COLOR_MINE; + case let l: int => yield if (l == i) COLOR_CLICKED_MINE else COLOR_MINE; + }; + raylib::draw_rectangle_v(cell_pos(b, i), cell_drawsize(), c); +}; + +fn draw_cell_free(b: *board, i: int) void = { + let pos = cell_pos(b, i); + let sz = cell_drawsize(); + raylib::draw_rectangle_v(pos, cell_drawsize(), COLOR_FREE); + + const n = b.cells[i].near_mines: u8; + const s = strings::fromutf8([n + '0']) as str; + const w = raylib::measure_text(s, sz.y: int); + const shift_x = (CELL_SIZE: f32 / 2.0) - (w: f32 / 2.0) - (HALFGAP_SIZE: f32); + const shift_y = (CELL_SIZE: f32 / 2.0) - (sz.y / 2.0); + let text_x = (pos.x + shift_x): int; + let text_y = (pos.y + shift_y): int; + raylib::draw_text(s, text_x, text_y, sz.y: int, COLOR_NR[n]); +}; + +fn draw_cell(b: *board, i: int) void = { + const c = b.cells[i]; + if (c.is_hidden) { + draw_cell_hidden(b, i); + } else if (c.is_mine) { + draw_cell_mine(b, i); + } else { + draw_cell_free(b, i); + }; +}; + +fn draw_board(b: *board) void = { + for (let i = 0; i < b.height; i += 1) + for (let j = 0; j < b.width; j += 1) + draw_cell(b, i*b.width + j); +}; + +export fn main() void = { + let game_board = new_board(BOARD_WIDTH, BOARD_HEIGHT, NMINES); + defer delete_board(game_board); + + raylib::set_config_flags(raylib::FLAG_WINDOW_RESIZABLE); + raylib::init_window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE); + for (!raylib::window_should_close()) { + raylib::begin_drawing(); + + raylib::clear_background(COLOR_BACKGROUND); + draw_board(game_board); + draw_bottom_bar(); + const restart = handle_interaction(game_board); + if (restart) { + delete_board(game_board); + game_board = new_board(BOARD_WIDTH, BOARD_HEIGHT, NMINES); + }; + + raylib::end_drawing(); + }; +}; -- cgit v1.3