Skip to content

Instantly share code, notes, and snippets.

@leduyquang753
Last active July 30, 2022 04:29
Show Gist options
  • Save leduyquang753/35a272025b84430e5dbdacbd79234b8f to your computer and use it in GitHub Desktop.
Save leduyquang753/35a272025b84430e5dbdacbd79234b8f to your computer and use it in GitHub Desktop.
Minesweeper played from an ANSI terminal.
/*
ANSI terminal Minesweeper game.
Written by Lê Duy Quang ([email protected]).
Command line arguments: <width> <height> <mines>.
If those are not specified, defaults to a 9.9 board with 10 mines
(beginner difficulty).
Controls:
Arrow keys to move.
Enter on the cell under the cursor:
If the cell is not yet opened and is not flagges/suspicious,
open that cell.
If the cell is opened and the number of surrounding flags is equal
to the number displayed in the cell, and there are no surrounding
suspicious cells, open all adjacent unflagged cells.
' (apostrophe) to mark the cell as has a mine (!) or suspicious (?).
Ctrl + C to force quit.
Requires a terminal with ANSI escape code support to display properly.
Have fun.
*/
#include <conio.h>
#include <chrono>
#include <iostream>
#include <queue>
#include <random>
#include <string>
#include <vector>
using namespace std::string_literals;
const std::string
// If you want to decipher this ANSI code madness, you can refer to
// https://en.wikipedia.org/wiki/ANSI_escape_code
escape = "\033["s,
restoreCursor = "\033[u"s,
backCursor = "\033[1D"s,
nextCursor = "\033[1C"s,
mineDisplay = "\033[38;2;255;0;0mX\033[0m"s,
displayStrings[] {
"\033[48;2;64;64;64m.\033[0m"s,
"\033[7m\033[38;2;255;255;0m!\033[0m"s,
"\033[7m\033[38;2;224;152;203m?\033[0m"s
},
numberStrings[] {
"\033[38;2;100;100;100m0\033[0m"s,
"\033[38;2;149;253;141m1\033[0m"s,
"\033[38;2;193;253;141m2\033[0m"s,
"\033[38;2;253;252;141m3\033[0m"s,
"\033[38;2;253;211;141m4\033[0m"s,
"\033[38;2;253;165;141m5\033[0m"s,
"\033[38;2;253;141;176m6\033[0m"s,
"\033[38;2;253;141;220m7\033[0m"s,
"\033[38;2;237;141;253m8\033[0m"s
};
/*
Places mines randomly on the board, avoiding around the first cell that
the player opened to guarantee an opening.
*/
void generateBoard(
std::vector<char> &board, const int &size, const int &mines,
const int &width, const int &height, const int &row, const int &col
) {
int edges = 0;
if (row == 0 || row == height - 1) edges++;
if (col == 0 || col == width - 1) edges++;
constexpr int edgesMapping[] {9, 6, 4};
std::vector<int> shuffler;
int poolSize = size - edgesMapping[edges];
shuffler.reserve(poolSize);
for (int r = 0; r < height; r++)
for (int c = 0; c < width; c++)
if (r < row-1 || r > row+1 || c < col-1 || c > col+1)
shuffler.push_back(r*width + c);
std::default_random_engine randomEngine((std::random_device())());
int cap = poolSize - mines - 1;
for (int last = poolSize-1; last > cap; last--) {
std::uniform_int_distribution<int> distribution(0, last);
int index = distribution(randomEngine);
board[shuffler[index]] = 1;
shuffler[index] = shuffler[last];
}
}
/*
Gets the number of mines adjacent to the specified cell.
*/
int getAdjacent(
std::vector<char> &board, char match,
const int &width, const int &height,
const int &row, const int &col
) {
int rb = row-1, re = row+2, cb = col-1, ce = col+2, count = 0;
for (int r = rb; r < re; r++)
if (r != -1 && r != height)
for (int c = cb; c < ce; c++)
if (c != -1 && c != width && board[r*width + c] == match)
count++;
return count;
}
std::string moveCursor(int &cursorX, int &cursorY, int x, int y) {
std::string s;
s += escape;
if (y > cursorY) {
s += std::to_string(y - cursorY);
s += 'B';
} else if (y < cursorY) {
s += std::to_string(cursorY - y);
s += 'A';
}
s += escape;
s += std::to_string(x << 1 | 1);
s += 'G';
cursorX = x;
cursorY = y;
return s;
}
struct Coords {
int row, col;
Coords(int row, int col): row{row}, col{col} {}
};
/*
Triggers an "opening" from a 0 cell, that is, discovering nearby 0 cells
and also opening them.
*/
void floodFill(
std::vector<char> &board, std::vector<char> &display,
const int &width, const int &height,
const int &row, const int &col,
int &opened,
std::string &updateString
) {
int cursorX = col, cursorY = row;
std::queue<Coords> queue;
queue.emplace(row, col);
while (!queue.empty()) {
Coords &coords = queue.front();
int rb = coords.row-1, re = coords.row+2,
cb = coords.col-1, ce = coords.col+2;
for (int r = rb; r < re; r++)
if (r != -1 && r != height)
for (int c = cb; c < ce; c++)
if (
c != -1 && c != width &&
display[r*width + c] == 0
) {
int mines = getAdjacent(
board, 1, width, height, r, c
);
display[r*width + c] = mines + 3;
updateString += moveCursor(cursorX, cursorY, c, r);
updateString += numberStrings[mines];
updateString += backCursor;
opened++;
if (mines == 0) queue.emplace(r, c);
}
queue.pop();
}
updateString += moveCursor(cursorX, cursorY, col, row);
}
/*
Primarily to show mines and also handle flagged cells to show whether the
flag was correctly placed on a mine, or not.
*/
void revealCell(
std::vector<char> &board, std::vector<char> &display,
const int &index, std::string &updateString
) {
char mine = board[index], state = display[index];
updateString +=
state == 1
? mine
? "\033[7m\033[38;2;149;253;141m!\033[0m"s
: "\033[7m\033[38;2;156;156;156m!\033[0m"s
: mine ? mineDisplay : nextCursor;
}
/*
Returns true if the opened cell is a mine, false if not or the cell is
nonexistent (out of bounds).
*/
bool openCell(
std::vector<char> &board, std::vector<char> &display,
const int &width, const int &height,
int &opened, int &cursorX, int &cursorY,
const int &row, const int &col,
std::string &updateString
) {
if (row == -1 || row == height || col == -1 || col == width)
return false;
int index = row*width + col;
if (display[index] != 0) return false;
updateString += moveCursor(cursorX, cursorY, col, row);
if (board[index]) { // It's a mine. Boom and game over.
updateString += escape;
if (cursorY != 0) {
updateString += std::to_string(cursorY);
updateString += 'A';
}
updateString += "\033[1G"s;
for (int row = 0; row < height; row++) {
revealCell(board, display, row*width, updateString);
for (int col = 1; col < width; col++) {
updateString += nextCursor;
revealCell(
board, display, row*width+col, updateString
);
}
updateString += "\033[1B\033[1G"s;
}
updateString += escape;
updateString += std::to_string(height - cursorY);
updateString += 'A';
updateString += escape;
updateString += std::to_string(cursorX << 1 | 1);
updateString += "G\033[7m";
updateString += mineDisplay;
updateString += restoreCursor;
updateString += "You detonated a mine. Better luck next time.\n"s;
return true;
} else { // Safe.
int minesHere = getAdjacent(
board, 1, width, height, cursorY, cursorX
);
display[index] = 3 + minesHere;
updateString += numberStrings[minesHere];
updateString += backCursor;
opened++;
if (minesHere == 0) floodFill(
board, display,
width, height, cursorY, cursorX,
opened, updateString
);
return false;
}
}
int getIntLength(int i) {
int res = 1;
i /= 10;
while (i != 0) {
res++;
i /= 10;
}
return res;
}
std::string padInt(int i, int length) {
std::string res;
const int padSize = length - getIntLength(i);
if (padSize > 0) res.append(padSize, ' ');
res += std::to_string(i);
return res;
}
void updateStatusBar(
const int &opened, const int &winCount,
const int &flagged, const int &mines,
const int &height,
const int &cursorX, const int &cursorY,
std::string &updateString
) {
updateString += restoreCursor;
updateString += "\033[1K\033[38;2;138;244;119m"s;
updateString += padInt(opened, getIntLength(winCount));
updateString += " / "s;
updateString += std::to_string(winCount);
updateString += " \033[38;2;244;204;119m"s;
updateString += padInt(flagged, getIntLength(mines));
updateString += " / "s;
updateString += std::to_string(mines);
updateString += "\033[0m\033["s;
updateString += std::to_string(height - cursorY);
updateString += "A\033["s;
updateString += std::to_string(cursorX << 1 | 1);
updateString += 'G';
}
std::string formatDuration(double duration) {
int
secs = duration,
h = secs / 3600,
m = secs % 3600 / 60,
s = secs % 60,
ms = (int)(duration * 1000) % 1000;
std::string res, millis = ","s;
if (ms < 100) millis += '0';
if (ms < 10) millis += '0';
millis += std::to_string(ms);
if (h != 0) {
res += std::to_string(h);
res += 'h';
}
if (secs > 59) {
if (h != 0 && m < 10) res += '0';
res += std::to_string(m);
res += ':';
if (s < 10) res += '0';
res += std::to_string(s);
res += millis;
} else {
res += std::to_string(s);
res += millis;
res += '"';
}
return res;
}
int main(int argc, char **argv) {
int width = 9, height = 9, mines = 10, size = width*height;
if (argc > 3) try {
width = std::stoi(argv[1]);
height = std::stoi(argv[2]);
mines = std::stoi(argv[3]);
size = width * height;
// The number of mines must be at least 9 fewer than the number of
// cells so as to make room for safe cells from the first move.
if (width < 4 || height < 4 || mines < 1 || mines > size - 9) {
std::cerr << "Invalid arguments.\n"s;
return 1;
}
} catch (const std::exception &e) {
std::cerr << "Invalid arguments.\n"s;
return 1;
}
/*
Board codes:
0: No flag.
1: Flag.
2: Question.
3 ÷ 11: Opened, number of mines = code – 3.
*/
std::vector<char> display(size, 0);
std::vector<char> board(size, 0);
int
cursorX = 0, cursorY = 0,
opened = 0, winCount = size-mines, flagged = 0;
std::string updateString;
for (int row = 0; row < height; row++) {
updateString += displayStrings[0];
for (int col = 1; col < width; col++) {
updateString += ' ';
updateString += displayStrings[0];
}
updateString += '\n';
}
updateString += "\033[s\033[1G\033["s;
updateString += std::to_string(height);
updateString += 'A';
updateStatusBar(
opened, winCount, flagged, mines,
height, cursorX, cursorY, updateString
);
std::cout << updateString;
std::chrono::time_point<std::chrono::steady_clock> startTime;
bool arrow = false, firstMove = true;
while (true) {
updateString.clear();
if (arrow) {
switch (_getch()) {
case 72: { // Up.
if (cursorY == 0) {
cursorY = height-1;
updateString += escape;
updateString += std::to_string(cursorY);
updateString += 'B';
} else {
cursorY--;
updateString += "\033[1A"s;
}
break;
}
case 80: { // Down.
if (cursorY == height-1) {
cursorY = 0;
updateString += escape;
updateString += std::to_string(height-1);
updateString += 'A';
} else {
cursorY++;
updateString += "\033[1B"s;
}
break;
}
case 75: { // Left.
cursorX = (cursorX + width - 1) % width;
updateString += escape;
updateString += std::to_string(cursorX << 1 | 1);
updateString += 'G';
break;
}
case 77: { // Right.
cursorX = (cursorX + 1) % width;
updateString += escape;
updateString += std::to_string(cursorX << 1 | 1);
updateString += 'G';
break;
}
}
arrow = false;
} else switch (_getch()) {
case 224: {
arrow = true;
break;
}
case 39: { // Toggle flag/suspicious.
char &cell = display[cursorY*width + cursorX];
if (cell < 3) {
cell = (cell + 1) % 3;
updateString += displayStrings[cell];
updateString += backCursor;
switch (cell) {
case 1:
flagged++;
break;
case 2:
flagged--;
break;
}
updateStatusBar(
opened, winCount, flagged, mines,
height, cursorX, cursorY, updateString
);
}
break;
}
case 13: { // Open.
int index = cursorY*width + cursorX;
char &cell = display[index];
if (cell == 1 || cell == 2) break;
if (
cell != 0 && (
getAdjacent(
display, 2, width, height, cursorY, cursorX
) != 0 ||
getAdjacent(
display, 1, width, height, cursorY, cursorX
) != cell - 3
)
) break;
if (firstMove) {
generateBoard(
board, size, mines,
width, height, cursorY, cursorX
);
startTime = std::chrono::steady_clock::now();
firstMove = false;
}
bool lost = false;
if (cell == 0) {
lost = openCell(
board, display, width, height,
opened, cursorX, cursorY,
cursorY, cursorX, updateString
);
} else {
const int rowOffset[] {-1, -1, -1, 0, 0, 1, 1, 1},
colOffset[] {-1, 0, 1, -1, 1, -1, 0, 1};
int row = cursorY, col = cursorX;
for (int i = 0; i < 8; i++) {
if (openCell(
board, display, width, height,
opened, cursorX, cursorY,
row+rowOffset[i], col+colOffset[i],
updateString
)) {
lost = true;
break;
}
}
if (!lost) {
updateString += moveCursor(
cursorX, cursorY, col, row
);
}
}
if (lost) {
std::cout << updateString;
return 0;
} else {
if (opened == winCount) {
updateString += restoreCursor;
updateString += "\033[0KYou win. The game took "s;
updateString += formatDuration(
std::chrono::duration_cast<
std::chrono::milliseconds
>(std::chrono::steady_clock::now()-startTime)
.count()/1000.d
);
updateString += ".\n"s;
std::cout << updateString;
return 0;
} else {
updateStatusBar(
opened, winCount, flagged, mines,
height, cursorX, cursorY, updateString
);
}
}
break;
}
case 3: {
updateString += restoreCursor;
updateString += '\n';
std::cout << updateString;
return 0;
break;
}
}
std::cout << updateString;
}
}
@IamWuan
Copy link

IamWuan commented May 31, 2021

quite gud =)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment