├── .github └── workflows │ └── compile.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── curses.supp ├── screenshot.png └── src ├── argparser.zig ├── core.zig ├── curses.zig ├── cursesflags.h ├── cursesui.zig ├── help.zig └── main.zig /.github/workflows/compile.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v2 16 | 17 | # Please keep commands in sync with README 18 | 19 | - name: Download Zig 20 | run: wget https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz 21 | 22 | - name: Verify downloaded file 23 | run: echo "d45312e61ebcc48032b77bc4cf7fd6915c11fa16e4aad116b66c9468211230ea zig-linux-x86_64-0.13.0.tar.xz" | sha256sum --check 24 | 25 | - name: Extract Zig 26 | run: tar xf zig-linux-x86_64-0.13.0.tar.xz 27 | 28 | - name: Rename extracted Zig folder 29 | run: mv zig-linux-x86_64-0.13.0 zig 30 | 31 | - name: Build with Zig 32 | run: zig/zig build 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | valgrind.log 2 | .zig-cache/ 3 | zig-out/ 4 | 5 | # downloaded zig before and after extracting, see README 6 | zig-*-*-*.tar.xz 7 | zig/ 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2024 Akuli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # curses-minesweeper 2 | 3 | This is a minesweeper game written with [Zig](https://github.com/ziglang/zig) 4 | using curses. 5 | 6 | ![screenshot](screenshot.png) 7 | 8 | If you have apt, you can install most of the dependencies and download the code 9 | like this: 10 | 11 | $ sudo apt install git wget libncurses5-dev libncursesw5-dev gcc 12 | $ git clone https://github.com/Akuli/curses-minesweeper 13 | $ cd curses-minesweeper 14 | 15 | Then [download zig 0.13.0](https://ziglang.org/download/) and move it to 16 | the `curses-minesweeper` directory, and run this: 17 | 18 | $ tar xf zig-linux-SOMETHING.tar.xz (use autocompletion) 19 | $ mv zig-linux-SOMETHING zig (use autocompletion) 20 | 21 | Now you can compile and run the project. 22 | 23 | $ zig/zig build 24 | 25 | Run the game: 26 | 27 | $ zig-out/bin/curses-minesweeper 28 | 29 | Add `--help` for more options. 30 | 31 | 32 | ## FAQ 33 | 34 | ### I can't get it to work! 35 | 36 | Create an issue. I'll try to help. 37 | 38 | ### Why did you write a minesweeper game? 39 | 40 | Because it's fun. 41 | 42 | ### Does it work on Windows? 43 | 44 | No, but Windows comes with a minesweeper. Windows command prompt and powershell 45 | are kind of awful anyway, and you probably want to use GUI applications instead 46 | of them whenever possible. 47 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | // Standard target options allows the person running `zig build` to choose 5 | // what target to build for. Here we do not override the defaults, which 6 | // means any target is allowed, and the default is native. Other options 7 | // for restricting supported target set are available. 8 | const target = b.standardTargetOptions(.{}); 9 | 10 | // Standard optimization options allow the person running `zig build` to select 11 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not 12 | // set a preferred release mode, allowing the user to decide how to optimize. 13 | const optimize = b.standardOptimizeOption(.{}); 14 | 15 | const exe = b.addExecutable(.{ 16 | .name = "curses-minesweeper", 17 | .root_source_file = b.path("src/main.zig"), 18 | .target = target, 19 | .optimize = optimize, 20 | }); 21 | 22 | exe.linkLibC(); 23 | exe.linkSystemLibrary("ncursesw"); 24 | exe.addIncludePath(b.path(".")); 25 | 26 | // This declares intent for the executable to be installed into the 27 | // standard location when the user invokes the "install" step (the default 28 | // step when running `zig build`). 29 | b.installArtifact(exe); 30 | 31 | // This *creates* a Run step in the build graph, to be executed when another 32 | // step is evaluated that depends on it. The next line below will establish 33 | // such a dependency. 34 | const run_cmd = b.addRunArtifact(exe); 35 | 36 | // By making the run step depend on the install step, it will be run from the 37 | // installation directory rather than directly from within the cache directory. 38 | // This is not necessary, however, if the application depends on other installed 39 | // files, this ensures they will be present and in the expected location. 40 | run_cmd.step.dependOn(b.getInstallStep()); 41 | 42 | // This allows the user to pass arguments to the application in the build 43 | // command itself, like this: `zig build run -- arg1 arg2 etc` 44 | if (b.args) |args| { 45 | run_cmd.addArgs(args); 46 | } 47 | 48 | // This creates a build step. It will be visible in the `zig build --help` menu, 49 | // and can be selected like this: `zig build run` 50 | // This will evaluate the `run` step rather than the default, which is "install". 51 | const run_step = b.step("run", "Run the app"); 52 | run_step.dependOn(&run_cmd.step); 53 | } 54 | -------------------------------------------------------------------------------- /curses.supp: -------------------------------------------------------------------------------- 1 | { 2 | 3 | Memcheck:Leak 4 | match-leak-kinds: reachable 5 | fun:malloc 6 | fun:strdup 7 | fun:_nc_setupterm 8 | fun:newterm 9 | fun:initscr 10 | fun:curses.initscr 11 | fun:main.0 12 | fun:std.special.callMain 13 | fun:std.special.callMainWithArgs 14 | fun:main 15 | } 16 | { 17 | 18 | Memcheck:Leak 19 | match-leak-kinds: reachable 20 | fun:malloc 21 | fun:strdup 22 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 23 | fun:_nc_first_db 24 | fun:_nc_read_entry 25 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 26 | fun:_nc_setupterm 27 | fun:newterm 28 | fun:initscr 29 | fun:curses.initscr 30 | fun:main.0 31 | fun:std.special.callMain 32 | fun:std.special.callMainWithArgs 33 | fun:main 34 | } 35 | { 36 | 37 | Memcheck:Leak 38 | match-leak-kinds: reachable 39 | fun:malloc 40 | fun:_nc_tparm_analyze 41 | fun:tparm 42 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 43 | fun:newterm 44 | fun:initscr 45 | fun:curses.initscr 46 | fun:main.0 47 | fun:std.special.callMain 48 | fun:std.special.callMainWithArgs 49 | fun:main 50 | } 51 | { 52 | 53 | Memcheck:Leak 54 | match-leak-kinds: reachable 55 | fun:malloc 56 | fun:_nc_home_terminfo 57 | fun:_nc_first_db 58 | fun:_nc_read_entry 59 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 60 | fun:_nc_setupterm 61 | fun:newterm 62 | fun:initscr 63 | fun:curses.initscr 64 | fun:main.0 65 | fun:std.special.callMain 66 | fun:std.special.callMainWithArgs 67 | fun:main 68 | } 69 | { 70 | 71 | Memcheck:Leak 72 | match-leak-kinds: reachable 73 | fun:calloc 74 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 75 | fun:newterm 76 | fun:initscr 77 | fun:curses.initscr 78 | fun:main.0 79 | fun:std.special.callMain 80 | fun:std.special.callMainWithArgs 81 | fun:main 82 | } 83 | { 84 | 85 | Memcheck:Leak 86 | match-leak-kinds: reachable 87 | fun:realloc 88 | fun:_nc_doalloc 89 | fun:_nc_read_termtype 90 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 91 | fun:_nc_read_entry 92 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 93 | fun:_nc_setupterm 94 | fun:newterm 95 | fun:initscr 96 | fun:curses.initscr 97 | fun:main.0 98 | fun:std.special.callMain 99 | fun:std.special.callMainWithArgs 100 | fun:main 101 | } 102 | { 103 | 104 | Memcheck:Leak 105 | match-leak-kinds: reachable 106 | fun:calloc 107 | fun:_nc_first_db 108 | fun:_nc_read_entry 109 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 110 | fun:_nc_setupterm 111 | fun:newterm 112 | fun:initscr 113 | fun:curses.initscr 114 | fun:main.0 115 | fun:std.special.callMain 116 | fun:std.special.callMainWithArgs 117 | fun:main 118 | } 119 | { 120 | 121 | Memcheck:Leak 122 | match-leak-kinds: reachable 123 | fun:realloc 124 | fun:_nc_doalloc 125 | fun:tparm 126 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 127 | fun:newterm 128 | fun:initscr 129 | fun:curses.initscr 130 | fun:main.0 131 | fun:std.special.callMain 132 | fun:std.special.callMainWithArgs 133 | fun:main 134 | } 135 | { 136 | 137 | Memcheck:Leak 138 | match-leak-kinds: reachable 139 | fun:realloc 140 | fun:_nc_doalloc 141 | fun:_nc_read_termtype 142 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 143 | fun:_nc_read_entry 144 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 145 | fun:_nc_setupterm 146 | fun:newterm 147 | fun:initscr 148 | fun:curses.initscr 149 | fun:main.0 150 | fun:std.special.callMain 151 | fun:std.special.callMainWithArgs 152 | fun:main 153 | } 154 | { 155 | 156 | Memcheck:Leak 157 | match-leak-kinds: reachable 158 | fun:malloc 159 | fun:_nc_first_db 160 | fun:_nc_read_entry 161 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 162 | fun:_nc_setupterm 163 | fun:newterm 164 | fun:initscr 165 | fun:curses.initscr 166 | fun:main.0 167 | fun:std.special.callMain 168 | fun:std.special.callMainWithArgs 169 | fun:main 170 | } 171 | { 172 | 173 | Memcheck:Leak 174 | match-leak-kinds: reachable 175 | fun:malloc 176 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 177 | fun:doupdate 178 | fun:wrefresh 179 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 180 | fun:wechochar 181 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 182 | fun:wgetch 183 | fun:curses.getch 184 | fun:main.0 185 | fun:std.special.callMain 186 | fun:std.special.callMainWithArgs 187 | fun:main 188 | } 189 | { 190 | 191 | Memcheck:Leak 192 | match-leak-kinds: reachable 193 | fun:calloc 194 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 195 | fun:newterm 196 | fun:initscr 197 | fun:curses.initscr 198 | fun:main.0 199 | fun:std.special.callMain 200 | fun:std.special.callMainWithArgs 201 | fun:main 202 | } 203 | { 204 | 205 | Memcheck:Leak 206 | match-leak-kinds: reachable 207 | fun:calloc 208 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 209 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 210 | fun:doupdate 211 | fun:wrefresh 212 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 213 | fun:wechochar 214 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 215 | fun:wgetch 216 | fun:curses.getch 217 | fun:main.0 218 | fun:std.special.callMain 219 | fun:std.special.callMainWithArgs 220 | fun:main 221 | } 222 | { 223 | 224 | Memcheck:Leak 225 | match-leak-kinds: reachable 226 | fun:calloc 227 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 228 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 229 | fun:doupdate 230 | fun:wrefresh 231 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 232 | fun:wechochar 233 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 234 | fun:wgetch 235 | fun:curses.getch 236 | fun:main.0 237 | fun:std.special.callMain 238 | fun:std.special.callMainWithArgs 239 | fun:main 240 | } 241 | { 242 | 243 | Memcheck:Leak 244 | match-leak-kinds: reachable 245 | fun:calloc 246 | fun:_nc_setupterm 247 | fun:newterm 248 | fun:initscr 249 | fun:curses.initscr 250 | fun:main.0 251 | fun:std.special.callMain 252 | fun:std.special.callMainWithArgs 253 | fun:main 254 | } 255 | { 256 | 257 | Memcheck:Leak 258 | match-leak-kinds: reachable 259 | fun:calloc 260 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 261 | fun:newwin 262 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 263 | fun:newterm 264 | fun:initscr 265 | fun:curses.initscr 266 | fun:main.0 267 | fun:std.special.callMain 268 | fun:std.special.callMainWithArgs 269 | fun:main 270 | } 271 | { 272 | 273 | Memcheck:Leak 274 | match-leak-kinds: reachable 275 | fun:calloc 276 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 277 | fun:newwin 278 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 279 | fun:newterm 280 | fun:initscr 281 | fun:curses.initscr 282 | fun:main.0 283 | fun:std.special.callMain 284 | fun:std.special.callMainWithArgs 285 | fun:main 286 | } 287 | { 288 | 289 | Memcheck:Leak 290 | match-leak-kinds: reachable 291 | fun:calloc 292 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 293 | fun:newwin 294 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 295 | fun:newterm 296 | fun:initscr 297 | fun:curses.initscr 298 | fun:main.0 299 | fun:std.special.callMain 300 | fun:std.special.callMainWithArgs 301 | fun:main 302 | } 303 | { 304 | 305 | Memcheck:Leak 306 | match-leak-kinds: reachable 307 | fun:calloc 308 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 309 | fun:newwin 310 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 311 | fun:newterm 312 | fun:initscr 313 | fun:curses.initscr 314 | fun:main.0 315 | fun:std.special.callMain 316 | fun:std.special.callMainWithArgs 317 | fun:main 318 | } 319 | { 320 | 321 | Memcheck:Leak 322 | match-leak-kinds: reachable 323 | fun:calloc 324 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 325 | fun:newwin 326 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 327 | fun:newterm 328 | fun:initscr 329 | fun:curses.initscr 330 | fun:main.0 331 | fun:std.special.callMain 332 | fun:std.special.callMainWithArgs 333 | fun:main 334 | } 335 | { 336 | 337 | Memcheck:Leak 338 | match-leak-kinds: reachable 339 | fun:calloc 340 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 341 | fun:newwin 342 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 343 | fun:newterm 344 | fun:initscr 345 | fun:curses.initscr 346 | fun:main.0 347 | fun:std.special.callMain 348 | fun:std.special.callMainWithArgs 349 | fun:main 350 | } 351 | { 352 | 353 | Memcheck:Leak 354 | match-leak-kinds: reachable 355 | fun:calloc 356 | fun:_nc_read_termtype 357 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 358 | fun:_nc_read_entry 359 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 360 | fun:_nc_setupterm 361 | fun:newterm 362 | fun:initscr 363 | fun:curses.initscr 364 | fun:main.0 365 | fun:std.special.callMain 366 | fun:std.special.callMainWithArgs 367 | fun:main 368 | } 369 | { 370 | 371 | Memcheck:Leak 372 | match-leak-kinds: reachable 373 | fun:malloc 374 | fun:_nc_read_termtype 375 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 376 | fun:_nc_read_entry 377 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 378 | fun:_nc_setupterm 379 | fun:newterm 380 | fun:initscr 381 | fun:curses.initscr 382 | fun:main.0 383 | fun:std.special.callMain 384 | fun:std.special.callMainWithArgs 385 | fun:main 386 | } 387 | { 388 | 389 | Memcheck:Leak 390 | match-leak-kinds: reachable 391 | fun:calloc 392 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 393 | fun:newterm 394 | fun:initscr 395 | fun:curses.initscr 396 | fun:main.0 397 | fun:std.special.callMain 398 | fun:std.special.callMainWithArgs 399 | fun:main 400 | } 401 | { 402 | 403 | Memcheck:Leak 404 | match-leak-kinds: reachable 405 | fun:malloc 406 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 407 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 408 | fun:doupdate 409 | fun:wrefresh 410 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 411 | fun:wechochar 412 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 413 | fun:wgetch 414 | fun:curses.getch 415 | fun:main.0 416 | fun:std.special.callMain 417 | fun:std.special.callMainWithArgs 418 | fun:main 419 | } 420 | { 421 | 422 | Memcheck:Leak 423 | match-leak-kinds: reachable 424 | fun:malloc 425 | fun:_nc_read_termtype 426 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 427 | fun:_nc_read_entry 428 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 429 | fun:_nc_setupterm 430 | fun:newterm 431 | fun:initscr 432 | fun:curses.initscr 433 | fun:main.0 434 | fun:std.special.callMain 435 | fun:std.special.callMainWithArgs 436 | fun:main 437 | } 438 | { 439 | 440 | Memcheck:Leak 441 | match-leak-kinds: reachable 442 | fun:calloc 443 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 444 | fun:newterm 445 | fun:initscr 446 | fun:curses.initscr 447 | fun:main.0 448 | fun:std.special.callMain 449 | fun:std.special.callMainWithArgs 450 | fun:main 451 | } 452 | { 453 | 454 | Memcheck:Leak 455 | match-leak-kinds: reachable 456 | fun:malloc 457 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 458 | fun:newterm 459 | fun:initscr 460 | fun:curses.initscr 461 | fun:main.0 462 | fun:std.special.callMain 463 | fun:std.special.callMainWithArgs 464 | fun:main 465 | } 466 | { 467 | 468 | Memcheck:Leak 469 | match-leak-kinds: reachable 470 | fun:realloc 471 | fun:_nc_doalloc 472 | fun:_nc_read_termtype 473 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 474 | fun:_nc_read_entry 475 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 476 | fun:_nc_setupterm 477 | fun:newterm 478 | fun:initscr 479 | fun:curses.initscr 480 | fun:main.0 481 | fun:std.special.callMain 482 | fun:std.special.callMainWithArgs 483 | fun:main 484 | } 485 | { 486 | 487 | Memcheck:Leak 488 | match-leak-kinds: reachable 489 | fun:calloc 490 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 491 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 492 | fun:newterm 493 | fun:initscr 494 | fun:curses.initscr 495 | fun:main.0 496 | fun:std.special.callMain 497 | fun:std.special.callMainWithArgs 498 | fun:main 499 | } 500 | { 501 | 502 | Memcheck:Leak 503 | match-leak-kinds: reachable 504 | fun:calloc 505 | fun:newwin 506 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 507 | fun:newterm 508 | fun:initscr 509 | fun:curses.initscr 510 | fun:main.0 511 | fun:std.special.callMain 512 | fun:std.special.callMainWithArgs 513 | fun:main 514 | } 515 | { 516 | 517 | Memcheck:Leak 518 | match-leak-kinds: reachable 519 | fun:calloc 520 | fun:newwin 521 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 522 | fun:newterm 523 | fun:initscr 524 | fun:curses.initscr 525 | fun:main.0 526 | fun:std.special.callMain 527 | fun:std.special.callMainWithArgs 528 | fun:main 529 | } 530 | { 531 | 532 | Memcheck:Leak 533 | match-leak-kinds: reachable 534 | fun:calloc 535 | fun:newwin 536 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 537 | fun:newterm 538 | fun:initscr 539 | fun:curses.initscr 540 | fun:main.0 541 | fun:std.special.callMain 542 | fun:std.special.callMainWithArgs 543 | fun:main 544 | } 545 | { 546 | 547 | Memcheck:Leak 548 | match-leak-kinds: reachable 549 | fun:calloc 550 | fun:_nc_add_to_try 551 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 552 | fun:_nc_keypad 553 | fun:curses.Window.keypad 554 | fun:main.0 555 | fun:std.special.callMain 556 | fun:std.special.callMainWithArgs 557 | fun:main 558 | } 559 | { 560 | 561 | Memcheck:Leak 562 | match-leak-kinds: reachable 563 | fun:malloc 564 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 565 | fun:doupdate 566 | fun:wrefresh 567 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 568 | fun:wgetch 569 | fun:curses.getch 570 | fun:main.0 571 | fun:std.special.callMain 572 | fun:std.special.callMainWithArgs 573 | fun:main 574 | } 575 | { 576 | 577 | Memcheck:Leak 578 | match-leak-kinds: reachable 579 | fun:realloc 580 | fun:_nc_doalloc 581 | fun:_nc_tparm_analyze 582 | fun:tparm 583 | fun:vidputs 584 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 585 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 586 | fun:doupdate 587 | fun:wrefresh 588 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 589 | fun:wgetch 590 | fun:curses.getch 591 | } 592 | { 593 | 594 | Memcheck:Leak 595 | match-leak-kinds: reachable 596 | fun:calloc 597 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 598 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 599 | fun:doupdate 600 | fun:wrefresh 601 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 602 | fun:wgetch 603 | fun:curses.getch 604 | fun:main.0 605 | fun:std.special.callMain 606 | fun:std.special.callMainWithArgs 607 | fun:main 608 | } 609 | { 610 | 611 | Memcheck:Leak 612 | match-leak-kinds: reachable 613 | fun:calloc 614 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 615 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 616 | fun:doupdate 617 | fun:wrefresh 618 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 619 | fun:wgetch 620 | fun:curses.getch 621 | fun:main.0 622 | fun:std.special.callMain 623 | fun:std.special.callMainWithArgs 624 | fun:main 625 | } 626 | { 627 | 628 | Memcheck:Leak 629 | match-leak-kinds: reachable 630 | fun:calloc 631 | fun:_nc_add_to_try 632 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 633 | fun:_nc_keypad 634 | fun:curses.Window.keypad 635 | fun:main.0 636 | fun:std.special.callMain 637 | fun:std.special.callMainWithArgs 638 | fun:main 639 | } 640 | { 641 | 642 | Memcheck:Leak 643 | match-leak-kinds: reachable 644 | fun:malloc 645 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 646 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 647 | fun:doupdate 648 | fun:wrefresh 649 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 650 | fun:wgetch 651 | fun:curses.getch 652 | fun:main.0 653 | fun:std.special.callMain 654 | fun:std.special.callMainWithArgs 655 | fun:main 656 | } 657 | { 658 | 659 | Memcheck:Leak 660 | match-leak-kinds: reachable 661 | fun:calloc 662 | fun:_nc_add_to_try 663 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 664 | fun:_nc_keypad 665 | fun:curses.Window.keypad 666 | fun:main.0 667 | fun:std.special.callMain 668 | fun:std.special.callMainWithArgs 669 | fun:main 670 | } 671 | { 672 | 673 | Memcheck:Leak 674 | match-leak-kinds: reachable 675 | fun:calloc 676 | fun:_nc_add_to_try 677 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 678 | fun:_nc_keypad 679 | fun:curses.Window.keypad 680 | fun:main.0 681 | fun:std.special.callMain 682 | fun:std.special.callMainWithArgs 683 | fun:main 684 | } 685 | { 686 | 687 | Memcheck:Leak 688 | match-leak-kinds: reachable 689 | fun:calloc 690 | fun:_nc_add_to_try 691 | obj:/lib/x86_64-linux-gnu/libtinfo.so.5.9 692 | fun:_nc_keypad 693 | fun:curses.Window.keypad 694 | fun:main.0 695 | fun:std.special.callMain 696 | fun:std.special.callMainWithArgs 697 | fun:main 698 | } 699 | { 700 | 701 | Memcheck:Leak 702 | match-leak-kinds: reachable 703 | fun:malloc 704 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 705 | fun:doupdate 706 | fun:wrefresh 707 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 708 | fun:wechochar 709 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 710 | fun:wgetch 711 | fun:curses.Window.getch 712 | fun:main.0 713 | fun:std.special.callMain 714 | fun:std.special.callMainWithArgs 715 | fun:main 716 | } 717 | { 718 | 719 | Memcheck:Leak 720 | match-leak-kinds: reachable 721 | fun:realloc 722 | fun:_nc_doalloc 723 | fun:_nc_tparm_analyze 724 | fun:tparm 725 | fun:vidputs 726 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 727 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 728 | fun:doupdate 729 | fun:wrefresh 730 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 731 | fun:wgetch 732 | fun:curses.Window.getch 733 | } 734 | { 735 | 736 | Memcheck:Leak 737 | match-leak-kinds: reachable 738 | fun:calloc 739 | fun:start_color 740 | fun:curses.start_color 741 | fun:cursesui.Ui.setupColors 742 | fun:cursesui.Ui.init 743 | fun:main.0 744 | fun:std.special.callMain 745 | fun:std.special.callMainWithArgs 746 | fun:main 747 | } 748 | { 749 | 750 | Memcheck:Leak 751 | match-leak-kinds: reachable 752 | fun:calloc 753 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 754 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 755 | fun:doupdate 756 | fun:wrefresh 757 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 758 | fun:wechochar 759 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 760 | fun:wgetch 761 | fun:curses.Window.getch 762 | fun:main.0 763 | fun:std.special.callMain 764 | fun:std.special.callMainWithArgs 765 | fun:main 766 | } 767 | { 768 | 769 | Memcheck:Leak 770 | match-leak-kinds: reachable 771 | fun:calloc 772 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 773 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 774 | fun:doupdate 775 | fun:wrefresh 776 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 777 | fun:wechochar 778 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 779 | fun:wgetch 780 | fun:curses.Window.getch 781 | fun:main.0 782 | fun:std.special.callMain 783 | fun:std.special.callMainWithArgs 784 | fun:main 785 | } 786 | { 787 | 788 | Memcheck:Leak 789 | match-leak-kinds: reachable 790 | fun:calloc 791 | fun:start_color 792 | fun:curses.start_color 793 | fun:cursesui.Ui.setupColors 794 | fun:cursesui.Ui.init 795 | fun:main.0 796 | fun:std.special.callMain 797 | fun:std.special.callMainWithArgs 798 | fun:main 799 | } 800 | { 801 | 802 | Memcheck:Leak 803 | match-leak-kinds: reachable 804 | fun:malloc 805 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 806 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 807 | fun:doupdate 808 | fun:wrefresh 809 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 810 | fun:wechochar 811 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 812 | fun:wgetch 813 | fun:curses.Window.getch 814 | fun:main.0 815 | fun:std.special.callMain 816 | fun:std.special.callMainWithArgs 817 | fun:main 818 | } 819 | { 820 | 821 | Memcheck:Leak 822 | match-leak-kinds: reachable 823 | fun:calloc 824 | fun:wresize 825 | fun:resize_term 826 | fun:resizeterm 827 | fun:_nc_update_screensize 828 | obj:/lib/x86_64-linux-gnu/libncursesw.so.5.9 829 | fun:wgetch 830 | fun:curses.Window.getch 831 | fun:help.show 832 | fun:main.0 833 | fun:std.special.callMain 834 | fun:std.special.callMainWithArgs 835 | fun:main 836 | } 837 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akuli/curses-minesweeper/8b5b69a3ab0398342b2a4c6279d3b5d631f722f6/screenshot.png -------------------------------------------------------------------------------- /src/argparser.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const clap = @import("zig-clap"); 3 | const cursesui = @import("cursesui.zig"); 4 | 5 | 6 | pub const Args = struct { 7 | width: u8, 8 | height: u8, 9 | nmines: u16, 10 | characters: cursesui.Characters, 11 | color: bool, 12 | }; 13 | 14 | fn parseSize(str: []const u8, width: *u8, height: *u8) !void { 15 | const i = std.mem.indexOfScalar(u8, str, 'x') orelse return error.CantFindTheX; 16 | width.* = try std.fmt.parseUnsigned(u8, str[0..i], 10); 17 | height.* = try std.fmt.parseUnsigned(u8, str[i+1..], 10); 18 | if (width.* == 0 or height.* == 0) { 19 | return error.MustNotBeZero; 20 | } 21 | } 22 | 23 | pub fn parse(allocator: std.mem.Allocator) !Args { 24 | const args = try std.process.argsAlloc(allocator); 25 | defer std.process.argsFree(allocator, args); 26 | 27 | var result = Args{ 28 | .width = 10, 29 | .height = 10, 30 | .nmines = 10, 31 | .characters = cursesui.unicode_characters, 32 | .color = true, 33 | }; 34 | 35 | var i: usize = 1; 36 | while (i < args.len) : (i += 1) { 37 | const arg = args[i]; 38 | if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) { 39 | std.debug.print("Usage: {s} [options]\n\nOptions:\n", .{args[0]}); 40 | std.debug.print(" -h, --help Display this help and exit\n", .{}); 41 | std.debug.print(" -s, --size How big to make minesweeper, e.g. 15x15\n", .{}); 42 | std.debug.print(" -n, --mine-count How many mines\n", .{}); 43 | std.debug.print(" -a, --ascii-only Use ASCII characters only\n", .{}); 44 | std.debug.print(" -c, --no-colors Do not use colors\n", .{}); 45 | std.process.exit(0); 46 | } else if (std.mem.eql(u8, arg, "-s") or std.mem.eql(u8, arg, "--size")) { 47 | i += 1; 48 | if (i >= args.len) { 49 | std.debug.print("Error: Missing value for size\n", .{}); 50 | std.process.exit(2); 51 | } 52 | const size = args[i]; 53 | parseSize(size, &result.width, &result.height) catch { 54 | std.debug.print("Error: Invalid minesweeper size \"{s}\"\n", .{size}); 55 | std.process.exit(2); 56 | }; 57 | } else if (std.mem.eql(u8, arg, "-n") or std.mem.eql(u8, arg, "--mine-count")) { 58 | i += 1; 59 | if (i >= args.len) { 60 | std.debug.print("Error: Missing value for mine count\n", .{}); 61 | std.process.exit(2); 62 | } 63 | const mineCount = args[i]; 64 | result.nmines = std.fmt.parseUnsigned(u16, mineCount, 10) catch { 65 | std.debug.print("Error: Invalid number of mines\n", .{}); 66 | std.process.exit(2); 67 | }; 68 | } else if (std.mem.eql(u8, arg, "-a") or std.mem.eql(u8, arg, "--ascii-only")) { 69 | result.characters = cursesui.ascii_characters; 70 | } else if (std.mem.eql(u8, arg, "-c") or std.mem.eql(u8, arg, "--no-colors")) { 71 | result.color = false; 72 | } else { 73 | std.debug.print("Error: Unknown argument \"{s}\"\n", .{arg}); 74 | std.process.exit(2); 75 | } 76 | } 77 | 78 | if (result.nmines >= @as(u16, result.width) * @as(u16, result.height)) { 79 | std.debug.print("Error: there must be less mines than places for mines\n", .{}); 80 | std.process.exit(2); 81 | } 82 | 83 | return result; 84 | } 85 | -------------------------------------------------------------------------------- /src/core.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | 4 | const Square = struct { 5 | opened: bool, 6 | mine: bool, 7 | flagged: bool, 8 | }; 9 | 10 | pub const SquareInfo = struct { 11 | opened: bool, 12 | mine: bool, 13 | flagged: bool, 14 | n_mines_around: u8, 15 | }; 16 | 17 | pub const GameStatus = enum { PLAY, WIN, LOSE }; 18 | 19 | pub const Game = struct { 20 | allocator: std.mem.Allocator, 21 | map: [][]Square, 22 | width: u8, 23 | height: u8, 24 | nmines: u16, 25 | status: GameStatus, 26 | mines_added: bool, 27 | rnd: std.Random, 28 | 29 | pub fn init(allocator: std.mem.Allocator, width: u8, height: u8, nmines: u16, rnd: std.Random) !Game { 30 | // in the beginning, there are width*height squares, but the mines are 31 | // added when the user has already opened one of them, otherwise the 32 | // first square that the user opens could be a mine 33 | std.debug.assert(@as(u16, width)*@as(u16, height) - 1 >= nmines); 34 | 35 | const map = try allocator.alloc([]Square, height); 36 | var nalloced: u8 = 0; 37 | errdefer { 38 | var i: u8 = 0; 39 | while (i < nalloced) : (i += 1) { 40 | allocator.free(map[i]); 41 | } 42 | allocator.free(map); 43 | } 44 | 45 | for (map) |*row| { 46 | row.* = try allocator.alloc(Square, width); 47 | nalloced += 1; 48 | for (row.*) |*square| { 49 | square.* = Square{ .opened = false, .mine = false, .flagged = false }; 50 | } 51 | } 52 | 53 | return Game{ 54 | .allocator = allocator, 55 | .map = map, 56 | .width = width, 57 | .height = height, 58 | .nmines = nmines, 59 | .status = GameStatus.PLAY, 60 | .mines_added = false, 61 | .rnd = rnd, 62 | }; 63 | } 64 | 65 | pub fn deinit(self: Game) void { 66 | for (self.map) |arr| { 67 | self.allocator.free(arr); 68 | } 69 | self.allocator.free(self.map); 70 | } 71 | 72 | const NeighborArray = [8][2]u8; 73 | 74 | fn getNeighbors(self: Game, x: u8, y: u8, res: [][2]u8) [][2]u8 { 75 | const neighbors = [_][2]i16{ 76 | [2]i16{@as(i16, x)-1, @as(i16, y)-1}, 77 | [2]i16{@as(i16, x)-1, @as(i16, y) }, 78 | [2]i16{@as(i16, x)-1, @as(i16, y)+1}, 79 | [2]i16{@as(i16, x), @as(i16, y)+1}, 80 | [2]i16{@as(i16, x)+1, @as(i16, y)+1}, 81 | [2]i16{@as(i16, x)+1, @as(i16, y) }, 82 | [2]i16{@as(i16, x)+1, @as(i16, y)-1}, 83 | [2]i16{@as(i16, x), @as(i16, y)-1}, 84 | }; 85 | var i: u8 = 0; 86 | 87 | for (neighbors) |neighbor| { 88 | const nx_signed = neighbor[0]; 89 | const ny_signed = neighbor[1]; 90 | if (0 <= nx_signed and nx_signed < @as(i16, self.width) and 0 <= ny_signed and ny_signed < @as(i16, self.height)) { 91 | res[i] = [2]u8{ @intCast(nx_signed), @intCast(ny_signed) }; 92 | i += 1; 93 | } 94 | } 95 | return res[0..i]; 96 | } 97 | 98 | fn countNeighborMines(self: Game, x: u8, y: u8) u8 { 99 | var neighbors: NeighborArray = undefined; 100 | var res: u8 = 0; 101 | for (self.getNeighbors(x, y, neighbors[0..])) |neighbor| { 102 | const nx = neighbor[0]; 103 | const ny = neighbor[1]; 104 | if (self.map[ny][nx].mine) { 105 | res += 1; 106 | } 107 | } 108 | return res; 109 | } 110 | 111 | pub fn getSquareInfo(self: Game, x: u8, y: u8) SquareInfo { 112 | return SquareInfo{ 113 | .opened = self.map[y][x].opened, 114 | .mine = self.map[y][x].mine, 115 | .flagged = self.map[y][x].flagged, 116 | .n_mines_around = self.countNeighborMines(x, y), 117 | }; 118 | } 119 | 120 | pub fn debugDump(self: Game) void { 121 | // M = showing mine, m = hidden mine, S = showing safe, s = hidden safe 122 | var y: u8 = 0; 123 | while (y < self.height) : (y += 1) { 124 | std.debug.warn("|"); 125 | var x: u8 = 0; 126 | while (x < self.width) : (x += 1) { 127 | if (self.map[y][x].opened) { 128 | if (self.map[y][x].mine) { 129 | std.debug.warn("M"); 130 | } else { 131 | std.debug.warn("S"); 132 | } 133 | } else { 134 | if (self.map[y][x].mine) { 135 | std.debug.warn("m"); 136 | } else { 137 | std.debug.warn("s"); 138 | } 139 | } 140 | 141 | if (self.map[y][x].mine) { 142 | std.debug.warn(" "); 143 | } else { 144 | std.debug.warn("{}", self.countNeighborMines(x, y)); 145 | } 146 | std.debug.warn("|"); 147 | } 148 | std.debug.warn("\n"); 149 | } 150 | std.debug.warn("status = {}\n", self.status); 151 | } 152 | 153 | fn openRecurser(self: *Game, x: u8, y: u8) void { 154 | if (self.map[y][x].opened) { 155 | return; 156 | } 157 | 158 | self.map[y][x].opened = true; 159 | if (!self.mines_added) { 160 | self.addMines(); 161 | self.mines_added = true; 162 | } 163 | 164 | if (self.map[y][x].mine) { 165 | self.status = GameStatus.LOSE; 166 | } else if (self.countNeighborMines(x, y) == 0) { 167 | var neighbors: NeighborArray = undefined; 168 | for (self.getNeighbors(x, y, neighbors[0..])) |neighbor| { 169 | const nx = neighbor[0]; 170 | const ny = neighbor[1]; 171 | self.openRecurser(nx, ny); 172 | } 173 | } 174 | } 175 | 176 | pub fn open(self: *Game, x: u8, y: u8) void { 177 | if (self.status != GameStatus.PLAY or self.map[y][x].flagged) { 178 | return; 179 | } 180 | 181 | self.openRecurser(x, y); 182 | switch (self.status) { 183 | GameStatus.PLAY => {}, 184 | GameStatus.LOSE => return, 185 | GameStatus.WIN => unreachable, // openRecurser shouldn't set this status 186 | } 187 | 188 | // try to find a non-mine, non-opened square 189 | // player won if there are none 190 | var xx: u8 = 0; 191 | while (xx < self.width) : (xx += 1) { 192 | var yy: u8 = 0; 193 | while (yy < self.height) : (yy += 1) { 194 | if (!self.map[yy][xx].opened and !self.map[yy][xx].mine) { 195 | return; 196 | } 197 | } 198 | } 199 | self.status = GameStatus.WIN; 200 | } 201 | 202 | fn addMines(self: *Game) void { 203 | var mined: u16 = 0; 204 | while (mined < self.nmines) { 205 | const x = self.rnd.uintLessThan(u8, self.width); 206 | const y = self.rnd.uintLessThan(u8, self.height); 207 | const square = &self.map[y][x]; 208 | if (!square.opened and !square.mine) { 209 | square.mine = true; 210 | std.debug.assert(self.map[y][x].mine); 211 | mined += 1; 212 | } 213 | } 214 | } 215 | 216 | pub fn openAroundIfSafe(self: *Game, x: u8, y: u8) void { 217 | if (self.status != GameStatus.PLAY or !self.map[y][x].opened) { 218 | return; 219 | } 220 | 221 | var arr: NeighborArray = undefined; 222 | var neighbor_mines: u8 = 0; 223 | var neighbor_flags: u8 = 0; 224 | for (self.getNeighbors(x, y, arr[0..])) |neighbor| { 225 | const nx = neighbor[0]; 226 | const ny = neighbor[1]; 227 | if (self.map[ny][nx].mine) { 228 | neighbor_mines += 1; 229 | } 230 | if (self.map[ny][nx].flagged) { 231 | neighbor_flags += 1; 232 | } 233 | } 234 | if (neighbor_mines != neighbor_flags) { 235 | return; 236 | } 237 | 238 | for (self.getNeighbors(x, y, arr[0..])) |neighbor| { 239 | const nx = neighbor[0]; 240 | const ny = neighbor[1]; 241 | self.open(nx, ny); 242 | } 243 | } 244 | 245 | pub fn openAroundEverythingSafe(self: *Game) void { 246 | var x: u8 = 0; 247 | while (x < self.width) : (x += 1) { 248 | var y: u8 = 0; 249 | while (y < self.height) : (y += 1) { 250 | self.openAroundIfSafe(x, y); 251 | } 252 | } 253 | } 254 | 255 | pub fn toggleFlag(self: *Game, x: u8, y: u8) void { 256 | if (self.status != GameStatus.PLAY or self.map[y][x].opened) { 257 | return; 258 | } 259 | self.map[y][x].flagged = !self.map[y][x].flagged; 260 | } 261 | }; 262 | -------------------------------------------------------------------------------- /src/curses.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @cImport({ 3 | @cInclude("src/cursesflags.h"); 4 | @cInclude("curses.h"); 5 | }); 6 | 7 | 8 | const Error = error.CursesError; 9 | 10 | fn checkError(res: c_int) !c_int { 11 | if (res == c.ERR) { 12 | return Error; 13 | } 14 | return res; 15 | } 16 | 17 | pub const KEY_RESIZE: c_int = c.KEY_RESIZE; 18 | pub const KEY_LEFT: c_int = c.KEY_LEFT; 19 | pub const KEY_RIGHT: c_int = c.KEY_RIGHT; 20 | pub const KEY_UP: c_int = c.KEY_UP; 21 | pub const KEY_DOWN: c_int = c.KEY_DOWN; 22 | 23 | 24 | pub const Window = struct { 25 | win: *c.WINDOW, 26 | allocator: std.mem.Allocator, 27 | 28 | // TODO: change more things to Window methods 29 | 30 | pub fn erase(self: Window) !void { 31 | _ = try checkError(c.werase(self.win)); 32 | } 33 | 34 | pub fn mvaddstr(self: Window, y: u16, x: u16, str: []const u8) !void { 35 | const cstr: []u8 = try self.allocator.alloc(u8, str.len + 1); 36 | defer self.allocator.free(cstr); 37 | std.mem.copyForwards(u8, cstr, str); 38 | cstr[str.len] = 0; 39 | _ = try checkError(c.mvwaddstr(self.win, y, x, cstr.ptr)); 40 | } 41 | 42 | // TODO: don't use "legacy" functions like getmaxy()? 43 | pub fn getmaxy(self: Window) u16 { return @intCast(c.getmaxy(self.win)); } 44 | pub fn getmaxx(self: Window) u16 { return @intCast(c.getmaxx(self.win)); } 45 | 46 | pub fn attron(self: Window, attr: c_int) !void { _ = try checkError(c.wattron(self.win, attr)); } 47 | pub fn attroff(self: Window, attr: c_int) !void { _ = try checkError(c.wattroff(self.win, attr)); } 48 | 49 | pub fn keypad(self: Window, bf: bool) !c_int { return checkError(c.keypad(self.win, bf)); } 50 | 51 | pub fn getch(self: Window) !c_int { return checkError(c.wgetch(self.win)); } 52 | }; 53 | 54 | pub const A_STANDOUT = c.MY_A_STANDOUT; 55 | 56 | 57 | pub fn initscr(allocator: std.mem.Allocator) !Window { 58 | const res = c.initscr() orelse return Error; 59 | return Window{ .win = res, .allocator = allocator }; 60 | } 61 | 62 | pub fn endwin() !void { 63 | _ = try checkError(c.endwin()); 64 | } 65 | 66 | pub fn curs_set(visibility: c_int) !c_int { 67 | return try checkError(c.curs_set(visibility)); 68 | } 69 | 70 | 71 | pub fn has_colors() bool { 72 | return c.has_colors(); 73 | } 74 | 75 | pub fn start_color() !void { 76 | _ = try checkError(c.start_color()); 77 | } 78 | 79 | const color_pair_attrs = [_]c_int{ 80 | -1, // 0 doesn't seem to be a valid color pair number, curses returns ERR for it 81 | c.MY_COLOR_PAIR_1, 82 | c.MY_COLOR_PAIR_2, 83 | c.MY_COLOR_PAIR_3, 84 | c.MY_COLOR_PAIR_4, 85 | c.MY_COLOR_PAIR_5, 86 | c.MY_COLOR_PAIR_6, 87 | c.MY_COLOR_PAIR_7, 88 | c.MY_COLOR_PAIR_8, 89 | c.MY_COLOR_PAIR_9, 90 | c.MY_COLOR_PAIR_10, 91 | }; 92 | 93 | pub const ColorPair = struct { 94 | id: c_short, 95 | 96 | pub fn init(id: c_short, front: c_short, back: c_short) !ColorPair { 97 | std.debug.assert(1 <= id and id < @as(c_short, color_pair_attrs.len)); 98 | _ = try checkError(c.init_pair(id, front, back)); 99 | return ColorPair{ .id = id }; 100 | } 101 | 102 | pub fn attr(self: ColorPair) c_int { 103 | return color_pair_attrs[@intCast(self.id)]; 104 | } 105 | }; 106 | 107 | // listed in init_pair man page 108 | pub const COLOR_BLACK = c.COLOR_BLACK; 109 | pub const COLOR_RED = c.COLOR_RED; 110 | pub const COLOR_GREEN = c.COLOR_GREEN; 111 | pub const COLOR_YELLOW = c.COLOR_YELLOW; 112 | pub const COLOR_BLUE = c.COLOR_BLUE; 113 | pub const COLOR_MAGENTA = c.COLOR_MAGENTA; 114 | pub const COLOR_CYAN = c.COLOR_CYAN; 115 | pub const COLOR_WHITE = c.COLOR_WHITE; 116 | -------------------------------------------------------------------------------- /src/cursesflags.h: -------------------------------------------------------------------------------- 1 | // https://github.com/ziglang/zig/issues/2070 2 | #include 3 | 4 | static const int MY_A_STANDOUT = A_STANDOUT; 5 | 6 | // i couldn't get an array to work 7 | static const int MY_COLOR_PAIR_1 = COLOR_PAIR(1); 8 | static const int MY_COLOR_PAIR_2 = COLOR_PAIR(2); 9 | static const int MY_COLOR_PAIR_3 = COLOR_PAIR(3); 10 | static const int MY_COLOR_PAIR_4 = COLOR_PAIR(4); 11 | static const int MY_COLOR_PAIR_5 = COLOR_PAIR(5); 12 | static const int MY_COLOR_PAIR_6 = COLOR_PAIR(6); 13 | static const int MY_COLOR_PAIR_7 = COLOR_PAIR(7); 14 | static const int MY_COLOR_PAIR_8 = COLOR_PAIR(8); 15 | static const int MY_COLOR_PAIR_9 = COLOR_PAIR(9); 16 | static const int MY_COLOR_PAIR_10 = COLOR_PAIR(10); 17 | -------------------------------------------------------------------------------- /src/cursesui.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const curses = @import("curses.zig"); 3 | const core = @import("core.zig"); 4 | 5 | 6 | pub const Characters = struct { 7 | horiz_line: []const u8, // - 8 | vert_line: []const u8, // | 9 | crossing_up: []const u8, // _|_ 10 | crossing_down: []const u8, // T 11 | crossing_left: []const u8, // --| 12 | crossing_right: []const u8, // |-- 13 | crossing_plus: []const u8, // --|-- 14 | corner_topleft: []const u8, // ,-- 15 | corner_topright: []const u8, // --. 16 | corner_bottomleft: []const u8, // |__ 17 | corner_bottomright: []const u8, // __| 18 | flag: []const u8, // F 19 | mine: []const u8, // * 20 | }; 21 | 22 | pub const ascii_characters = Characters{ 23 | .horiz_line = "-", 24 | .vert_line = "|", 25 | .crossing_up = "-", 26 | .crossing_down = "-", 27 | .crossing_left = "|", 28 | .crossing_right = "|", 29 | .crossing_plus = "+", 30 | .corner_topleft = ",", 31 | .corner_topright = ".", 32 | .corner_bottomleft = "`", 33 | .corner_bottomright = "'", 34 | .flag = "F", 35 | .mine = "*", 36 | }; 37 | 38 | pub const unicode_characters = Characters{ 39 | .horiz_line = "\xe2\x94\x80", // BOX DRAWINGS LIGHT HORIZONTAL 40 | .vert_line = "\xe2\x94\x82", // BOX DRAWINGS LIGHT VERTICAL 41 | .crossing_up = "\xe2\x94\xb4", // BOX DRAWINGS LIGHT UP AND HORIZONTAL 42 | .crossing_down = "\xe2\x94\xac", // BOX DRAWINGS LIGHT DOWN AND HORIZONTAL 43 | .crossing_left = "\xe2\x94\xa4", // BOX DRAWINGS LIGHT VERTICAL AND LEFT 44 | .crossing_right = "\xe2\x94\x9c", // BOX DRAWINGS LIGHT VERTICAL AND RIGHT 45 | .crossing_plus = "\xe2\x94\xbc", // BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL 46 | .corner_topleft = "\xe2\x95\xad", // BOX DRAWINGS LIGHT ARC DOWN AND RIGHT 47 | .corner_topright = "\xe2\x95\xae", // BOX DRAWINGS LIGHT ARC DOWN AND LEFT 48 | .corner_bottomleft = "\xe2\x95\xb0", // BOX DRAWINGS LIGHT ARC UP AND RIGHT 49 | .corner_bottomright = "\xe2\x95\xaf", // BOX DRAWINGS LIGHT ARC UP AND LEFT 50 | .flag = "\xe2\x9a\x91", // BLACK FLAG 51 | .mine = "\xe2\x88\x97", // ASTERISK OPERATOR 52 | }; 53 | 54 | 55 | pub const Ui = struct { 56 | selected_x: u8, 57 | selected_y: u8, 58 | game: *core.Game, 59 | window: curses.Window, 60 | chars: Characters, 61 | colors: bool, 62 | number_attrs: [9]c_int, // for coloring the numbers that show how many mines are around 63 | status_message: ?[]const u8, 64 | 65 | pub fn init(game: *core.Game, window: curses.Window, characters: Characters, want_color: bool) !Ui { 66 | var self = Ui{ 67 | .selected_x = 0, 68 | .selected_y = 0, 69 | .game = game, 70 | .window = window, 71 | .chars = characters, 72 | .colors = false, 73 | .number_attrs = undefined, 74 | .status_message = null, 75 | }; 76 | try self.setupColors(want_color and curses.has_colors()); 77 | return self; 78 | } 79 | 80 | fn setupColors(self: *Ui, use_colors: bool) !void { 81 | if (!use_colors) { 82 | @memset(&self.number_attrs, 0); 83 | return; 84 | } 85 | 86 | try curses.start_color(); 87 | const colors = comptime[_]c_short{ 88 | curses.COLOR_BLUE, 89 | curses.COLOR_GREEN, 90 | curses.COLOR_YELLOW, 91 | curses.COLOR_RED, 92 | curses.COLOR_CYAN, 93 | curses.COLOR_MAGENTA, 94 | curses.COLOR_MAGENTA, 95 | curses.COLOR_MAGENTA, 96 | curses.COLOR_MAGENTA, 97 | }; 98 | std.debug.assert(colors.len == self.number_attrs.len); 99 | for (colors, 0..) |color, i| { 100 | const pair = try curses.ColorPair.init(@intCast(i+1), color, curses.COLOR_BLACK); 101 | self.number_attrs[i] = pair.attr(); 102 | } 103 | } 104 | 105 | fn getWidth(self: *const Ui) u16 { return (self.game.width * @as(u16, "|---".len)) + @as(u16, "|".len); } 106 | fn getHeight(self: *const Ui) u16 { return (self.game.height * 2) + 1; } 107 | 108 | fn drawLine(self: *const Ui, y: u16, xleft: u16, left: []const u8, mid: []const u8, right: []const u8, horiz: []const u8) !void { 109 | var x: u16 = xleft; 110 | 111 | var i: u8 = 0; 112 | while (i < self.game.width) : (i += 1) { 113 | try self.window.mvaddstr(y, x, (if (i == 0) left else mid)); 114 | x += 1; 115 | var j: u8 = 0; 116 | while (j < 3) : (j += 1) { 117 | try self.window.mvaddstr(y, x, horiz); 118 | x += 1; 119 | } 120 | } 121 | try self.window.mvaddstr(y, x, right); 122 | } 123 | 124 | fn drawGrid(self: *const Ui) !void { 125 | const top: u16 = (self.window.getmaxy() - self.getHeight()) / 2; 126 | const left: u16 = (self.window.getmaxx() - self.getWidth()) / 2; 127 | 128 | var gamey: u8 = 0; 129 | var y: u16 = top; 130 | while (gamey < self.game.height) : (gamey += 1) { 131 | if (gamey == 0) { 132 | try self.drawLine(y, left, 133 | self.chars.corner_topleft, self.chars.crossing_down, self.chars.corner_topright, 134 | self.chars.horiz_line); 135 | } else { 136 | try self.drawLine(y, left, 137 | self.chars.crossing_right, self.chars.crossing_plus, self.chars.crossing_left, 138 | self.chars.horiz_line); 139 | } 140 | y += 1; 141 | 142 | var x: u16 = left; 143 | var gamex: u8 = 0; 144 | while (gamex < self.game.width) : (gamex += 1) { 145 | var attrs: c_int = 0; 146 | if (gamex == self.selected_x and gamey == self.selected_y) { 147 | attrs |= curses.A_STANDOUT; 148 | } 149 | 150 | const info = self.game.getSquareInfo(gamex, gamey); 151 | var msg1: []const u8 = ""; 152 | var msg2: []const u8 = ""; 153 | const numbers = "012345678"; 154 | 155 | if ((self.game.status == core.GameStatus.PLAY and info.opened) 156 | or (self.game.status != core.GameStatus.PLAY and !info.mine)) { 157 | msg1 = numbers[info.n_mines_around..info.n_mines_around+1]; 158 | attrs |= self.number_attrs[info.n_mines_around]; 159 | } else if (self.game.status == core.GameStatus.PLAY) { 160 | if (info.flagged) { 161 | msg1 = self.chars.flag; 162 | } 163 | } else { 164 | msg1 = self.chars.mine; 165 | if (info.flagged) { 166 | msg2 = self.chars.flag; 167 | } 168 | } 169 | 170 | try self.window.mvaddstr(y, x, self.chars.vert_line); 171 | x += 1; 172 | 173 | try self.window.attron(attrs); 174 | { 175 | try self.window.mvaddstr(y, x, " "); // make sure that all 3 character places get attrs 176 | x += 1; 177 | try self.window.mvaddstr(y, x, msg1); 178 | x += 1; 179 | try self.window.mvaddstr(y, x, msg2); 180 | x += 1; 181 | } 182 | try self.window.attroff(attrs); 183 | } 184 | try self.window.mvaddstr(y, x, self.chars.vert_line); 185 | y += 1; 186 | } 187 | 188 | try self.drawLine(y, left, 189 | self.chars.corner_bottomleft, self.chars.crossing_up, self.chars.corner_bottomright, 190 | self.chars.horiz_line); 191 | } 192 | 193 | pub fn setStatusMessage(self: *Ui, msg: []const u8) void { 194 | self.status_message = msg; 195 | } 196 | 197 | // this may overlap the grid on a small terminal, it doesn't matter 198 | fn drawStatusText(self: *const Ui, msg: []const u8) !void { 199 | try self.window.attron(curses.A_STANDOUT); 200 | try self.window.mvaddstr(self.window.getmaxy()-1, 0, msg); 201 | try self.window.attroff(curses.A_STANDOUT); 202 | } 203 | 204 | pub fn draw(self: *Ui) !void { 205 | try self.drawGrid(); 206 | 207 | if (self.status_message == null) { 208 | switch(self.game.status) { 209 | core.GameStatus.PLAY => {}, 210 | core.GameStatus.WIN => self.setStatusMessage("You won! :D Press n to play again."), 211 | core.GameStatus.LOSE => self.setStatusMessage("Game Over :( Press n to play again."), 212 | } 213 | } 214 | if (self.status_message) |msg| { 215 | try self.drawStatusText(msg); 216 | self.status_message = null; 217 | } 218 | } 219 | 220 | // returns whether to keep running the game 221 | pub fn onResize(self: *const Ui) !bool { 222 | if (self.window.getmaxy() < self.getHeight() or self.window.getmaxx() < self.getWidth()) { 223 | try curses.endwin(); 224 | var stderr = std.io.getStdErr().writer(); 225 | try stderr.print("Terminal is too small :( Need {}x{}.\n", .{ self.getWidth(), self.getHeight() }); 226 | return false; 227 | } 228 | return true; 229 | } 230 | 231 | pub fn moveSelection(self: *Ui, xdiff: i8, ydiff: i8) void { 232 | switch (xdiff) { 233 | 1 => if (self.selected_x != self.game.width-1) { self.selected_x += 1; }, 234 | -1 => if (self.selected_x != 0) { self.selected_x -= 1; }, 235 | 0 => {}, 236 | else => unreachable, 237 | } 238 | switch(ydiff) { 239 | 1 => if (self.selected_y != self.game.height-1) { self.selected_y += 1; }, 240 | -1 => if (self.selected_y != 0) { self.selected_y -= 1; }, 241 | 0 => {}, 242 | else => unreachable, 243 | } 244 | } 245 | 246 | pub fn openSelected(self: *const Ui) void { self.game.open(self.selected_x, self.selected_y); } 247 | pub fn toggleFlagSelected(self: *const Ui) void { self.game.toggleFlag(self.selected_x, self.selected_y); } 248 | pub fn openAroundIfSafe(self: *const Ui) void { self.game.openAroundIfSafe(self.selected_x, self.selected_y); } 249 | pub fn openAroundEverythingSafe(self: *const Ui) void { self.game.openAroundEverythingSafe(); } 250 | }; 251 | -------------------------------------------------------------------------------- /src/help.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const curses = @import("curses.zig"); 3 | 4 | 5 | pub const KeyBinding = struct { key: []const u8, help: []const u8 }; 6 | 7 | const Wrapper = struct { 8 | s: []const u8, 9 | maxlen: usize, 10 | 11 | fn nextLine(self: *Wrapper) ?[]const u8 { 12 | if (self.s.len == 0) { 13 | return null; 14 | } 15 | 16 | var line = if (std.mem.indexOfScalar(u8, self.s, '\n')) |i| self.s[0..i] else self.s; 17 | if (line.len > self.maxlen) { 18 | line = line[0..self.maxlen]; 19 | if (std.mem.lastIndexOfScalar(u8, line, ' ')) |i| { 20 | line = line[0..i]; 21 | } 22 | } 23 | 24 | if (self.s.len > line.len and (self.s[line.len] == '\n' or self.s[line.len] == ' ')) { 25 | self.s = self.s[line.len+1..]; 26 | } else { 27 | self.s = self.s[line.len..]; 28 | } 29 | return line; 30 | } 31 | }; 32 | 33 | 34 | // if you modify this, make sure that it fits on an 80x24 standard-sized terminal! 35 | const text_with_single_newlines = 36 | \\The goal is to open all squares that don't contain mines, but none of the squares that contain 37 | \\mines. When you have opened a square, the number of the square indicates how many mines there 38 | \\are in the 8 other squares around the opened square (aka "neighbor squares"). The game ends 39 | \\when all non-mine squares are opened (you win), or a mine square is opened (you lose). 40 | \\ 41 | \\You can flag the mines to keep track of them more easily (but you don't need to). This has no 42 | \\effect on winning or losing; the flagging is meant to be nothing more than a handy way to keep 43 | \\track of the mines you have already found. 44 | \\ 45 | \\Flagging the mines can make playing the game a lot easier. For example, if you have opened a 46 | \\square and it shows the number 3, and you have flagged exactly 3 of its neighbors, then you 47 | \\can open all other neighbors because they can't contain mines. This is how the "d" key works. 48 | ; 49 | 50 | // does nothing to \n\n repeated, but replaces single \n with spaces 51 | fn removeSingleNewlines(s: []const u8, allocator: std.mem.Allocator) ![]u8 { 52 | var result = std.ArrayList(u8).init(allocator); 53 | errdefer result.deinit(); 54 | 55 | var i: usize = 0; 56 | while (i < s.len) { 57 | if (i+1 < s.len and s[i] == '\n' and s[i+1] == '\n') { 58 | try result.append('\n'); 59 | try result.append('\n'); 60 | i += 2; 61 | } else if (s[i] == '\n') { 62 | try result.append(' '); 63 | i += 1; 64 | } else { 65 | try result.append(s[i]); 66 | i += 1; 67 | } 68 | } 69 | return result.toOwnedSlice(); 70 | } 71 | 72 | fn incrementY(window: curses.Window, y: *u16) !void { 73 | y.* += 1; 74 | if (y.* >= window.getmaxy()) { 75 | return error.TerminalIsTooSmall; 76 | } 77 | } 78 | 79 | fn drawText(window: curses.Window, key_bindings: []const KeyBinding, allocator: std.mem.Allocator) !void { 80 | try window.erase(); 81 | 82 | var maxlen: u16 = 0; 83 | for (key_bindings) |kb| { 84 | if (kb.key.len > maxlen) { 85 | maxlen = @intCast(kb.key.len); 86 | } 87 | } 88 | 89 | var y: u16 = 0; 90 | try window.mvaddstr(y, 0, "Key Bindings:"); 91 | try incrementY(window, &y); 92 | for (key_bindings) |kb| { 93 | try window.mvaddstr(y, 2, kb.key); 94 | try window.mvaddstr(y, 2 + maxlen + 2, kb.help); 95 | try incrementY(window, &y); 96 | } 97 | try incrementY(window, &y); 98 | 99 | { 100 | const text = try removeSingleNewlines(text_with_single_newlines, allocator); 101 | defer allocator.free(text); 102 | 103 | var wrapper = Wrapper{ .s = text, .maxlen = window.getmaxx() }; 104 | while (wrapper.nextLine()) |line| { 105 | try window.mvaddstr(y, 0, line); 106 | try incrementY(window, &y); 107 | } 108 | } 109 | 110 | try window.attron(curses.A_STANDOUT); 111 | try window.mvaddstr(window.getmaxy() - 1, 0, "Press q to quit this help..."); 112 | try window.attroff(curses.A_STANDOUT); 113 | } 114 | 115 | // returns true if ok, false if help message didn't fit on the terminal 116 | pub fn show(window: curses.Window, key_bindings: []const KeyBinding, allocator: std.mem.Allocator) !bool { 117 | while (true) { 118 | drawText(window, key_bindings, allocator) catch |err| switch(err) { 119 | error.TerminalIsTooSmall => return false, // it might be playable even if help doesn't fit 120 | else => return err, 121 | }; 122 | switch (try window.getch()) { 123 | 'Q', 'q' => return true, 124 | else => {}, 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c_locale = @cImport(@cInclude("locale.h")); 3 | const argparser = @import("argparser.zig"); 4 | const core = @import("core.zig"); 5 | const curses = @import("curses.zig"); 6 | const cursesui = @import("cursesui.zig"); 7 | const help = @import("help.zig"); 8 | 9 | 10 | pub fn main() anyerror!void { 11 | const allocator = std.heap.c_allocator; 12 | 13 | // displaying unicode characters in curses needs this and cursesw in build.zig 14 | _ = c_locale.setlocale(c_locale.LC_ALL, ""); 15 | 16 | const args = try argparser.parse(allocator); 17 | 18 | // Initialize random generator with the current time as seed. 19 | const time = std.time.milliTimestamp(); 20 | var rnd = std.rand.DefaultPrng.init(@intCast(time)); 21 | 22 | var game = try core.Game.init(allocator, args.width, args.height, args.nmines, rnd.random()); 23 | defer game.deinit(); 24 | 25 | const stdscr = try curses.initscr(allocator); 26 | var endwin_called: bool = false; 27 | defer { 28 | if (!endwin_called) { 29 | curses.endwin() catch {}; // failures are ignored because not much can be done to them 30 | } 31 | } 32 | 33 | _ = try curses.curs_set(0); 34 | _ = try stdscr.keypad(true); 35 | 36 | var ui = try cursesui.Ui.init(&game, stdscr, args.characters, args.color); 37 | 38 | if (!try ui.onResize()) { 39 | endwin_called = true; 40 | return; 41 | } 42 | 43 | // FIXME: the help doesn't fit in 80x24 terminal 44 | const key_bindings = comptime[_]help.KeyBinding{ 45 | help.KeyBinding{ .key = "q", .help = "quit the game" }, 46 | help.KeyBinding{ .key = "h", .help = "show this help" }, 47 | help.KeyBinding{ .key = "n", .help = "new game" }, 48 | help.KeyBinding{ .key = "arrow keys", .help = "move the selection" }, 49 | help.KeyBinding{ .key = "enter", .help = "open the selected square" }, 50 | help.KeyBinding{ .key = "f", .help = "flag or unflag the selected square" }, 51 | help.KeyBinding{ .key = "d", .help = "open all non-flagged neighbors if the correct number of them are flagged" }, 52 | help.KeyBinding{ .key = "e", .help = "like pressing d in all the squares" }, 53 | }; 54 | ui.setStatusMessage("Press h for help."); 55 | 56 | while (true) { 57 | try stdscr.erase(); 58 | try ui.draw(); 59 | 60 | switch (try stdscr.getch()) { 61 | 'Q', 'q' => return, 62 | 'N', 'n' => game = try core.Game.init(allocator, args.width, args.height, args.nmines, rnd.random()), 63 | 'H', 'h' => { 64 | const help_fit_on_terminal = try help.show(stdscr, key_bindings[0..], allocator); 65 | // terminal may have been resized while looking at help 66 | if (!try ui.onResize()) { 67 | endwin_called = true; 68 | return; 69 | } 70 | if (!help_fit_on_terminal) { 71 | ui.setStatusMessage("Please make your terminal bigger to read the help message."); 72 | } 73 | }, 74 | curses.KEY_RESIZE => { 75 | if (!try ui.onResize()) { 76 | endwin_called = true; 77 | return; 78 | } 79 | }, 80 | curses.KEY_LEFT => ui.moveSelection(-1, 0), 81 | curses.KEY_RIGHT => ui.moveSelection(1, 0), 82 | curses.KEY_UP => ui.moveSelection(0, -1), 83 | curses.KEY_DOWN => ui.moveSelection(0, 1), 84 | '\n' => ui.openSelected(), 85 | 'F', 'f' => ui.toggleFlagSelected(), 86 | 'D', 'd' => ui.openAroundIfSafe(), 87 | 'E', 'e' => ui.openAroundEverythingSafe(), 88 | else => {}, 89 | } 90 | } 91 | } 92 | --------------------------------------------------------------------------------