Skip to content

Commit

Permalink
Add files via upload
Browse files Browse the repository at this point in the history
  • Loading branch information
3m4r5 authored Jun 7, 2024
1 parent acf569c commit d3e2cef
Show file tree
Hide file tree
Showing 15 changed files with 934 additions and 0 deletions.
107 changes: 107 additions & 0 deletions README.md
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>
82 changes: 82 additions & 0 deletions baseMenu.cpp
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;
}
};
2 changes: 2 additions & 0 deletions games.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#include "games/gameOfLife/gameOfLife.cpp"
#include "games/snake/snake.cpp"
95 changes: 95 additions & 0 deletions games/gameOfLife/decode.cpp
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));
}
};
73 changes: 73 additions & 0 deletions games/gameOfLife/fileMenu.cpp
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);
}
};
Loading

0 comments on commit d3e2cef

Please sign in to comment.