Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
huytrinhm committed Dec 23, 2023
0 parents commit 0d3914a
Show file tree
Hide file tree
Showing 14 changed files with 1,239 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build/
report.pdf
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 huytrinhm

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
minesweeper:
mkdir -p build/ && cd src/ && \
g++ -Wall -O2 -std=c++17 \
minesweeper.cpp game_controller.cpp ui_controller.cpp \
-o ../build/minesweeper \
-static-libstdc++ -static-libgcc \
-Wl,-Bstatic -lstdc++ -lpthread \
-Wl,-Bdynamic \
-Wl,--as-needed -Wl,--strip-all
146 changes: 146 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
C++ Minesweeper
===============

![Gameplay](screenshots/game.png)

This is a simple minesweeper game that runs directly in the terminal (with out-of-the-box **mouse** and **keyboard** navigation support). The project is implemented in C++, utilizing [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code). Terminal mouse and keyboard support was achieved by Win32 APIs (via `windows.h`), DOS console input/output APIs (via `conio.h`) on Win32 systems, and UNIX terminal input/output APIs (via `termios.h`, `sys/ioctl.h`, `unistd.h`) on UNIX systems.

The program is tested on Windows 11 (Version 22H2) and Windows Subsystem for Linux (Version 2.0.9.0).

See `demo.mp4` for demo gameplay.

Features
--------
1. **Gameplay**
- Customize board size (number of rows, columns and mines).
- First click guaranteed to be empty.
- Player can click on a opened cell with enough flags to quickly open all remaining neighbor cells.

2. **UI**
- Main menu: New game, Resume game (if exists), Quit.
- Start game menu: customize board size and number of mines.
- Game screen: Live timer and mines counter, colorful minefield.
- Pause screen, win screen and lose screen allow player to save game or go back to main menu to retry.
- **Auto re-render UI if detecting terminal resize.**
- **Auto notify player if current terminal size is too small to display the game screen after resize.**

3. **Control**
- By keyboard: use arrow keys to navigate through minefield, spacebar to open cell and `F` to flag cell, escape key to pause game, back to menu.
- **By mouse** (Windows only, not yet implemented for UNIX systems): hover effects, left click to select option / open cell, right click to flag cell.

4. **Other**
- Auto-save highscore (best time) for each board size combination.
- Options to save and resume game (with continued timer).
- Timer only starts after first move.

Compile and Run
---------------
If you have `make` on your system, simply run `make minesweeper` to build the project. Otherwise, you can compile it using the following command:
```bash
mkdir -p build/ && cd src/ && \
g++ -Wall -O2 -std=c++17 \
minesweeper.cpp game_controller.cpp ui_controller.cpp \
-o ../build/minesweeper \
-static-libstdc++ -static-libgcc \
-Wl,-Bstatic -lstdc++ -lpthread \
-Wl,-Bdynamic \
-Wl,--as-needed -Wl,--strip-all
```

Then, you can run `build/minesweeper.exe` or `build/minesweeper`.

