Skip to content

Commit d3e2cef

Browse files
authored
Add files via upload
1 parent acf569c commit d3e2cef

15 files changed

+934
-0
lines changed

README.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# cliArcade
2+
> Play retro games in your linux terminal.
3+
4+
### Whats new?
5+
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.
6+
<details open><summary>video demos</summary>
7+
8+
> [!TIP]
9+
> 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).
10+
11+
|[![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|
12+
|:-:|:-:|
13+
|[![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
14+
15+
</details><details><summary>How does it work?</summary>
16+
17+
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:
18+
```cpp
19+
auto lookupArray = L" ▘▝▀▖▌▞▛▗▚▐▜▄▙▟█";
20+
for (int i = 0, h = height(); i < h; i += 2){
21+
printBuffer.emplace_back(L"");
22+
for (int j = 0, w = width(); j < w; j += 2)
23+
printBuffer.back() += lookupArray[
24+
rawBuffer[i][j]
25+
| rawBuffer[i][j + 1] << 1
26+
| rawBuffer[i + 1][j] << 2
27+
| rawBuffer[i + 1][j + 1] << 3
28+
];
29+
}
30+
```
31+
</details><details><summary>Project File Structure</summary>
32+
33+
> [!NOTE]
34+
> The entire project results in a single object file because this project is small and the compile time is not an issue.
35+
36+
```
37+
cliArcade
38+
├─README.md
39+
├─main.cpp
40+
├─terminal.cpp // Utilities for interacting with the terminal.
41+
├─baseMenu.cpp // A base class for creating a selection menu.
42+
├─mainMenu.cpp // A menu for choosing the game.
43+
├─resolutionMenu.cpp // A menu for choosing the resolution.
44+
├─games.h // Included games.
45+
└─games
46+
├─snake
47+
│ └─snake.cpp
48+
└─gameOfLife
49+
├─gameOfLife.cpp
50+
├─patternMenu.cpp // A menu to choose pattern importing method.
51+
├─fileMenu.cpp // A file filter and picker.
52+
├─formatMenu.cpp // A menu for choosing the input format.
53+
├─decode.cpp // Methods for decoding pattern formats.
54+
└─pattern_files/
55+
```
56+
</details><details><summary>Build Instructions</summary>
57+
58+
\
59+
1- Download the source code:
60+
```sh
61+
wget https://github.com/3m4r5/cliArcade/archive/refs/heads/main.zip
62+
```
63+
2- unzip the downloaded file:
64+
```sh
65+
unzip main.zip
66+
```
67+
and optionally remove the compressed archive:
68+
```sh
69+
rm main.zip
70+
```
71+
3- cd into the main directory:
72+
```sh
73+
cd cliArcade-main/
74+
```
75+
4- compile:
76+
```sh
77+
g++ main.cpp -o cliArcade
78+
```
79+
or
80+
```sh
81+
clang++ main.cpp -o cliArcade
82+
```
83+
5- run the executable:
84+
```sh
85+
./cliArcade
86+
```
87+
</details><details><summary>Roadmap</summary>
88+
89+
Here's several improvements that could be made:
90+
- Support more platforms (windows, macos).
91+
- Add more games like tetris or pacman.
92+
- Support more [file formats](https://conwaylife.com/wiki/File_formats):
93+
- [x] .cells
94+
- [x] .rle
95+
- [ ] life 1.05
96+
- [ ] life 1.06
97+
- [ ] apgcode
98+
- [ ] .mc
99+
- [ ] .mcl
100+
- [ ] .plf
101+
- [ ] .l
102+
- [ ] .rule
103+
104+
- [store neighbors count for each cell](https://youtu.be/dAfWKmKF34).
105+
- Add support for color theming using ansii escape codes.
106+
- Utilize the gpu for parallelization.
107+
</details>

baseMenu.cpp

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#pragma once
2+
#include "terminal.cpp"
3+
#include <math.h>
4+
using namespace std;
5+
6+
class baseMenu{
7+
protected:
8+
int titleHeight = 1,
9+
state = 0,
10+
cardWidth = 2, cardHeight = 2,
11+
gridWidth = 1,
12+
hMargin = 1, vMargin = 1,
13+
hPad = 1, vPad = 1;
14+
wstring title;
15+
vector<vector<wstring>> cardsContent;
16+
vector<wstring> emptyMenu;
17+
void init(){
18+
createCards();
19+
int menuWidth = (cardWidth + hMargin) * gridWidth + hMargin;
20+
emptyMenu.emplace_back(pad(title, menuWidth));
21+
for (int i = 1; i < vMargin; i++) emptyMenu.emplace_back(repeat(L" ", menuWidth));
22+
for (int i = 0, len = ceil(1.0 * cards.size() / gridWidth); i < len; i++){
23+
for (auto j: row(i)) emptyMenu.emplace_back(j);
24+
for (int i = 0; i < vMargin; i++) emptyMenu.emplace_back(repeat(L" ", menuWidth));
25+
}
26+
}
27+
void setFinalState(){
28+
while (1){
29+
state = max(0, min(state, int(cardsContent.size() - 1)));
30+
draw();
31+
switch (terminal::getKey()){
32+
case 'A': state -= gridWidth; break;
33+
case 'B': state += gridWidth; break;
34+
case 'C': state++; break;
35+
case 'D': state--; break;
36+
case ' ':
37+
case '\r': return;
38+
case 'x':
39+
case 'c': terminal::exit();
40+
}
41+
}
42+
}
43+
private:
44+
vector<vector<wstring>> cards;
45+
wstring repeat(wstring s, int times){
46+
wstring r;
47+
while (times--) r += s;
48+
return r;
49+
}
50+
wstring pad(wstring s, int width){
51+
int padding = width - s.size();
52+
return repeat(L" ", padding / 2) + s + repeat(L" ", ceil(padding / 2.0));
53+
}
54+
void draw(){
55+
terminal::printBuffer = emptyMenu;
56+
int left = hMargin + (state % gridWidth) * (cardWidth + hMargin), top = titleHeight + (state / gridWidth) * (cardHeight + vMargin);
57+
terminal::highlightArea(left, left + cardWidth - 1, top, top + cardHeight - 1);
58+
terminal::print();
59+
}
60+
void createCards(){
61+
for (int j = 0, len = cardsContent.size(); j < len; j++){
62+
vector<wstring> card;
63+
card.emplace_back(L"" + repeat(L"", cardWidth - 2) + L"");
64+
for (int i = 0; i < vPad; i++) card.emplace_back(L"" + repeat(L" ", cardWidth - 2) + L"");
65+
for (auto i: cardsContent[j])card.emplace_back(L"" + pad(i, cardWidth - 2) + L"");
66+
for (int i = 0; i < vPad; i++) card.emplace_back(L"" + repeat(L" ", cardWidth - 2) + L"");
67+
card.emplace_back(L"" + repeat(L"", cardWidth - 2) + L"");
68+
cards.emplace_back(card);
69+
}
70+
}
71+
vector<wstring> row(int i){
72+
vector<wstring> cardRow;
73+
int firstCard = i * gridWidth, lastCard = min(firstCard + gridWidth, int(cardsContent.size()));
74+
for (int i = 0; i < cardHeight; i++){
75+
cardRow.emplace_back(repeat(L" ", hMargin));
76+
for (int j = firstCard; j < lastCard; j++){
77+
cardRow.back() += cards[j][i] + repeat(L" ", hMargin);
78+
}
79+
}
80+
return cardRow;
81+
}
82+
};

games.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#include "games/gameOfLife/gameOfLife.cpp"
2+
#include "games/snake/snake.cpp"

games/gameOfLife/decode.cpp

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#pragma once
2+
#include "../../terminal.cpp"
3+
using namespace std;
4+
#define padding 3
5+
6+
struct pattern {
7+
vector<vector<bool>> grid;
8+
vector<int> birth = {3};
9+
vector<int> survive = {2, 3};
10+
};
11+
12+
class decode {
13+
public:
14+
static pattern cells(string patternCode){
15+
return text(patternCode, 'O', '.', '!');
16+
}
17+
static pattern rle(string patternCode) {
18+
while (patternCode[0] == '#') patternCode = patternCode.substr(patternCode.find('\n') + 1);
19+
pattern p;
20+
int x = 0, y = 0;
21+
size_t pos = patternCode.find("\n");
22+
if (pos == string::npos) {
23+
wcout << "Invalid RLE: Missing header";
24+
terminal::exit();
25+
}
26+
string header = patternCode.substr(0, pos++);
27+
size_t xPos = header.find("x = "),
28+
yPos = header.find("y = "),
29+
rulePos = header.find("rule = ");
30+
if (xPos == string::npos || yPos == string::npos) {
31+
wcout << "Invalid RLE: Missing dimensions";
32+
terminal::exit();
33+
}
34+
x = stoi(header.substr(xPos + 4));
35+
y = stoi(header.substr(yPos + 4, header.find(',', yPos) - yPos - 4));
36+
p.grid.resize(y, vector<bool>(x, 0));
37+
if (rulePos != string::npos) {
38+
p.birth = {}, p.survive = {};
39+
string ruleStr = header.substr(rulePos + 7);
40+
size_t slashPos = ruleStr.find('/');
41+
string birthStr = ruleStr.substr(0, slashPos);
42+
string surviveStr = ruleStr.substr(slashPos + 1);
43+
if (!isdigit(birthStr[0])) birthStr = birthStr.substr(1);
44+
if (!isdigit(surviveStr[0])) surviveStr = surviveStr.substr(1);
45+
else swap(birthStr, surviveStr);
46+
for (char c: birthStr) p.birth.emplace_back(c - 48);
47+
for (char c: surviveStr) if (isdigit(c)) p.survive.emplace_back(c - 48);
48+
}
49+
int currentRow = 0,
50+
currentCol = 0,
51+
count = 0;
52+
char c;
53+
for (; pos < patternCode.size() && patternCode[pos] != '!'; pos++) {
54+
c = patternCode[pos];
55+
if (isdigit(c)) count = count * 10 + (c - 48);
56+
else {
57+
if (count == 0) count = 1;
58+
switch (c) {
59+
case 'b': for (int i = 0; i < count; i++) p.grid[currentRow][currentCol++] = 0; break;
60+
case 'o': for (int i = 0; i < count; i++) p.grid[currentRow][currentCol++] = 1; break;
61+
case '$': currentRow += count, currentCol = 0; break;
62+
}
63+
count = 0;
64+
}
65+
}
66+
pad(p, padding);
67+
return p;
68+
}
69+
private:
70+
static pattern text(string patternCode, char alive, const char dead, char comment){
71+
while (patternCode[0] == comment) patternCode = patternCode.substr(patternCode.find('\n') + 1);
72+
int width = patternCode.find('\n') + 1;
73+
pattern p;
74+
p.grid.emplace_back(vector<bool>());
75+
for (char c: patternCode){
76+
if (c == dead) p.grid.back().emplace_back(0);
77+
else if (c == alive) p.grid.back().emplace_back(1);
78+
else if (c == '\n'){
79+
p.grid.emplace_back(vector<bool>());
80+
}
81+
}
82+
pad(p, padding);
83+
return p;
84+
}
85+
static void pad(pattern &p, int pad){
86+
int maxWidth = 0;
87+
for (auto i: p.grid) maxWidth = max(maxWidth, int(i.size()));
88+
for (int j = 0; j < pad; j++) p.grid.emplace(p.grid.begin(), vector<bool>(maxWidth + 2 * pad, 0));
89+
for (auto &i: p.grid) {
90+
for (int j = 0; j < pad; j++) i.emplace(i.begin(), 0);
91+
i.resize(maxWidth + 2 * pad, 0);
92+
}
93+
p.grid.resize(p.grid.size() + 2 * pad, vector<bool>(maxWidth + 2 * pad));
94+
}
95+
};

games/gameOfLife/fileMenu.cpp

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#include "../../baseMenu.cpp"
2+
#include "decode.cpp"
3+
#include <algorithm>
4+
#include <dirent.h>
5+
#include <fstream>
6+
#include <cstring>
7+
#include <sys/types.h>
8+
#include <sys/stat.h>
9+
10+
using namespace std;
11+
12+
class fileMenu : public baseMenu {
13+
public:
14+
vector<string> filePaths;
15+
fileMenu(){
16+
int maxLen = 0;
17+
addFiles(".", 5);
18+
if (!filePaths.size()){
19+
char c = '\0';
20+
terminal::clear();
21+
terminal::cursorShow();
22+
wcout << "No supported pattern files detected in the current directory!\
23+
\nDo want to explore some pattern files [y/n]? ";
24+
cin >> c;
25+
if (tolower(c) == 'y') system("xdg-open https://copy.sh/life/examples");
26+
terminal::exit();
27+
}
28+
for (string name: filePaths) {
29+
wstring wName(name.begin(), name.end());
30+
vector<wstring> tmp;
31+
tmp.emplace_back(wName.substr(wName.rfind('/') + 1));
32+
maxLen = max(maxLen, int(tmp.back().size()));
33+
cardsContent.emplace_back(tmp);
34+
}
35+
cardWidth = maxLen + 4,
36+
cardHeight = 5,
37+
gridWidth = max(2, min(3, 40 / maxLen)),
38+
title = L"Choose a file:";
39+
init();
40+
}
41+
pattern getPattern(){
42+
setFinalState();
43+
string fileName = filePaths[state], line , content = "",
44+
fileExtension = fileName.substr(fileName.rfind('.') + 1);
45+
ifstream file(fileName);
46+
pattern p;
47+
while (getline(file, line)) content += '\n' + line;
48+
content = content.substr(1);
49+
if (fileExtension == "rle") p = decode::rle(content);
50+
else if (fileExtension == "cells") p = decode::cells(content);
51+
return p;
52+
};
53+
private:
54+
void addFiles(string path, int depth){
55+
if (!depth) return;
56+
string extensions[] = {"rle", "cells"};
57+
DIR *dir;
58+
dirent *entry;
59+
dir = opendir(path.c_str());
60+
while ((entry = readdir(dir)) != nullptr) {
61+
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
62+
string fullPath = path + "/" + entry->d_name;
63+
struct stat entryInfo;
64+
if (stat(fullPath.c_str(), &entryInfo) == 0 && S_ISDIR(entryInfo.st_mode)) addFiles(fullPath, depth - 1);
65+
else {
66+
int dotIndex = fullPath.rfind('.');
67+
if (dotIndex > 1 && find(extensions, end(extensions), fullPath.substr(dotIndex + 1)) != end(extensions))
68+
filePaths.emplace_back(fullPath);
69+
}
70+
}
71+
closedir(dir);
72+
}
73+
};

0 commit comments

Comments
 (0)