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 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
@@ -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,4 @@
1
+ from .core import create_engine
2
+ from .screen import create_buffer, draw_char, draw_string, clear_buffer, render_buffer
3
+ from .storage import create_history, record_state, undo_state
4
+ from . import colors
@@ -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"]
@@ -0,0 +1,6 @@
1
+ from funcade.screen import draw_string
2
+ from funcade import colors
3
+
4
+ def render_game(state, buffer):
5
+ draw_string(buffer, 15, 7, "CONSTRUCTOR IS READY!", colors.GREEN + colors.BOLD)
6
+ draw_string(buffer, 13, 9, "Write your game here...", colors.WHITE)
funcade-0.1.0/state.py ADDED
@@ -0,0 +1,5 @@
1
+ def init_game():
2
+ return {
3
+ "score": 0,
4
+ "game_over": False
5
+ }
@@ -0,0 +1,6 @@
1
+ def update_game(state, key, dt):
2
+
3
+ if key == 'ESCAPE':
4
+ state["game_over"] = True
5
+
6
+ return state