Prebuilt binaries are also provided in the [Releases](https://github.com/huytrinhm/cpp-minesweeper/releases) section. However, the underlying architecture and system APIs differ from machine to machine and cannot be static-linked, be cautious that the prebuilt binaries may not work properly.

Project structure
-----------------
The `src/` directory includes:
1. `minesweeper.cpp`
- The entrypoint of the game.
- Contains core logic of the game loop and save/load game functionalities.

List of functions:
```cpp
bool gameLoop(GameState& state, bool isSaved);
void saveGame(const GameState& state);
bool loadGame(GameState& state);
void deleteSave();
void loadHighscores(int highScores[MAX_M][MAX_N][MAX_M * MAX_N], size_t size);
void saveHighscores(int highScores[MAX_M][MAX_N][MAX_M * MAX_N], size_t size);
int main(); // entrypoint
```
2. `game_controller.h`, `game_controller.cpp`:
- Utility library for managing game logic (create and manage game states).
- The game state is modeled as follows:
```cpp
struct GameState_s {
bool board[MAX_M][MAX_N]; // 0: bomb; 1: empty
int display[MAX_M][MAX_N]; // 0-8: numbers; 9: not yet opened; 10: flag; 11: bomb
int rows, cols, bombCount, elapsedTime;
bool generated;
};
```

List of functions:
```cpp
void initBoard(GameState& state, int rows, int cols, int bombCount); // initialize game state
void genBoard(GameState& state, int r, int c); // randomize minefield
bool inBound(const GameState& state, int r, int c); // check position is inside minefield
int updateDisplayPosition(GameState& state, int r, int c); // update mine count number
bool openPosition(GameState& state, int r, int c); // triggered when player click on a cell
void toggleFlagPosition(GameState& state, int r, int c); // triggered when player flag a cell
void openAllBomb(GameState& state); // game over procedure
bool isWinState(GameState& state);
```
3. `ui_controller.h`, `ui_controller.cpp`:
- Utility library for managing game UI (render menu and minefield, handle player mouse and keyboard inputs).
- Using preprocessor directive `#ifdef _WIN32` to implement platform dependent features (terminal input/output APIs, WIN32 APIs to capture mouse events).
List of functions:
```cpp
void initConsole();
void closeConsole();
void hideCursor();
void showCursor();
void getConsoleWidthHeight(int& width, int& height);
void clearScreen(int mode);
void clearScreenInline(int mode);
bool screenToBoard(GameState& state, // convert screen coordinates to board coordinates
int screen_r,
int screen_c,
int& board_r,
int& board_c);
void render(const GameState& state, // render game board
int cursor_r,
int cursor_c,
bool skipHeader = false,
bool skipBoard = false,
bool skipFooter = false);
int getInput(); // platform-specific get keyboard input
bool getMouseInput(int& r, int& c, int& event); // platform-specific get mouse input (TODO: implement for UNIX system)
int mainMenu(bool saved); // render main menu
void startGameMenu(int& rows, int& cols, int& bombCount); // render start game menu
bool loseMenu(const GameState& state, int cursor_r, int cursor_c); // render lose menu
bool winMenu(const GameState& state, int bestTime); // render win menu
int pauseMenu(const GameState& state); // render pause menu
void wait(); // wait for keyboard input
```

Gameplay
--------

Main menu:
![Main menu](screenshots/menu.png)

Start game menu:
![Start game menu](screenshots/options.png)

Game screen:
![Game screen](screenshots/game.png)

Lose screen:
![Lose screen](screenshots/lose.png)

Win screen:
![Win screen](screenshots/win.png)
Binary file added screenshots/game.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/lose.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/options.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/win.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
133 changes: 133 additions & 0 deletions src/game_controller.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#include "game_controller.h"
#include <algorithm>
#include <chrono>
#include <random>

const int dr[8] = {-1, -1, -1, 0, 0, 1, 1, 1};
const int dc[8] = {-1, 0, 1, -1, 1, -1, 0, 1};

std::mt19937_64 rng(
std::chrono::steady_clock::now().time_since_epoch().count());

void initBoard(GameState& state, int rows, int cols, int bombCount) {
state.rows = rows;
state.cols = cols;
state.elapsedTime = 0;
state.bombCount = bombCount;
state.generated = false;

for (int r = 0; r <= rows + 1; r++) {
for (int c = 0; c <= cols + 1; c++) {
state.board[r][c] = 0;
state.display[r][c] = 9;
}
}
}

void genBoard(GameState& state, int r, int c) {
int bombCandidate[state.rows * state.cols - 1];

for (int i = 0, p = 0; i < state.rows * state.cols; i++)
if (i != (r - 1) * state.cols + c - 1)
bombCandidate[p++] = i;

std::shuffle(bombCandidate, bombCandidate + state.rows * state.cols - 1, rng);

for (int i = 0; i < state.bombCount; i++)
state.board[bombCandidate[i] / state.cols + 1]
[bombCandidate[i] % state.cols + 1] = 1;
state.generated = true;
}

bool inBound(const GameState& state, int r, int c) {
return r > 0 && c > 0 && r <= state.rows && c <= state.cols;
}

int updateDisplayPosition(GameState& state, int r, int c) {
if (state.board[r][c] == 1)
return -1;
int count = 0;
for (int i = 0; i < 8; i++)
count += state.board[r + dr[i]][c + dc[i]];
state.display[r][c] = count;
return count != 0;
}

bool openPosition(GameState& state, int r, int c) {
if (!inBound(state, r, c))
return true;

int stack[MAX_M * MAX_N + 10][2];
int sTop = 0;

if (state.display[r][c] <= 8) {
int count = 0;
for (int i = 0; i < 8; i++)
count += state.display[r + dr[i]][c + dc[i]] == 10;
if (count == state.display[r][c]) {
for (int i = 0; i < 8; i++)
if (state.display[r + dr[i]][c + dc[i]] == 9 &&
state.board[r + dr[i]][c + dc[i]]) {
return false;
} else if (state.display[r + dr[i]][c + dc[i]] == 9) {
stack[sTop][0] = r + dr[i];
stack[sTop++][1] = c + dc[i];
}
}
}

stack[sTop][0] = r;
stack[sTop++][1] = c;

while (sTop > 0) {
int sR = stack[--sTop][0];
int sC = stack[sTop][1];

if (state.display[sR][sC] <= 8)
continue;

int updateResult = updateDisplayPosition(state, sR, sC);

if (updateResult == -1) {
state.display[sR][sC] = 11;
return false;
}

if (updateResult != 0)
continue;

for (int i = 0; i < 8; i++) {
if (inBound(state, sR + dr[i], sC + dc[i]) &&
state.display[sR + dr[i]][sC + dc[i]] > 8) {
stack[sTop][0] = sR + dr[i];
stack[sTop++][1] = sC + dc[i];
}
}
}

return true;
}

void toggleFlagPosition(GameState& state, int r, int c) {
if (!inBound(state, r, c) || state.display[r][c] <= 8)
return;
state.display[r][c] = state.display[r][c] == 9 ? 10 : 9;
}

void openAllBomb(GameState& state) {
for (int r = 1; r <= state.rows; r++)
for (int c = 1; c <= state.cols; c++)
if (state.board[r][c]) {
if (state.display[r][c] != 10)
state.display[r][c] = 11;
} else if (state.display[r][c] == 10)
state.display[r][c] = 12;
}

bool isWinState(GameState& state) {
for (int r = 1; r <= state.rows; r++)
for (int c = 1; c <= state.cols; c++)
if (!state.board[r][c] && state.display[r][c] > 8)
return false;
return true;
}
24 changes: 24 additions & 0 deletions src/game_controller.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#ifndef GAME_CONTROLLER_H
#define GAME_CONTROLLER_H

const int MAX_M = 120;
const int MAX_N = 120;

struct GameState_s {
bool board[MAX_M][MAX_N];
int display[MAX_M][MAX_N];
int rows, cols, bombCount, elapsedTime;
bool generated;
};
typedef struct GameState_s GameState;

void initBoard(GameState& state, int rows, int cols, int bombCount);
void genBoard(GameState& state, int r, int c);
bool inBound(const GameState& state, int r, int c);
int updateDisplayPosition(GameState& state, int r, int c);
bool openPosition(GameState& state, int r, int c);
void toggleFlagPosition(GameState& state, int r, int c);
void openAllBomb(GameState& state);
bool isWinState(GameState& state);

#endif
Loading

0 comments on commit 0d3914a

Please sign in to comment.