funcade 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- funcade-0.1.0/LICENSE +21 -0
- funcade-0.1.0/PKG-INFO +36 -0
- funcade-0.1.0/README.md +24 -0
- funcade-0.1.0/funcade/__init__.py +4 -0
- funcade-0.1.0/funcade/__main__.py +85 -0
- funcade-0.1.0/funcade/colors.py +23 -0
- funcade-0.1.0/funcade/core.py +41 -0
- funcade-0.1.0/funcade/input.py +49 -0
- funcade-0.1.0/funcade/screen.py +45 -0
- funcade-0.1.0/funcade/storage.py +28 -0
- funcade-0.1.0/main.py +31 -0
- funcade-0.1.0/pyproject.toml +24 -0
- funcade-0.1.0/render.py +6 -0
- funcade-0.1.0/state.py +5 -0
- funcade-0.1.0/update.py +6 -0
funcade-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dursun
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
funcade-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: funcade
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Functional console game engine
|
|
5
|
+
Author-email: Dursun <dursunhasanov06@gmail.com>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# funcade 🎮
|
|
14
|
+
|
|
15
|
+
A lightweight, functional console game engine for Python. Build retro 2D grid-based terminal games with ease!
|
|
16
|
+
|
|
17
|
+
`funcade` takes care of console rendering, keyboard input handling, ANSI colors, and game loop stabilization, leaving you to focus purely on your game logic. It also features a built-in project generator called **Golem**.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 🚀 Key Features
|
|
22
|
+
|
|
23
|
+
* **Pure Functional Approach**: Separate your game into `state` (data), `update` (pure logic), and `render` (visuals).
|
|
24
|
+
* **Zero Dependencies**: Uses standard Python libraries and terminal ANSI escape sequences.
|
|
25
|
+
* **Window Management**: Automatically runs games in a dedicated, perfectly sized Windows console window.
|
|
26
|
+
* **Built-in Time Machine**: Built-in state history features (`undo_state`) to easily implement "rewind time" mechanics.
|
|
27
|
+
* **Golem CLI**: Jumpstart new game projects in seconds.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 🛠Installation
|
|
32
|
+
|
|
33
|
+
You can install `funcade` directly from PyPI:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install funcade
|
funcade-0.1.0/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# funcade 🎮
|
|
2
|
+
|
|
3
|
+
A lightweight, functional console game engine for Python. Build retro 2D grid-based terminal games with ease!
|
|
4
|
+
|
|
5
|
+
`funcade` takes care of console rendering, keyboard input handling, ANSI colors, and game loop stabilization, leaving you to focus purely on your game logic. It also features a built-in project generator called **Golem**.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🚀 Key Features
|
|
10
|
+
|
|
11
|
+
* **Pure Functional Approach**: Separate your game into `state` (data), `update` (pure logic), and `render` (visuals).
|
|
12
|
+
* **Zero Dependencies**: Uses standard Python libraries and terminal ANSI escape sequences.
|
|
13
|
+
* **Window Management**: Automatically runs games in a dedicated, perfectly sized Windows console window.
|
|
14
|
+
* **Built-in Time Machine**: Built-in state history features (`undo_state`) to easily implement "rewind time" mechanics.
|
|
15
|
+
* **Golem CLI**: Jumpstart new game projects in seconds.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🛠Installation
|
|
20
|
+
|
|
21
|
+
You can install `funcade` directly from PyPI:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install funcade
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
TEMPLATE_STATE = """def init_game():
|
|
5
|
+
return {
|
|
6
|
+
"score": 0,
|
|
7
|
+
"game_over": False
|
|
8
|
+
}
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
TEMPLATE_UPDATE = """def update_game(state, action, dt):
|
|
12
|
+
if action == 'ESCAPE':
|
|
13
|
+
state["game_over"] = True
|
|
14
|
+
return state
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
TEMPLATE_RENDER = """from funcade.screen import draw_string
|
|
18
|
+
from funcade import colors
|
|
19
|
+
|
|
20
|
+
def render_game(state, buffer):
|
|
21
|
+
draw_string(buffer, 15, 7, "CONSTRUCTOR IS READY!", colors.GREEN + colors.BOLD)
|
|
22
|
+
draw_string(buffer, 13, 9, "Write your game here...", colors.WHITE)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
TEMPLATE_MAIN = """import sys
|
|
26
|
+
import os
|
|
27
|
+
|
|
28
|
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
29
|
+
|
|
30
|
+
from funcade import create_engine
|
|
31
|
+
from state import init_game
|
|
32
|
+
from update import update_game
|
|
33
|
+
from render import render_game
|
|
34
|
+
|
|
35
|
+
def main():
|
|
36
|
+
run_game = create_engine(
|
|
37
|
+
init_fn=init_game,
|
|
38
|
+
update_fn=update_game,
|
|
39
|
+
render_fn=render_game,
|
|
40
|
+
width=50,
|
|
41
|
+
height=15,
|
|
42
|
+
fps=20
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
run_game()
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
main()
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def create_project(project_name):
|
|
52
|
+
if os.path.exists(project_name):
|
|
53
|
+
print(f"Error: Path '{project_name}' already exists!")
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
|
|
56
|
+
print(f"Building Golem to activate '{project_name}'...")
|
|
57
|
+
os.makedirs(project_name, exist_ok=True)
|
|
58
|
+
|
|
59
|
+
files = {
|
|
60
|
+
"state.py": TEMPLATE_STATE,
|
|
61
|
+
"update.py": TEMPLATE_UPDATE,
|
|
62
|
+
"render.py": TEMPLATE_RENDER,
|
|
63
|
+
"main.py": TEMPLATE_MAIN
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for filename, content in files.items():
|
|
67
|
+
filepath = os.path.join(project_name, filename)
|
|
68
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
69
|
+
f.write(content)
|
|
70
|
+
print(f" [+] Created {filepath}")
|
|
71
|
+
|
|
72
|
+
print(f"\\nGolem successfully awoken! Play your new game by running:")
|
|
73
|
+
print(f" cd {project_name}")
|
|
74
|
+
print(f" python main.py")
|
|
75
|
+
|
|
76
|
+
def main():
|
|
77
|
+
if len(sys.argv) < 3 or sys.argv[1] != 'golem':
|
|
78
|
+
print("Usage: python -m funcade golem <project_name>")
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
|
|
81
|
+
project_name = sys.argv[2]
|
|
82
|
+
create_project(project_name)
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
main()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# ANSI escape sequences for coloring console output
|
|
2
|
+
RESET = "\033[0m"
|
|
3
|
+
BOLD = "\033[1m"
|
|
4
|
+
|
|
5
|
+
# Foregrounds
|
|
6
|
+
BLACK = "\033[30m"
|
|
7
|
+
RED = "\033[31m"
|
|
8
|
+
GREEN = "\033[32m"
|
|
9
|
+
YELLOW = "\033[33m"
|
|
10
|
+
BLUE = "\033[34m"
|
|
11
|
+
MAGENTA = "\033[35m"
|
|
12
|
+
CYAN = "\033[36m"
|
|
13
|
+
WHITE = "\033[37m"
|
|
14
|
+
|
|
15
|
+
# Backgrounds
|
|
16
|
+
BG_BLACK = "\033[40m"
|
|
17
|
+
BG_RED = "\033[41m"
|
|
18
|
+
BG_GREEN = "\033[42m"
|
|
19
|
+
BG_YELLOW = "\033[43m"
|
|
20
|
+
BG_BLUE = "\033[44m"
|
|
21
|
+
BG_MAGENTA = "\033[45m"
|
|
22
|
+
BG_CYAN = "\033[46m"
|
|
23
|
+
BG_WHITE = "\033[47m"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
from .input import get_key
|
|
4
|
+
from .screen import create_buffer, clear_buffer, render_buffer
|
|
5
|
+
|
|
6
|
+
def create_engine(init_fn, update_fn, render_fn, width=50, height=15, fps=20):
|
|
7
|
+
def start():
|
|
8
|
+
sys.stdout.write("\033[2J\033[H\033[?25l")
|
|
9
|
+
sys.stdout.flush()
|
|
10
|
+
|
|
11
|
+
state = init_fn()
|
|
12
|
+
buffer = create_buffer(width, height)
|
|
13
|
+
dt = 1 / fps
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
while True:
|
|
17
|
+
start_time = time.time()
|
|
18
|
+
|
|
19
|
+
key = get_key()
|
|
20
|
+
if key == 'ESCAPE':
|
|
21
|
+
break
|
|
22
|
+
|
|
23
|
+
next_state = update_fn(state, key, dt)
|
|
24
|
+
if next_state is None:
|
|
25
|
+
break
|
|
26
|
+
state = next_state
|
|
27
|
+
|
|
28
|
+
clear_buffer(buffer)
|
|
29
|
+
render_fn(state, buffer)
|
|
30
|
+
|
|
31
|
+
render_buffer(buffer)
|
|
32
|
+
|
|
33
|
+
elapsed = time.time() - start_time
|
|
34
|
+
sleep_time = dt - elapsed
|
|
35
|
+
if sleep_time > 0:
|
|
36
|
+
time.sleep(sleep_time)
|
|
37
|
+
finally:
|
|
38
|
+
sys.stdout.write("\033[2J\033[H\033[?25h")
|
|
39
|
+
sys.stdout.flush()
|
|
40
|
+
|
|
41
|
+
return start
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
if os.name == 'nt':
|
|
5
|
+
import msvcrt
|
|
6
|
+
def get_key():
|
|
7
|
+
if msvcrt.kbhit():
|
|
8
|
+
ch = msvcrt.getch()
|
|
9
|
+
if ch in (b'\x00', b'\xe0'):
|
|
10
|
+
if msvcrt.kbhit():
|
|
11
|
+
ch2 = msvcrt.getch()
|
|
12
|
+
mapping = {b'H': 'UP', b'P': 'DOWN', b'K': 'LEFT', b'M': 'RIGHT'}
|
|
13
|
+
return mapping.get(ch2, None)
|
|
14
|
+
try:
|
|
15
|
+
key = ch.decode('utf-8').upper()
|
|
16
|
+
if key == '\r': return 'ENTER'
|
|
17
|
+
if key == '\x1b': return 'ESCAPE'
|
|
18
|
+
if key == ' ': return 'SPACE'
|
|
19
|
+
return key
|
|
20
|
+
except:
|
|
21
|
+
return None
|
|
22
|
+
return None
|
|
23
|
+
else:
|
|
24
|
+
import termios
|
|
25
|
+
import tty
|
|
26
|
+
import select
|
|
27
|
+
def get_key():
|
|
28
|
+
fd = sys.stdin.fileno()
|
|
29
|
+
old_settings = termios.tcgetattr(fd)
|
|
30
|
+
try:
|
|
31
|
+
tty.setraw(sys.stdin.fileno())
|
|
32
|
+
rlist, _, _ = select.select([sys.stdin], [], [], 0.0)
|
|
33
|
+
if rlist:
|
|
34
|
+
ch = sys.stdin.read(1)
|
|
35
|
+
if ch == '\x1b':
|
|
36
|
+
rlist2, _, _ = select.select([sys.stdin], [], [], 0.05)
|
|
37
|
+
if rlist2:
|
|
38
|
+
ch2 = sys.stdin.read(2)
|
|
39
|
+
if ch2 == '[A': return 'UP'
|
|
40
|
+
if ch2 == '[B': return 'DOWN'
|
|
41
|
+
if ch2 == '[C': return 'RIGHT'
|
|
42
|
+
if ch2 == '[D': return 'LEFT'
|
|
43
|
+
return 'ESCAPE'
|
|
44
|
+
if ch in ('\n', '\r'): return 'ENTER'
|
|
45
|
+
if ch == ' ': return 'SPACE'
|
|
46
|
+
return ch.upper()
|
|
47
|
+
finally:
|
|
48
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
49
|
+
return None
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from .colors import RESET
|
|
3
|
+
|
|
4
|
+
def create_buffer(width, height, default_char=' ', default_color=RESET):
|
|
5
|
+
return {
|
|
6
|
+
'width': width,
|
|
7
|
+
'height': height,
|
|
8
|
+
'grid': [[(default_char, default_color) for _ in range(width)] for _ in range(height)]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
def draw_char(buffer, x, y, char, color=RESET):
|
|
12
|
+
if 0 <= x < buffer['width'] and 0 <= y < buffer['height']:
|
|
13
|
+
buffer['grid'][y][x] = (char, color)
|
|
14
|
+
return buffer
|
|
15
|
+
|
|
16
|
+
def draw_string(buffer, x, y, text, color=RESET):
|
|
17
|
+
for i, char in enumerate(text):
|
|
18
|
+
draw_char(buffer, x + i, y, char, color)
|
|
19
|
+
return buffer
|
|
20
|
+
|
|
21
|
+
def clear_buffer(buffer, default_char=' ', default_color=RESET):
|
|
22
|
+
w, h = buffer['width'], buffer['height']
|
|
23
|
+
for y in range(h):
|
|
24
|
+
for x in range(w):
|
|
25
|
+
buffer['grid'][y][x] = (default_char, default_color)
|
|
26
|
+
return buffer
|
|
27
|
+
|
|
28
|
+
def render_buffer(buffer):
|
|
29
|
+
sys.stdout.write('\033[H')
|
|
30
|
+
|
|
31
|
+
lines = []
|
|
32
|
+
for y in range(buffer['height']):
|
|
33
|
+
row_str = []
|
|
34
|
+
last_color = None
|
|
35
|
+
for x in range(buffer['width']):
|
|
36
|
+
char, color = buffer['grid'][y][x]
|
|
37
|
+
if color != last_color:
|
|
38
|
+
row_str.append(color)
|
|
39
|
+
last_color = color
|
|
40
|
+
row_str.append(char)
|
|
41
|
+
row_str.append(RESET)
|
|
42
|
+
lines.append("".join(row_str))
|
|
43
|
+
|
|
44
|
+
sys.stdout.write("\n".join(lines) + "\n")
|
|
45
|
+
sys.stdout.flush()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
|
|
3
|
+
def create_history(max_depth=50):
|
|
4
|
+
return {
|
|
5
|
+
'history': [],
|
|
6
|
+
'max_depth': max_depth
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
def record_state(history_obj, state):
|
|
10
|
+
state_copy = copy.deepcopy(state)
|
|
11
|
+
new_history = list(history_obj['history'])
|
|
12
|
+
new_history.append(state_copy)
|
|
13
|
+
if len(new_history) > history_obj['max_depth']:
|
|
14
|
+
new_history.pop(0)
|
|
15
|
+
return {
|
|
16
|
+
**history_obj,
|
|
17
|
+
'history': new_history
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
def undo_state(history_obj):
|
|
21
|
+
if len(history_obj['history']) > 1:
|
|
22
|
+
new_history = list(history_obj['history'])
|
|
23
|
+
new_history.pop()
|
|
24
|
+
previous_state = new_history[-1]
|
|
25
|
+
return previous_state, {**history_obj, 'history': new_history}
|
|
26
|
+
elif len(history_obj['history']) == 1:
|
|
27
|
+
return history_obj['history'][0], history_obj
|
|
28
|
+
return None, history_obj
|
funcade-0.1.0/main.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
if os.name == 'nt' and '--external' not in sys.argv:
|
|
6
|
+
cmd = 'start "Game Constructor" cmd /c "mode con: cols=54 lines=18 && python main.py --external"'
|
|
7
|
+
subprocess.Popen(cmd, shell=True, close_fds=True)
|
|
8
|
+
sys.exit()
|
|
9
|
+
|
|
10
|
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".")))
|
|
11
|
+
|
|
12
|
+
from funcade import create_engine
|
|
13
|
+
|
|
14
|
+
from state import init_game
|
|
15
|
+
from update import update_game
|
|
16
|
+
from render import render_game
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
run_game = create_engine(
|
|
20
|
+
init_fn=init_game,
|
|
21
|
+
update_fn=update_game,
|
|
22
|
+
render_fn=render_game,
|
|
23
|
+
width=50,
|
|
24
|
+
height=15,
|
|
25
|
+
fps=20
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
run_game()
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
main()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "funcade"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Dursun", email = "dursunhasanov06@gmail.com" }
|
|
10
|
+
]
|
|
11
|
+
description = "Functional console game engine"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
funcade = "funcade.__main__:main"
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["funcade"]
|
funcade-0.1.0/render.py
ADDED
funcade-0.1.0/state.py
ADDED