-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 0d3914a
Showing
14 changed files
with
1,239 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
build/ | ||
report.pdf |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.