-
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
Showing
15 changed files
with
934 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,107 @@ | ||
# cliArcade | ||
> Play retro games in your linux terminal. | ||
### Whats new? | ||
there's hundreds of similar projects out there but what makes this one unique feature is the ability to choose from 6 different rendering resolutions. | ||
<details open><summary>video demos</summary> | ||
|
||
> [!TIP] | ||
> If the output doesn't look right it is probably because of the font your terminal is using, use one of these recommended terminal emulators instead: [kitty](https://sw.kovidgoyal.net/kitty), [wizterm](https://wezfurlong.org/wezterm), [foot](https://codeberg.org/dnkl/foot). | ||
|[![vid](https://img.youtube.com/vi/ZEhrxV3D07o/default.jpg)](https://youtu.be/ZEhrxV3D07o)<br>Snake|[![vid](https://img.youtube.com/vi/YGyJRs-reIk/default.jpg)](https://youtu.be/YGyJRs-reIk)<br>Oscillators| | ||
|:-:|:-:| | ||
|[![vid](https://img.youtube.com/vi/GkgLRtLd3XY/default.jpg)](https://youtu.be/GkgLRtLd3XY)<br>Manual Entry|[![vid](https://img.youtube.com/vi/2-pmjYV7ReA/default.jpg)](https://youtu.be/2-pmjYV7ReA)<br>Replicator | ||
|
||
</details><details><summary>How does it work?</summary> | ||
|
||
I was inspired by [the unicode implementation of braille characters](https://en.wikipedia.org/wiki/Braille_Patterns), So I followed a similar approach and made a lookup array to index into: | ||
```cpp | ||
auto lookupArray = L" ▘▝▀▖▌▞▛▗▚▐▜▄▙▟█"; | ||
for (int i = 0, h = height(); i < h; i += 2){ | ||
printBuffer.emplace_back(L""); | ||
for (int j = 0, w = width(); j < w; j += 2) | ||
printBuffer.back() += lookupArray[ | ||
rawBuffer[i][j] | ||
| rawBuffer[i][j + 1] << 1 | ||
| rawBuffer[i + 1][j] << 2 | ||
| rawBuffer[i + 1][j + 1] << 3 | ||
]; | ||
} | ||
``` | ||
</details><details><summary>Project File Structure</summary> | ||
|
||
> [!NOTE] | ||
> The entire project results in a single object file because this project is small and the compile time is not an issue. | ||
``` | ||
cliArcade | ||
├─README.md | ||
├─main.cpp | ||
├─terminal.cpp // Utilities for interacting with the terminal. | ||
├─baseMenu.cpp // A base class for creating a selection menu. | ||
├─mainMenu.cpp // A menu for choosing the game. | ||
├─resolutionMenu.cpp // A menu for choosing the resolution. | ||
├─games.h // Included games. | ||
└─games | ||
├─snake | ||
│ └─snake.cpp | ||
└─gameOfLife | ||
├─gameOfLife.cpp | ||
├─patternMenu.cpp // A menu to choose pattern importing method. | ||
├─fileMenu.cpp // A file filter and picker. | ||
├─formatMenu.cpp // A menu for choosing the input format. | ||
├─decode.cpp // Methods for decoding pattern formats. | ||
└─pattern_files/ | ||
``` | ||
</details><details><summary>Build Instructions</summary> | ||
|
||
\ | ||
1- Download the source code: | ||
```sh | ||
wget https://github.com/3m4r5/cliArcade/archive/refs/heads/main.zip | ||
``` | ||
2- unzip the downloaded file: | ||
```sh | ||
unzip main.zip | ||
``` | ||
and optionally remove the compressed archive: | ||
```sh | ||
rm main.zip | ||
``` | ||
3- cd into the main directory: | ||
```sh | ||
cd cliArcade-main/ | ||
``` | ||
4- compile: | ||
```sh | ||
g++ main.cpp -o cliArcade | ||
``` | ||
or | ||
```sh | ||
clang++ main.cpp -o cliArcade | ||
``` | ||
5- run the executable: | ||
```sh | ||
./cliArcade | ||
``` | ||
</details><details><summary>Roadmap</summary> | ||
|
||
Here's several improvements that could be made: | ||
- Support more platforms (windows, macos). | ||
- Add more games like tetris or pacman. | ||
- Support more [file formats](https://conwaylife.com/wiki/File_formats): | ||
- [x] .cells | ||
- [x] .rle | ||
- [ ] life 1.05 | ||
- [ ] life 1.06 | ||
- [ ] apgcode | ||
- [ ] .mc | ||
- [ ] .mcl | ||
- [ ] .plf | ||
- [ ] .l | ||
- [ ] .rule | ||
|
||
- [store neighbors count for each cell](https://youtu.be/dAfWKmKF34). | ||
- Add support for color theming using ansii escape codes. | ||
- Utilize the gpu for parallelization. | ||
</details> |
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,82 @@ | ||
#pragma once | ||
#include "terminal.cpp" | ||
#include <math.h> | ||
using namespace std; | ||
|
||
class baseMenu{ | ||
protected: | ||
int titleHeight = 1, | ||
state = 0, | ||
cardWidth = 2, cardHeight = 2, | ||
gridWidth = 1, | ||
hMargin = 1, vMargin = 1, | ||
hPad = 1, vPad = 1; | ||
wstring title; | ||
vector<vector<wstring>> cardsContent; | ||
vector<wstring> emptyMenu; | ||
void init(){ | ||
createCards(); | ||
int menuWidth = (cardWidth + hMargin) * gridWidth + hMargin; | ||
emptyMenu.emplace_back(pad(title, menuWidth)); | ||
for (int i = 1; i < vMargin; i++) emptyMenu.emplace_back(repeat(L" ", menuWidth)); | ||
for (int i = 0, len = ceil(1.0 * cards.size() / gridWidth); i < len; i++){ | ||
for (auto j: row(i)) emptyMenu.emplace_back(j); | ||
for (int i = 0; i < vMargin; i++) emptyMenu.emplace_back(repeat(L" ", menuWidth)); | ||
} | ||
} | ||
void setFinalState(){ | ||
while (1){ | ||
state = max(0, min(state, int(cardsContent.size() - 1))); | ||
draw(); | ||
switch (terminal::getKey()){ | ||
case 'A': state -= gridWidth; break; | ||
case 'B': state += gridWidth; break; | ||
case 'C': state++; break; | ||
case 'D': state--; break; | ||
case ' ': | ||
case '\r': return; | ||
case 'x': | ||
case 'c': terminal::exit(); | ||
} | ||
} | ||
} | ||
private: | ||
vector<vector<wstring>> cards; | ||
wstring repeat(wstring s, int times){ | ||
wstring r; | ||
while (times--) r += s; | ||
return r; | ||
} | ||
wstring pad(wstring s, int width){ | ||
int padding = width - s.size(); | ||
return repeat(L" ", padding / 2) + s + repeat(L" ", ceil(padding / 2.0)); | ||
} | ||
void draw(){ | ||
terminal::printBuffer = emptyMenu; | ||
int left = hMargin + (state % gridWidth) * (cardWidth + hMargin), top = titleHeight + (state / gridWidth) * (cardHeight + vMargin); | ||
terminal::highlightArea(left, left + cardWidth - 1, top, top + cardHeight - 1); | ||
terminal::print(); | ||
} | ||
void createCards(){ | ||
for (int j = 0, len = cardsContent.size(); j < len; j++){ | ||
vector<wstring> card; | ||
card.emplace_back(L"╭" + repeat(L"─", cardWidth - 2) + L"╮"); | ||
for (int i = 0; i < vPad; i++) card.emplace_back(L"│" + repeat(L" ", cardWidth - 2) + L"│"); | ||
for (auto i: cardsContent[j])card.emplace_back(L"│" + pad(i, cardWidth - 2) + L"│"); | ||
for (int i = 0; i < vPad; i++) card.emplace_back(L"│" + repeat(L" ", cardWidth - 2) + L"│"); | ||
card.emplace_back(L"╰" + repeat(L"─", cardWidth - 2) + L"╯"); | ||
cards.emplace_back(card); | ||
} | ||
} | ||
vector<wstring> row(int i){ | ||
vector<wstring> cardRow; | ||
int firstCard = i * gridWidth, lastCard = min(firstCard + gridWidth, int(cardsContent.size())); | ||
for (int i = 0; i < cardHeight; i++){ | ||
cardRow.emplace_back(repeat(L" ", hMargin)); | ||
for (int j = firstCard; j < lastCard; j++){ | ||
cardRow.back() += cards[j][i] + repeat(L" ", hMargin); | ||
} | ||
} | ||
return cardRow; | ||
} | ||
}; |
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 @@ | ||
#include "games/gameOfLife/gameOfLife.cpp" | ||
#include "games/snake/snake.cpp" |
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,95 @@ | ||
#pragma once | ||
#include "../../terminal.cpp" | ||
using namespace std; | ||
#define padding 3 | ||
|
||
struct pattern { | ||
vector<vector<bool>> grid; | ||
vector<int> birth = {3}; | ||
vector<int> survive = {2, 3}; | ||
}; | ||
|
||
class decode { | ||
public: | ||
static pattern cells(string patternCode){ | ||
return text(patternCode, 'O', '.', '!'); | ||
} | ||
static pattern rle(string patternCode) { | ||
while (patternCode[0] == '#') patternCode = patternCode.substr(patternCode.find('\n') + 1); | ||
pattern p; | ||
int x = 0, y = 0; | ||
size_t pos = patternCode.find("\n"); | ||
if (pos == string::npos) { | ||
wcout << "Invalid RLE: Missing header"; | ||
terminal::exit(); | ||
} | ||
string header = patternCode.substr(0, pos++); | ||
size_t xPos = header.find("x = "), | ||
yPos = header.find("y = "), | ||
rulePos = header.find("rule = "); | ||
if (xPos == string::npos || yPos == string::npos) { | ||
wcout << "Invalid RLE: Missing dimensions"; | ||
terminal::exit(); | ||
} | ||
x = stoi(header.substr(xPos + 4)); | ||
y = stoi(header.substr(yPos + 4, header.find(',', yPos) - yPos - 4)); | ||
p.grid.resize(y, vector<bool>(x, 0)); | ||
if (rulePos != string::npos) { | ||
p.birth = {}, p.survive = {}; | ||
string ruleStr = header.substr(rulePos + 7); | ||
size_t slashPos = ruleStr.find('/'); | ||
string birthStr = ruleStr.substr(0, slashPos); | ||
string surviveStr = ruleStr.substr(slashPos + 1); | ||
if (!isdigit(birthStr[0])) birthStr = birthStr.substr(1); | ||
if (!isdigit(surviveStr[0])) surviveStr = surviveStr.substr(1); | ||
else swap(birthStr, surviveStr); | ||
for (char c: birthStr) p.birth.emplace_back(c - 48); | ||
for (char c: surviveStr) if (isdigit(c)) p.survive.emplace_back(c - 48); | ||
} | ||
int currentRow = 0, | ||
currentCol = 0, | ||
count = 0; | ||
char c; | ||
for (; pos < patternCode.size() && patternCode[pos] != '!'; pos++) { | ||
c = patternCode[pos]; | ||
if (isdigit(c)) count = count * 10 + (c - 48); | ||
else { | ||
if (count == 0) count = 1; | ||
switch (c) { | ||
case 'b': for (int i = 0; i < count; i++) p.grid[currentRow][currentCol++] = 0; break; | ||
case 'o': for (int i = 0; i < count; i++) p.grid[currentRow][currentCol++] = 1; break; | ||
case '$': currentRow += count, currentCol = 0; break; | ||
} | ||
count = 0; | ||
} | ||
} | ||
pad(p, padding); | ||
return p; | ||
} | ||
private: | ||
static pattern text(string patternCode, char alive, const char dead, char comment){ | ||
while (patternCode[0] == comment) patternCode = patternCode.substr(patternCode.find('\n') + 1); | ||
int width = patternCode.find('\n') + 1; | ||
pattern p; | ||
p.grid.emplace_back(vector<bool>()); | ||
for (char c: patternCode){ | ||
if (c == dead) p.grid.back().emplace_back(0); | ||
else if (c == alive) p.grid.back().emplace_back(1); | ||
else if (c == '\n'){ | ||
p.grid.emplace_back(vector<bool>()); | ||
} | ||
} | ||
pad(p, padding); | ||
return p; | ||
} | ||
static void pad(pattern &p, int pad){ | ||
int maxWidth = 0; | ||
for (auto i: p.grid) maxWidth = max(maxWidth, int(i.size())); | ||
for (int j = 0; j < pad; j++) p.grid.emplace(p.grid.begin(), vector<bool>(maxWidth + 2 * pad, 0)); | ||
for (auto &i: p.grid) { | ||
for (int j = 0; j < pad; j++) i.emplace(i.begin(), 0); | ||
i.resize(maxWidth + 2 * pad, 0); | ||
} | ||
p.grid.resize(p.grid.size() + 2 * pad, vector<bool>(maxWidth + 2 * pad)); | ||
} | ||
}; |
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,73 @@ | ||
#include "../../baseMenu.cpp" | ||
#include "decode.cpp" | ||
#include <algorithm> | ||
#include <dirent.h> | ||
#include <fstream> | ||
#include <cstring> | ||
#include <sys/types.h> | ||
#include <sys/stat.h> | ||
|
||
using namespace std; | ||
|
||
class fileMenu : public baseMenu { | ||
public: | ||
vector<string> filePaths; | ||
fileMenu(){ | ||
int maxLen = 0; | ||
addFiles(".", 5); | ||
if (!filePaths.size()){ | ||
char c = '\0'; | ||
terminal::clear(); | ||
terminal::cursorShow(); | ||
wcout << "No supported pattern files detected in the current directory!\ | ||
\nDo want to explore some pattern files [y/n]? "; | ||
cin >> c; | ||
if (tolower(c) == 'y') system("xdg-open https://copy.sh/life/examples"); | ||
terminal::exit(); | ||
} | ||
for (string name: filePaths) { | ||
wstring wName(name.begin(), name.end()); | ||
vector<wstring> tmp; | ||
tmp.emplace_back(wName.substr(wName.rfind('/') + 1)); | ||
maxLen = max(maxLen, int(tmp.back().size())); | ||
cardsContent.emplace_back(tmp); | ||
} | ||
cardWidth = maxLen + 4, | ||
cardHeight = 5, | ||
gridWidth = max(2, min(3, 40 / maxLen)), | ||
title = L"Choose a file:"; | ||
init(); | ||
} | ||
pattern getPattern(){ | ||
setFinalState(); | ||
string fileName = filePaths[state], line , content = "", | ||
fileExtension = fileName.substr(fileName.rfind('.') + 1); | ||
ifstream file(fileName); | ||
pattern p; | ||
while (getline(file, line)) content += '\n' + line; | ||
content = content.substr(1); | ||
if (fileExtension == "rle") p = decode::rle(content); | ||
else if (fileExtension == "cells") p = decode::cells(content); | ||
return p; | ||
}; | ||
private: | ||
void addFiles(string path, int depth){ | ||
if (!depth) return; | ||
string extensions[] = {"rle", "cells"}; | ||
DIR *dir; | ||
dirent *entry; | ||
dir = opendir(path.c_str()); | ||
while ((entry = readdir(dir)) != nullptr) { | ||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; | ||
string fullPath = path + "/" + entry->d_name; | ||
struct stat entryInfo; | ||
if (stat(fullPath.c_str(), &entryInfo) == 0 && S_ISDIR(entryInfo.st_mode)) addFiles(fullPath, depth - 1); | ||
else { | ||
int dotIndex = fullPath.rfind('.'); | ||
if (dotIndex > 1 && find(extensions, end(extensions), fullPath.substr(dotIndex + 1)) != end(extensions)) | ||
filePaths.emplace_back(fullPath); | ||
} | ||
} | ||
closedir(dir); | ||
} | ||
}; |
Oops, something went wrong.