diff options
Diffstat (limited to 'minesweeper.ha')
| -rw-r--r-- | minesweeper.ha | 318 |
1 files changed, 318 insertions, 0 deletions
diff --git a/minesweeper.ha b/minesweeper.ha new file mode 100644 index 0000000..4567c7b --- /dev/null +++ b/minesweeper.ha | |||
| @@ -0,0 +1,318 @@ | |||
| 1 | use fmt; | ||
| 2 | use math::random; | ||
| 3 | use raylib; | ||
| 4 | use strings; | ||
| 5 | use time; | ||
| 6 | |||
| 7 | // Types | ||
| 8 | type cell = struct { | ||
| 9 | is_mine: bool, | ||
| 10 | is_hidden: bool, | ||
| 11 | is_flag: bool, | ||
| 12 | near_mines: int, | ||
| 13 | near_flags: int | ||
| 14 | }; | ||
| 15 | |||
| 16 | type board = struct { | ||
| 17 | cells: []cell, | ||
| 18 | last_clicked: (int | void), | ||
| 19 | width: int, | ||
| 20 | height: int, | ||
| 21 | nmines: int, | ||
| 22 | nfreed: int, | ||
| 23 | finished: bool, | ||
| 24 | }; | ||
| 25 | |||
| 26 | // Gameplay constants | ||
| 27 | const BOARD_WIDTH = 8; | ||
| 28 | const BOARD_HEIGHT = 8; | ||
| 29 | const NMINES = 10; | ||
| 30 | |||
| 31 | // Graphics-related constants | ||
| 32 | const WINDOW_WIDTH = 1000; | ||
| 33 | const WINDOW_HEIGHT = 800; | ||
| 34 | const WINDOW_TITLE = "Minesweeper"; | ||
| 35 | |||
| 36 | const FLAG_SIZE = 40; | ||
| 37 | const CELL_SIZE = 80; | ||
| 38 | const HALFGAP_SIZE = 4; | ||
| 39 | const BOTTOM_BAR_THICKNESS = 40; | ||
| 40 | |||
| 41 | const COLOR_BACKGROUND = raylib::color { r = 230, g = 230, b = 230, a = 255 }; | ||
| 42 | const COLOR_FREE = raylib::color { r = 200, g = 200, b = 200, a = 255 }; | ||
| 43 | const COLOR_HIDDEN = raylib::color { r = 100, g = 100, b = 100, a = 255 }; | ||
| 44 | const COLOR_FLAG = raylib::color { r = 30, g = 30, b = 30, a = 255 }; | ||
| 45 | const COLOR_MINE = raylib::color { r = 255, g = 0, b = 0, a = 255 }; | ||
| 46 | const COLOR_CLICKED_MINE = raylib::color { r = 255, g = 100, b = 100, a = 255 }; | ||
| 47 | const COLOR_NR: [9]raylib::color = [ | ||
| 48 | // TODO, maybe: different color for each number | ||
| 49 | raylib::color { r = 0, g = 0, b = 0, a = 255 }, // 0, unused | ||
| 50 | raylib::color { r = 0, g = 0, b = 0, a = 255 }, // 1 | ||
| 51 | raylib::color { r = 0, g = 0, b = 0, a = 255 }, // 2 | ||
| 52 | raylib::color { r = 0, g = 0, b = 0, a = 255 }, // 3 | ||
| 53 | raylib::color { r = 0, g = 0, b = 0, a = 255 }, // 4 | ||
| 54 | raylib::color { r = 0, g = 0, b = 0, a = 255 }, // 5 | ||
| 55 | raylib::color { r = 0, g = 0, b = 0, a = 255 }, // 6 | ||
| 56 | raylib::color { r = 0, g = 0, b = 0, a = 255 }, // 7 | ||
| 57 | raylib::color { r = 0, g = 0, b = 0, a = 255 }, // 8 | ||
| 58 | ]; | ||
| 59 | |||
| 60 | let bottom_left_text = "Playing..."; | ||
| 61 | |||
| 62 | fn draw_bottom_bar() void = { | ||
| 63 | const color_text = raylib::color { r = 200, g = 0, b = 0, a = 255 }; | ||
| 64 | const text_gap = 4; | ||
| 65 | let h = raylib::get_screen_height(); | ||
| 66 | let w = raylib::get_screen_width(); | ||
| 67 | let tl = raylib::vector2 { x = 0.0, y = (h - BOTTOM_BAR_THICKNESS) : f32 }; | ||
| 68 | let sz = raylib::vector2 { x = w: f32, y = h: f32 }; | ||
| 69 | |||
| 70 | raylib::draw_rectangle_v(tl, sz, COLOR_BACKGROUND); | ||
| 71 | raylib::draw_text(bottom_left_text, text_gap, | ||
| 72 | (h - BOTTOM_BAR_THICKNESS + text_gap), | ||
| 73 | (BOTTOM_BAR_THICKNESS - 2 * text_gap), color_text); | ||
| 74 | }; | ||
| 75 | |||
| 76 | fn foreach_neighbor( | ||
| 77 | b: *board, | ||
| 78 | ii: int, | ||
| 79 | f: *fn(b: *board, jj: int) void | ||
| 80 | ) void = { | ||
| 81 | let i = ii / b.width; | ||
| 82 | let j = ii % b.width; | ||
| 83 | |||
| 84 | const neighbors: [8](int, int) = [ | ||
| 85 | (i-1, j-1), (i-1, j), (i-1, j+1), | ||
| 86 | (i, j-1), (i, j+1), | ||
| 87 | (i+1, j-1), (i+1, j), (i+1, j+1) | ||
| 88 | ]; | ||
| 89 | for (let p .. neighbors) | ||
| 90 | if (p.0 >= 0 && p.0 < b.height && | ||
| 91 | p.1 >= 0 && p.1 < b.width) | ||
| 92 | f(b, p.0*b.width + p.1); | ||
| 93 | }; | ||
| 94 | |||
| 95 | fn inc_near_mines(b: *board, ii: int) void = b.cells[ii].near_mines += 1; | ||
| 96 | fn inc_near_flags(b: *board, ii: int) void = b.cells[ii].near_flags += 1; | ||
| 97 | fn dec_near_flags(b: *board, ii: int) void = b.cells[ii].near_flags -= 1; | ||
| 98 | |||
| 99 | fn new_board(w: int, h: int, n: int) *board = { | ||
| 100 | let empty_cell = cell { | ||
| 101 | is_mine = false, | ||
| 102 | is_hidden = true, | ||
| 103 | is_flag = false, | ||
| 104 | near_mines = 0, | ||
| 105 | near_flags = 0 | ||
| 106 | }; | ||
| 107 | let b: *board = alloc(board { | ||
| 108 | cells = alloc([empty_cell ...], (w*h): size)!, | ||
| 109 | last_clicked = void, | ||
| 110 | width = w, | ||
| 111 | height = h, | ||
| 112 | nmines = n, | ||
| 113 | nfreed = 0, | ||
| 114 | finished = false | ||
| 115 | })!; | ||
| 116 | |||
| 117 | const seed: u64 = time::now(time::clock::REALTIME).sec : u64; | ||
| 118 | const rand: math::random::random = math::random::init(seed); | ||
| 119 | for (let i = 0; i < n; i += 1) { | ||
| 120 | let j = math::random::u32n(&rand, (w*h-i): u32): int; | ||
| 121 | for (let k = 0; k <= j; k += 1) | ||
| 122 | if (b.cells[k].is_mine) | ||
| 123 | j += 1; | ||
| 124 | b.cells[j].is_mine = true; | ||
| 125 | foreach_neighbor(b, j, &inc_near_mines); | ||
| 126 | }; | ||
| 127 | return b; | ||
| 128 | }; | ||
| 129 | |||
| 130 | fn delete_board(b: *board) void = { | ||
| 131 | free(b.cells); | ||
| 132 | free(b); | ||
| 133 | }; | ||
| 134 | |||
| 135 | fn game_lost(b: *board) void = { | ||
| 136 | for (let c &.. b.cells) | ||
| 137 | c.is_hidden = false; | ||
| 138 | bottom_left_text = "You lost! Press n for a new game"; | ||
| 139 | b.finished = true; | ||
| 140 | }; | ||
| 141 | |||
| 142 | fn game_won(b: *board) void = { | ||
| 143 | for (let c &.. b.cells) | ||
| 144 | c.is_hidden = false; | ||
| 145 | bottom_left_text = "You won! Press n for a new game"; | ||
| 146 | b.finished = true; | ||
| 147 | }; | ||
| 148 | |||
| 149 | fn leftclick_if_hidden_noflag(b: *board, i: int) void = { | ||
| 150 | if (b.cells[i].is_hidden && ! b.cells[i].is_flag) | ||
| 151 | leftclick_cell(b, i); | ||
| 152 | }; | ||
| 153 | |||
| 154 | fn leftclick_cell(b: *board, i: int) void = { | ||
| 155 | let c = &(b.cells[i]); | ||
| 156 | if (c.is_hidden) { | ||
| 157 | if (c.is_flag) | ||
| 158 | return; | ||
| 159 | b.last_clicked = i; | ||
| 160 | c.is_hidden = false; | ||
| 161 | if (c.is_mine) { | ||
| 162 | game_lost(b); | ||
| 163 | } else { | ||
| 164 | b.nfreed += 1; | ||
| 165 | if (b.nfreed == len(b.cells): int - b.nmines) | ||
| 166 | game_won(b); | ||
| 167 | }; | ||
| 168 | if (c.near_mines == 0 && c.near_flags == 0) | ||
| 169 | foreach_neighbor(b, i, &leftclick_if_hidden_noflag); | ||
| 170 | } else { | ||
| 171 | if (c.near_flags == c.near_mines) | ||
| 172 | foreach_neighbor(b, i, &leftclick_if_hidden_noflag); | ||
| 173 | }; | ||
| 174 | }; | ||
| 175 | |||
| 176 | fn rightclick_cell(b: *board, i: int) void = { | ||
| 177 | let c = &(b.cells[i]); | ||
| 178 | if (c.is_hidden) { | ||
| 179 | c.is_flag = !c.is_flag; | ||
| 180 | const f = if (c.is_flag) &inc_near_flags else &dec_near_flags; | ||
| 181 | foreach_neighbor(b, i, f); | ||
| 182 | } else { | ||
| 183 | return; | ||
| 184 | }; | ||
| 185 | }; | ||
| 186 | |||
| 187 | // Return true if the user started a new game | ||
| 188 | fn handle_interaction(b: *board) bool = { | ||
| 189 | const p = raylib::get_mouse_position(); | ||
| 190 | if (raylib::is_mouse_button_pressed(raylib::MOUSE_BUTTON_LEFT)) { | ||
| 191 | match (get_cell_index(b, p)) { | ||
| 192 | case void => return false; | ||
| 193 | case let i: int => leftclick_cell(b, i); | ||
| 194 | }; | ||
| 195 | }; | ||
| 196 | if (raylib::is_mouse_button_pressed(raylib::MOUSE_BUTTON_RIGHT)) { | ||
| 197 | match (get_cell_index(b, p)) { | ||
| 198 | case void => return false; | ||
| 199 | case let i: int => rightclick_cell(b, i); | ||
| 200 | }; | ||
| 201 | }; | ||
| 202 | if (raylib::is_key_pressed(raylib::KEY_N) && b.finished) | ||
| 203 | return true; | ||
| 204 | return false; | ||
| 205 | }; | ||
| 206 | |||
| 207 | fn top_gap() int = { | ||
| 208 | let h = raylib::get_screen_height(); | ||
| 209 | return (h - BOTTOM_BAR_THICKNESS - BOARD_HEIGHT * CELL_SIZE) / 2; | ||
| 210 | }; | ||
| 211 | |||
| 212 | fn side_gap() int = { | ||
| 213 | let w = raylib::get_screen_width(); | ||
| 214 | return (w - BOARD_WIDTH * CELL_SIZE) / 2; | ||
| 215 | }; | ||
| 216 | |||
| 217 | fn cell_pos(b: *board, i: int) raylib::vector2 = { | ||
| 218 | return raylib::vector2 { | ||
| 219 | x = ((i % b.width) * CELL_SIZE + HALFGAP_SIZE + side_gap()) : f32, | ||
| 220 | y = ((i / b.width) * CELL_SIZE + HALFGAP_SIZE + top_gap()) : f32 | ||
| 221 | }; | ||
| 222 | }; | ||
| 223 | |||
| 224 | fn flag_pos(b: *board, i: int) raylib::vector2 = { | ||
| 225 | const shift = (CELL_SIZE - FLAG_SIZE) / 2; | ||
| 226 | return raylib::vector2 { | ||
| 227 | x = ((i % b.width) * CELL_SIZE + shift + side_gap()) : f32, | ||
| 228 | y = ((i / b.width) * CELL_SIZE + shift + top_gap()) : f32 | ||
| 229 | }; | ||
| 230 | }; | ||
| 231 | |||
| 232 | fn get_cell_index(b: *board, p: raylib::vector2) (int | void) = { | ||
| 233 | const i = (p.y: int - top_gap()) / CELL_SIZE; | ||
| 234 | const j: int = (p.x: int - side_gap()) / CELL_SIZE; | ||
| 235 | if (i < b.height && j < b.width) | ||
| 236 | return i * b.width + j; | ||
| 237 | }; | ||
| 238 | |||
| 239 | fn cell_drawsize() raylib::vector2 = raylib::vector2 { | ||
| 240 | x = (CELL_SIZE - 2*HALFGAP_SIZE) : f32, | ||
| 241 | y = (CELL_SIZE - 2*HALFGAP_SIZE) : f32 | ||
| 242 | }; | ||
| 243 | |||
| 244 | fn flag_drawsize() raylib::vector2 = raylib::vector2 { | ||
| 245 | x = FLAG_SIZE : f32, | ||
| 246 | y = FLAG_SIZE : f32 | ||
| 247 | }; | ||
| 248 | |||
| 249 | fn draw_flag(b: *board, i: int) void = | ||
| 250 | raylib::draw_rectangle_v(flag_pos(b, i), flag_drawsize(), COLOR_FLAG); | ||
| 251 | |||
| 252 | fn draw_cell_hidden(b: *board, i: int) void = { | ||
| 253 | raylib::draw_rectangle_v(cell_pos(b, i), cell_drawsize(), COLOR_HIDDEN); | ||
| 254 | if (b.cells[i].is_flag) | ||
| 255 | draw_flag(b, i); | ||
| 256 | }; | ||
| 257 | |||
| 258 | fn draw_cell_mine(b: *board, i: int) void = { | ||
| 259 | const c = match (b.last_clicked) { | ||
| 260 | case void => yield COLOR_MINE; | ||
| 261 | case let l: int => yield if (l == i) COLOR_CLICKED_MINE else COLOR_MINE; | ||
| 262 | }; | ||
| 263 | raylib::draw_rectangle_v(cell_pos(b, i), cell_drawsize(), c); | ||
| 264 | }; | ||
| 265 | |||
| 266 | fn draw_cell_free(b: *board, i: int) void = { | ||
| 267 | let pos = cell_pos(b, i); | ||
| 268 | let sz = cell_drawsize(); | ||
| 269 | raylib::draw_rectangle_v(pos, cell_drawsize(), COLOR_FREE); | ||
| 270 | |||
| 271 | const n = b.cells[i].near_mines: u8; | ||
| 272 | const s = strings::fromutf8([n + '0']) as str; | ||
| 273 | const w = raylib::measure_text(s, sz.y: int); | ||
| 274 | const shift_x = (CELL_SIZE: f32 / 2.0) - (w: f32 / 2.0) - (HALFGAP_SIZE: f32); | ||
| 275 | const shift_y = (CELL_SIZE: f32 / 2.0) - (sz.y / 2.0); | ||
| 276 | let text_x = (pos.x + shift_x): int; | ||
| 277 | let text_y = (pos.y + shift_y): int; | ||
| 278 | raylib::draw_text(s, text_x, text_y, sz.y: int, COLOR_NR[n]); | ||
| 279 | }; | ||
| 280 | |||
| 281 | fn draw_cell(b: *board, i: int) void = { | ||
| 282 | const c = b.cells[i]; | ||
| 283 | if (c.is_hidden) { | ||
| 284 | draw_cell_hidden(b, i); | ||
| 285 | } else if (c.is_mine) { | ||
| 286 | draw_cell_mine(b, i); | ||
| 287 | } else { | ||
| 288 | draw_cell_free(b, i); | ||
| 289 | }; | ||
| 290 | }; | ||
| 291 | |||
| 292 | fn draw_board(b: *board) void = { | ||
| 293 | for (let i = 0; i < b.height; i += 1) | ||
| 294 | for (let j = 0; j < b.width; j += 1) | ||
| 295 | draw_cell(b, i*b.width + j); | ||
| 296 | }; | ||
| 297 | |||
| 298 | export fn main() void = { | ||
| 299 | let game_board = new_board(BOARD_WIDTH, BOARD_HEIGHT, NMINES); | ||
| 300 | defer delete_board(game_board); | ||
| 301 | |||
| 302 | raylib::set_config_flags(raylib::FLAG_WINDOW_RESIZABLE); | ||
| 303 | raylib::init_window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE); | ||
| 304 | for (!raylib::window_should_close()) { | ||
| 305 | raylib::begin_drawing(); | ||
| 306 | |||
| 307 | raylib::clear_background(COLOR_BACKGROUND); | ||
| 308 | draw_board(game_board); | ||
| 309 | draw_bottom_bar(); | ||
| 310 | const restart = handle_interaction(game_board); | ||
| 311 | if (restart) { | ||
| 312 | delete_board(game_board); | ||
| 313 | game_board = new_board(BOARD_WIDTH, BOARD_HEIGHT, NMINES); | ||
| 314 | }; | ||
| 315 | |||
| 316 | raylib::end_drawing(); | ||
| 317 | }; | ||
| 318 | }; | ||
