olympus-engine 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nithil Gadde
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.
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: olympus-engine
3
+ Version: 0.1.0
4
+ Summary: A from-scratch 2D game engine for Python
5
+ Author: Nithil Gadde
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/nithilgadde/Olympus
8
+ Project-URL: Repository, https://github.com/nithilgadde/Olympus
9
+ Project-URL: Issues, https://github.com/nithilgadde/Olympus/issues
10
+ Keywords: game-engine,fantasy-console,pico-8,pixel-art,gamedev,retro,2d,sdl2
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Topic :: Games/Entertainment
14
+ Classifier: Topic :: Multimedia :: Graphics
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Operating System :: OS Independent
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: numpy
28
+ Requires-Dist: pysdl2
29
+ Requires-Dist: pysdl2-dll
30
+ Requires-Dist: pillow
31
+ Dynamic: license-file
32
+
33
+ # Olympus
34
+
35
+ A from-scratch 2D fantasy-console game engine for Python. Build tiny pixel games on a 128×128 screen with a fixed 16-color palette and a few lines of code.
36
+
37
+ ![Olympus demo](https://raw.githubusercontent.com/nithilgadde/Olympus/main/docs/demo.gif)
38
+
39
+ ## Try it
40
+
41
+ ```bash
42
+ pip install olympus-engine
43
+ olympus-editor # draw sprites with the mouse, export to code
44
+ ```
45
+
46
+ Then make a tiny game in about 15 lines — see **Quick start** below.
47
+
48
+ ## Quick start
49
+
50
+ Make a game in about 15 lines:
51
+
52
+ ```python
53
+ import olympus as ol
54
+
55
+ screen = ol.Screen(128, 128)
56
+ x, y = 60, 60
57
+
58
+ def update():
59
+ global x, y
60
+ if screen.btn(ol.LEFT): x -= 1
61
+ if screen.btn(ol.RIGHT): x += 1
62
+ if screen.btn(ol.UP): y -= 1
63
+ if screen.btn(ol.DOWN): y += 1
64
+
65
+ def draw():
66
+ screen.cls(ol.BLACK)
67
+ screen.rectfill(x, y, x + 7, y + 7, ol.RED)
68
+ screen.text("HELLO OLYMPUS", 18, 40, ol.WHITE)
69
+
70
+ screen.run(update, draw)
71
+ ```
72
+
73
+ You write `update` (game logic) and `draw` (pixels); the engine runs them 60 times a second and handles the window, input, and rendering.
74
+
75
+ ## Features
76
+
77
+ - **128×128 indexed framebuffer** with a fixed 16-color palette — the classic fantasy-console look.
78
+ - **Hand-written rasterizers**: pixels, lines (Bresenham), rectangles, and circles (midpoint), outlined or filled.
79
+ - **Sprites** you can hand-draw as text or load from a PNG — loaded images are automatically quantized down to the 16-color palette.
80
+ - **Animation, tilemaps, and AABB collision** — cycle frames for walk cycles, build levels as grids of characters, and check what's solid.
81
+ - **A built-in pixel font** for scores, labels, and menus.
82
+ - **Sound**: square-wave synthesis for retro blips, plus loading and playing your own `.wav` files.
83
+ - **A built-in sprite editor** (`olympus-editor`) — draw with the mouse and export straight to engine code, with undo, save/load, and onion-skinning.
84
+
85
+ ## Running locally
86
+
87
+ Requirements:
88
+
89
+ - **Python 3.9+**
90
+ - No system libraries to install by hand — `pysdl2-dll` ships the SDL2 binaries, and `pip` installs everything else (numpy, PySDL2, Pillow) automatically.
91
+
92
+ From the project root (the folder with `pyproject.toml`):
93
+
94
+ ```bash
95
+ python -m venv venv
96
+ source venv/bin/activate # Windows: venv\Scripts\activate
97
+ pip install -e .
98
+
99
+ python examples/demo.py # run the demo game
100
+ olympus-editor # open the sprite editor
101
+ ```
102
+
103
+ ## How it works
104
+
105
+ Olympus is "from scratch" in a specific, deliberate way: **no graphics or game library does the drawing.** The screen is a NumPy array of palette *indices* (not RGB), and every shape is rasterized by hand — Bresenham lines, midpoint circles, a sprite blitter, a pixel font, and square-wave audio synthesized sample by sample. SDL2 is only used to open a window, present the finished pixel buffer, and read input. It is **not** a wrapper around pygame.
106
+
107
+ A couple of decisions worth calling out:
108
+
109
+ - **The framebuffer stores color indices, not RGB.** Drawing just writes small integers (0–15) into a 2D array, which is fast and keeps the palette constraint baked into the design. The conversion to real RGB happens once per frame, with a single NumPy lookup, right before handing the buffer to SDL.
110
+ - **Loading a PNG means quantizing it.** Because the framebuffer is indexed, imported images can't be copied pixel-for-pixel — every pixel is snapped to its nearest palette color in a vectorized NumPy pass. That preserves the console's look no matter what art you throw at it.
111
+
112
+ ## Credits
113
+
114
+ Built with [PySDL2](https://github.com/py-sdl/py-sdl2) (windowing, input, audio output), [NumPy](https://numpy.org/) (the framebuffer and rasterization), and [Pillow](https://python-pillow.org/) (PNG decoding). Inspired by [PICO-8](https://www.lexaloffle.com/pico-8.php) and the fantasy-console idea.
115
+
116
+ Created by Nithil Gadde · MIT License
@@ -0,0 +1,84 @@
1
+ # Olympus
2
+
3
+ A from-scratch 2D fantasy-console game engine for Python. Build tiny pixel games on a 128×128 screen with a fixed 16-color palette and a few lines of code.
4
+
5
+ ![Olympus demo](https://raw.githubusercontent.com/nithilgadde/Olympus/main/docs/demo.gif)
6
+
7
+ ## Try it
8
+
9
+ ```bash
10
+ pip install olympus-engine
11
+ olympus-editor # draw sprites with the mouse, export to code
12
+ ```
13
+
14
+ Then make a tiny game in about 15 lines — see **Quick start** below.
15
+
16
+ ## Quick start
17
+
18
+ Make a game in about 15 lines:
19
+
20
+ ```python
21
+ import olympus as ol
22
+
23
+ screen = ol.Screen(128, 128)
24
+ x, y = 60, 60
25
+
26
+ def update():
27
+ global x, y
28
+ if screen.btn(ol.LEFT): x -= 1
29
+ if screen.btn(ol.RIGHT): x += 1
30
+ if screen.btn(ol.UP): y -= 1
31
+ if screen.btn(ol.DOWN): y += 1
32
+
33
+ def draw():
34
+ screen.cls(ol.BLACK)
35
+ screen.rectfill(x, y, x + 7, y + 7, ol.RED)
36
+ screen.text("HELLO OLYMPUS", 18, 40, ol.WHITE)
37
+
38
+ screen.run(update, draw)
39
+ ```
40
+
41
+ You write `update` (game logic) and `draw` (pixels); the engine runs them 60 times a second and handles the window, input, and rendering.
42
+
43
+ ## Features
44
+
45
+ - **128×128 indexed framebuffer** with a fixed 16-color palette — the classic fantasy-console look.
46
+ - **Hand-written rasterizers**: pixels, lines (Bresenham), rectangles, and circles (midpoint), outlined or filled.
47
+ - **Sprites** you can hand-draw as text or load from a PNG — loaded images are automatically quantized down to the 16-color palette.
48
+ - **Animation, tilemaps, and AABB collision** — cycle frames for walk cycles, build levels as grids of characters, and check what's solid.
49
+ - **A built-in pixel font** for scores, labels, and menus.
50
+ - **Sound**: square-wave synthesis for retro blips, plus loading and playing your own `.wav` files.
51
+ - **A built-in sprite editor** (`olympus-editor`) — draw with the mouse and export straight to engine code, with undo, save/load, and onion-skinning.
52
+
53
+ ## Running locally
54
+
55
+ Requirements:
56
+
57
+ - **Python 3.9+**
58
+ - No system libraries to install by hand — `pysdl2-dll` ships the SDL2 binaries, and `pip` installs everything else (numpy, PySDL2, Pillow) automatically.
59
+
60
+ From the project root (the folder with `pyproject.toml`):
61
+
62
+ ```bash
63
+ python -m venv venv
64
+ source venv/bin/activate # Windows: venv\Scripts\activate
65
+ pip install -e .
66
+
67
+ python examples/demo.py # run the demo game
68
+ olympus-editor # open the sprite editor
69
+ ```
70
+
71
+ ## How it works
72
+
73
+ Olympus is "from scratch" in a specific, deliberate way: **no graphics or game library does the drawing.** The screen is a NumPy array of palette *indices* (not RGB), and every shape is rasterized by hand — Bresenham lines, midpoint circles, a sprite blitter, a pixel font, and square-wave audio synthesized sample by sample. SDL2 is only used to open a window, present the finished pixel buffer, and read input. It is **not** a wrapper around pygame.
74
+
75
+ A couple of decisions worth calling out:
76
+
77
+ - **The framebuffer stores color indices, not RGB.** Drawing just writes small integers (0–15) into a 2D array, which is fast and keeps the palette constraint baked into the design. The conversion to real RGB happens once per frame, with a single NumPy lookup, right before handing the buffer to SDL.
78
+ - **Loading a PNG means quantizing it.** Because the framebuffer is indexed, imported images can't be copied pixel-for-pixel — every pixel is snapped to its nearest palette color in a vectorized NumPy pass. That preserves the console's look no matter what art you throw at it.
79
+
80
+ ## Credits
81
+
82
+ Built with [PySDL2](https://github.com/py-sdl/py-sdl2) (windowing, input, audio output), [NumPy](https://numpy.org/) (the framebuffer and rasterization), and [Pillow](https://python-pillow.org/) (PNG decoding). Inspired by [PICO-8](https://www.lexaloffle.com/pico-8.php) and the fantasy-console idea.
83
+
84
+ Created by Nithil Gadde · MIT License
@@ -0,0 +1,22 @@
1
+ from .screen import Screen, LEFT, RIGHT, UP, DOWN, O, X
2
+ from .palette import (
3
+ PALETTE, BLACK, DARKBLUE, DARKPURPLE, DARKGREEN, BROWN, DARKGREY,
4
+ LIGHTGREY, WHITE, RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, PINK, PEACH,
5
+ )
6
+ from .sprites import make_sprite, load_sprite
7
+ from .sound import Sound, load_wav
8
+ from .anim import Anim
9
+ from .tilemap import Tilemap, TILE
10
+ from .collide import overlap, hits_any
11
+ from .font import FONT
12
+
13
+ __version__ = "0.1.0"
14
+
15
+ __all__ = [
16
+ "Screen", "LEFT", "RIGHT", "UP", "DOWN", "O", "X",
17
+ "PALETTE", "BLACK", "DARKBLUE", "DARKPURPLE", "DARKGREEN", "BROWN",
18
+ "DARKGREY", "LIGHTGREY", "WHITE", "RED", "ORANGE", "YELLOW", "GREEN",
19
+ "BLUE", "INDIGO", "PINK", "PEACH",
20
+ "make_sprite", "load_sprite", "Sound", "load_wav", "Anim",
21
+ "Tilemap", "TILE", "overlap", "hits_any", "FONT",
22
+ ]
@@ -0,0 +1,20 @@
1
+ class Anim:
2
+ def __init__(self, frames, speed=6):
3
+ self.frames = frames
4
+ self.speed = speed
5
+ self.t = 0
6
+ self.i = 0
7
+
8
+ def reset(self):
9
+ self.t = 0
10
+ self.i = 0
11
+
12
+ def update(self):
13
+ self.t += 1
14
+ if self.t >= self.speed:
15
+ self.t = 0
16
+ self.i = (self.i + 1) % len(self.frames)
17
+
18
+ def frame(self):
19
+ return self.frames[self.i]
20
+
@@ -0,0 +1,10 @@
1
+ def overlap(a, b):
2
+ ax, ay, aw, ah, = a
3
+ bx, by, bw, bh = b
4
+ return (ax < bx + bw and ax + aw > bx and ay < by + bh and ay + ah > by)
5
+
6
+ def hits_any(box, boxes):
7
+ for b in boxes:
8
+ if overlap(box, b):
9
+ return True
10
+ return False
@@ -0,0 +1,191 @@
1
+ import ctypes
2
+ import sdl2
3
+ import numpy as np
4
+ from .screen import Screen
5
+
6
+ VW = VH = 128
7
+ SCALE = 5
8
+ GRID = 16
9
+ CANVAS = 96
10
+ CELL = CANVAS // GRID
11
+ CANVAS_X = CANVAS_Y = 4
12
+ PAL_X, PAL_Y, SW = 4, 104, 7
13
+ PV_X, PV_Y = 104, 4
14
+ TRANSPARENT = 255
15
+ SAVE_FILE = "sprite.sav"
16
+
17
+ grid = np.full((GRID, GRID), TRANSPARENT, dtype=np.uint8)
18
+ selected = 8
19
+ undo_stack = []
20
+ onion = None
21
+ show_onion = True
22
+
23
+ def in_canvas(mx, my):
24
+ return (CANVAS_X <= mx < CANVAS_X + GRID * CELL and CANVAS_Y <= my < CANVAS_Y + GRID * CELL)
25
+
26
+ def canvas_cell(mx, my):
27
+ return (my - CANVAS_Y) // CELL, (mx - CANVAS_X) // CELL
28
+
29
+ def palette_index(mx, my):
30
+ if PAL_Y <= my < PAL_Y + SW and mx >= PAL_X:
31
+ i = (mx - PAL_X) // SW
32
+ if 0 <= i < 16:
33
+ return i
34
+ return None
35
+
36
+ def grid_rows():
37
+ out = []
38
+ for r in range(GRID):
39
+ line = "".join("." if grid[r, c] == TRANSPARENT else format(grid[r, c], "x") for c in range(GRID))
40
+ out.append(line)
41
+ return out
42
+
43
+ def to_code():
44
+ body = "\n".join(' "%s",' % row for row in grid_rows())
45
+ return "make_sprite([\n" + body + "\n])"
46
+
47
+ def push_undo():
48
+ undo_stack.append(grid.copy())
49
+ if len(undo_stack) > 50:
50
+ undo_stack.pop(0)
51
+
52
+
53
+ def save_grid():
54
+ with open(SAVE_FILE, "w") as f:
55
+ f.write("\n".join(grid_rows()) + "\n")
56
+
57
+
58
+ def load_grid():
59
+ try:
60
+ with open(SAVE_FILE) as f:
61
+ lines = [ln.rstrip("\n") for ln in f if ln.strip()]
62
+ except FileNotFoundError:
63
+ return
64
+ push_undo()
65
+ grid[:, :] = TRANSPARENT
66
+ for r in range(min(GRID, len(lines))):
67
+ for c in range(min(GRID, len(lines[r]))):
68
+ ch = lines[r][c]
69
+ grid[r, c] = TRANSPARENT if ch == "." else int(ch, 16)
70
+
71
+
72
+ def draw(screen):
73
+ screen.cls(1) # dark-blue background = grid lines
74
+ # canvas
75
+ for r in range(GRID):
76
+ for c in range(GRID):
77
+ x = CANVAS_X + c * CELL
78
+ y = CANVAS_Y + r * CELL
79
+ v = grid[r, c]
80
+ if v != TRANSPARENT:
81
+ col = v
82
+ elif show_onion and onion is not None and onion[r, c] != TRANSPARENT:
83
+ col = 2 # ghost of the onion frame (dark purple)
84
+ else:
85
+ col = 6 if (r + c) % 2 == 0 else 5 # empty = checkerboard
86
+ screen.rectfill(x, y, x + CELL - 2, y + CELL - 2, col)
87
+ # palette strip
88
+ for i in range(16):
89
+ x = PAL_X + i * SW
90
+ screen.rectfill(x, PAL_Y, x + SW - 2, PAL_Y + SW - 2, i)
91
+ sx = PAL_X + selected * SW
92
+ screen.rect(sx - 1, PAL_Y - 1, sx + SW - 1, PAL_Y + SW - 1, 7)
93
+ # 1x live preview (actual size)
94
+ for r in range(GRID):
95
+ for c in range(GRID):
96
+ if grid[r, c] != TRANSPARENT:
97
+ screen.pset(PV_X + c, PV_Y + r, grid[r, c])
98
+ screen.rect(PV_X - 1, PV_Y - 1, PV_X + GRID, PV_Y + GRID, 6)
99
+ screen.text("C:" + str(selected), 104, 26, 7)
100
+ screen.text("ON" if show_onion else "OFF", 104, 34, 7)
101
+ # help
102
+ screen.text("E:CODE S:SAVE L:LOAD", 4, 114, 7)
103
+ screen.text("Z:UNDO C:CLR O/H:ONION", 4, 120, 7)
104
+
105
+
106
+ def main():
107
+ global selected, onion, show_onion
108
+ sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO)
109
+ window = sdl2.SDL_CreateWindow(
110
+ b"Olympus Sprite Editor",
111
+ sdl2.SDL_WINDOWPOS_CENTERED, sdl2.SDL_WINDOWPOS_CENTERED,
112
+ VW * SCALE, VH * SCALE, sdl2.SDL_WINDOW_SHOWN)
113
+ renderer = sdl2.SDL_CreateRenderer(window, -1, sdl2.SDL_RENDERER_ACCELERATED)
114
+ sdl2.SDL_SetHint(sdl2.SDL_HINT_RENDER_SCALE_QUALITY, b"0")
115
+ texture = sdl2.SDL_CreateTexture(
116
+ renderer, sdl2.SDL_PIXELFORMAT_RGB24,
117
+ sdl2.SDL_TEXTUREACCESS_STREAMING, VW, VH)
118
+
119
+ screen = Screen(VW, VH)
120
+ event = sdl2.SDL_Event()
121
+ mxp, myp = ctypes.c_int(0), ctypes.c_int(0)
122
+ prev_held = False
123
+ running = True
124
+
125
+ while running:
126
+ while sdl2.SDL_PollEvent(ctypes.byref(event)):
127
+ if event.type == sdl2.SDL_QUIT:
128
+ running = False
129
+ elif event.type == sdl2.SDL_KEYDOWN:
130
+ k = event.key.keysym.sym
131
+ if k == sdl2.SDLK_ESCAPE:
132
+ running = False
133
+ elif k == sdl2.SDLK_c:
134
+ push_undo(); grid[:, :] = TRANSPARENT
135
+ elif k == sdl2.SDLK_e:
136
+ code = to_code()
137
+ print("\n" + code + "\n")
138
+ with open("sprite_out.txt", "w") as f:
139
+ f.write(code + "\n")
140
+ elif k == sdl2.SDLK_s:
141
+ save_grid()
142
+ elif k == sdl2.SDLK_l:
143
+ load_grid()
144
+ elif k == sdl2.SDLK_z:
145
+ if undo_stack:
146
+ grid[:, :] = undo_stack.pop()
147
+ elif k == sdl2.SDLK_o:
148
+ onion = grid.copy(); show_onion = True
149
+ elif k == sdl2.SDLK_h:
150
+ show_onion = not show_onion
151
+
152
+ buttons = sdl2.SDL_GetMouseState(ctypes.byref(mxp), ctypes.byref(myp))
153
+ mx, my = mxp.value // SCALE, myp.value // SCALE
154
+ left = buttons & sdl2.SDL_BUTTON_LMASK
155
+ right = buttons & sdl2.SDL_BUTTON_RMASK
156
+ held = bool(left or right)
157
+
158
+ # snapshot once at the start of each stroke that begins on the canvas
159
+ if held and not prev_held and in_canvas(mx, my):
160
+ push_undo()
161
+ prev_held = held
162
+
163
+ if left:
164
+ if in_canvas(mx, my):
165
+ r, c = canvas_cell(mx, my)
166
+ grid[r, c] = selected
167
+ else:
168
+ pi = palette_index(mx, my)
169
+ if pi is not None:
170
+ selected = pi
171
+ if right and in_canvas(mx, my):
172
+ r, c = canvas_cell(mx, my)
173
+ grid[r, c] = TRANSPARENT
174
+
175
+ draw(screen)
176
+ rgb = screen.to_rgb()
177
+ sdl2.SDL_UpdateTexture(texture, None, ctypes.c_void_p(rgb.ctypes.data), VW *
178
+ 3)
179
+ sdl2.SDL_RenderClear(renderer)
180
+ sdl2.SDL_RenderCopy(renderer, texture, None, None)
181
+ sdl2.SDL_RenderPresent(renderer)
182
+ sdl2.SDL_Delay(16)
183
+
184
+ sdl2.SDL_DestroyTexture(texture)
185
+ sdl2.SDL_DestroyRenderer(renderer)
186
+ sdl2.SDL_DestroyWindow(window)
187
+ sdl2.SDL_Quit()
188
+
189
+
190
+ if __name__ == "__main__":
191
+ main()
@@ -0,0 +1,43 @@
1
+ FONT = {
2
+ 'A': ["XXX", "X.X", "XXX", "X.X", "X.X"],
3
+ 'B': ["XX.", "X.X", "XX.", "X.X", "XX."],
4
+ 'C': ["XXX", "X..", "X..", "X..", "XXX"],
5
+ 'D': ["XX.", "X.X", "X.X", "X.X", "XX."],
6
+ 'E': ["XXX", "X..", "XX.", "X..", "XXX"],
7
+ 'F': ["XXX", "X..", "XX.", "X..", "X.."],
8
+ 'G': ["XXX", "X..", "X.X", "X.X", "XXX"],
9
+ 'H': ["X.X", "X.X", "XXX", "X.X", "X.X"],
10
+ 'I': ["XXX", ".X.", ".X.", ".X.", "XXX"],
11
+ 'J': ["..X", "..X", "..X", "X.X", "XXX"],
12
+ 'K': ["X.X", "X.X", "XX.", "X.X", "X.X"],
13
+ 'L': ["X..", "X..", "X..", "X..", "XXX"],
14
+ 'M': ["X.X", "XXX", "XXX", "X.X", "X.X"],
15
+ 'N': ["X.X", "XXX", "XXX", "XXX", "X.X"],
16
+ 'O': ["XXX", "X.X", "X.X", "X.X", "XXX"],
17
+ 'P': ["XXX", "X.X", "XXX", "X..", "X.."],
18
+ 'Q': ["XXX", "X.X", "X.X", "XXX", "..X"],
19
+ 'R': ["XX.", "X.X", "XX.", "X.X", "X.X"],
20
+ 'S': ["XXX", "X..", "XXX", "..X", "XXX"],
21
+ 'T': ["XXX", ".X.", ".X.", ".X.", ".X."],
22
+ 'U': ["X.X", "X.X", "X.X", "X.X", "XXX"],
23
+ 'V': ["X.X", "X.X", "X.X", "X.X", ".X."],
24
+ 'W': ["X.X", "X.X", "XXX", "XXX", "X.X"],
25
+ 'X': ["X.X", "X.X", ".X.", "X.X", "X.X"],
26
+ 'Y': ["X.X", "X.X", ".X.", ".X.", ".X."],
27
+ 'Z': ["XXX", "..X", ".X.", "X..", "XXX"],
28
+ '0': ["XXX", "X.X", "X.X", "X.X", "XXX"],
29
+ '1': [".X.", "XX.", ".X.", ".X.", "XXX"],
30
+ '2': ["XXX", "..X", "XXX", "X..", "XXX"],
31
+ '3': ["XXX", "..X", "XXX", "..X", "XXX"],
32
+ '4': ["X.X", "X.X", "XXX", "..X", "..X"],
33
+ '5': ["XXX", "X..", "XXX", "..X", "XXX"],
34
+ '6': ["XXX", "X..", "XXX", "X.X", "XXX"],
35
+ '7': ["XXX", "..X", "..X", "..X", "..X"],
36
+ '8': ["XXX", "X.X", "XXX", "X.X", "XXX"],
37
+ '9': ["XXX", "X.X", "XXX", "..X", "XXX"],
38
+ '.': ["...", "...", "...", "...", ".X."],
39
+ ':': ["...", ".X.", "...", ".X.", "..."],
40
+ '!': [".X.", ".X.", ".X.", "...", ".X."],
41
+ '-': ["...", "...", "XXX", "...", "..."],
42
+ ' ': ["...", "...", "...", "...", "..."],
43
+ }
@@ -0,0 +1,26 @@
1
+ import numpy as np
2
+
3
+ PALETTE = np.array([
4
+ (0, 0, 0), # 0 black
5
+ (29, 43, 83), # 1 dark-blue
6
+ (126, 37, 83), # 2 dark-purple
7
+ (0, 135, 81), # 3 dark-green
8
+ (171, 82, 54), # 4 brown
9
+ (95, 87, 79), # 5 dark-grey
10
+ (194, 195, 199), # 6 light-grey
11
+ (255, 241, 232), # 7 white
12
+ (255, 0, 77), # 8 red
13
+ (255, 163, 0), # 9 orange
14
+ (255, 236, 39), # 10 yellow
15
+ (0, 228, 54), # 11 green
16
+ (41, 173, 255), # 12 blue
17
+ (131, 118, 156), # 13 indigo
18
+ (255, 119, 168), # 14 pink
19
+ (255, 204, 170), # 15 peach
20
+ ], dtype=np.uint8)
21
+
22
+ # friendly names, so you can write screen.pset(x, y, RED)
23
+ BLACK, DARKBLUE, DARKPURPLE, DARKGREEN = 0, 1, 2, 3
24
+ BROWN, DARKGREY, LIGHTGREY, WHITE = 4, 5, 6, 7
25
+ RED, ORANGE, YELLOW, GREEN = 8, 9, 10, 11
26
+ BLUE, INDIGO, PINK, PEACH = 12, 13, 14, 15
@@ -0,0 +1,199 @@
1
+ import numpy as np
2
+ from .palette import PALETTE
3
+ import ctypes, sdl2
4
+ from .font import FONT
5
+
6
+ LEFT, RIGHT, UP, DOWN, O, X = 0, 1, 2, 3, 4, 5
7
+
8
+ _SCANCODES = [
9
+ sdl2.SDL_SCANCODE_LEFT,
10
+ sdl2.SDL_SCANCODE_RIGHT,
11
+ sdl2.SDL_SCANCODE_UP,
12
+ sdl2.SDL_SCANCODE_DOWN,
13
+ sdl2.SDL_SCANCODE_Z,
14
+ sdl2.SDL_SCANCODE_X,
15
+ ]
16
+
17
+ class Screen:
18
+
19
+ def __init__(self, w, h):
20
+ self.w = w
21
+ self.h = h
22
+ self.fb = np.zeros((h, w), dtype=np.uint8)
23
+ self._btn = [False] * 6
24
+ self._prev = [False] * 6
25
+
26
+ def cls(self, color):
27
+ self.fb[:, :] = color
28
+
29
+ def pset(self, x, y, color):
30
+ if 0 <= x < self.w and 0 <= y < self.h:
31
+ self.fb[y, x] = color
32
+
33
+ def line(self, x0, y0, x1, y1, color):
34
+ x0, y0, x1, y1 = int(x0), int(y0), int(x1), int(y1)
35
+ dx = abs(x1 - x0)
36
+ dy = -abs(y1 - y0)
37
+ sx = 1 if x0 < x1 else -1
38
+ sy = 1 if y0 < y1 else -1
39
+ err = dx + dy
40
+ while True:
41
+ self.pset(x0, y0, color)
42
+ if x0 == x1 and y0 == y1:
43
+ break
44
+ e2 = 2 * err
45
+ if e2 >= dy:
46
+ err += dy
47
+ x0 += sx
48
+ if e2 <= dx:
49
+ err += dx
50
+ y0 += sy
51
+
52
+ def rectfill(self, x0, y0, x1, y1, color):
53
+ x0, x1 = sorted((int(x0), int(x1)))
54
+ y0, y1 = sorted((int(y0), int(y1)))
55
+ x0 = max(0, x0)
56
+ y0 = max(0, y0)
57
+ x1 = min(self.w - 1, x1)
58
+ y1 = min(self.h - 1, y1)
59
+ if x0 <= x1 and y0 <= y1:
60
+ self.fb[y0:y1 + 1, x0:x1 + 1] = color
61
+
62
+ def rect(self, x0, y0, x1, y1, color):
63
+ self.line(x0, y0, x1, y0, color)
64
+ self.line(x0, y1, x1, y1, color)
65
+ self.line(x0, y0, x0, y1, color)
66
+ self.line(x1, y0, x1, y1, color)
67
+
68
+ def circ(self, cx, cy, r, color):
69
+ cx, cy, r = int(cx), int(cy), int(r)
70
+ if r < 0:
71
+ return
72
+ x = r
73
+ y = 0
74
+ err = 1 - r
75
+ while x >= y:
76
+ self.pset(cx + x, cy + y, color)
77
+ self.pset(cx + y, cy + x, color)
78
+ self.pset(cx - y, cy + x, color)
79
+ self.pset(cx - x, cy + y, color)
80
+ self.pset(cx - x, cy - y, color)
81
+ self.pset(cx - y, cy - x, color)
82
+ self.pset(cx + y, cy - x, color)
83
+ self.pset(cx + x, cy - y, color)
84
+ y += 1
85
+ if err < 0:
86
+ err += 2 * y + 1
87
+ else:
88
+ x -= 1
89
+ err += 2 * (y - x) + 1
90
+
91
+ def circfill(self, cx, cy, r, color):
92
+ cx, cy, r = int(cx), int(cy), int(r)
93
+ if r < 0:
94
+ return
95
+ x = r
96
+ y = 0
97
+ err = 1 - r
98
+ while x >= y:
99
+ # instead of plotting 8 edge points, draw horizontal spans between them
100
+ self.rectfill(cx - x, cy + y, cx + x, cy + y, color)
101
+ self.rectfill(cx - x, cy - y, cx + x, cy - y, color)
102
+ self.rectfill(cx - y, cy + x, cx + y, cy + x, color)
103
+ self.rectfill(cx - y, cy - x, cx + y, cy - x, color)
104
+ y += 1
105
+ if err < 0:
106
+ err += 2 * y + 1
107
+ else:
108
+ x -= 1
109
+ err += 2 * (y - x) + 1
110
+
111
+ def to_rgb(self):
112
+ return np.ascontiguousarray(PALETTE[self.fb])
113
+
114
+ def btn(self, i):
115
+ return self._btn[i]
116
+
117
+ def btnp(self, i):
118
+ return self._btn[i] and not self._prev[i]
119
+
120
+ def spr(self, sprite, x, y, flip_x=False, flip_y=False):
121
+ s = sprite
122
+ if flip_x:
123
+ s = s[:, ::-1]
124
+ if flip_y:
125
+ s = s[::-1, :]
126
+ sh, sw = s.shape
127
+ x, y = int(x), int(y)
128
+
129
+ sx0 = max(0, -x)
130
+ sy0 = max(0, -y)
131
+ sx1 = min(sw, self.w - x)
132
+ sy1 = min(sh, self.h - y)
133
+ if sx0 >= sx1 or sy0 >= sy1:
134
+ return
135
+
136
+ sub = s[sy0:sy1, sx0:sx1]
137
+ dst = self.fb[y + sy0:y + sy1, x + sx0:x + sx1]
138
+ mask = sub != 255
139
+ dst[mask] = sub[mask]
140
+
141
+ def text(self, s, x, y, color):
142
+ cx = x
143
+ for ch in str(s).upper():
144
+ glyph = FONT.get(ch)
145
+ if glyph is not None:
146
+ for j in range(5):
147
+ row = glyph[j]
148
+ for i in range(3):
149
+ if row[i] != '.':
150
+ self.pset(cx + i, y + j, color)
151
+ cx += 4
152
+ return cx
153
+
154
+ def run(self, update, draw, scale=5, title=b"Olympus"):
155
+ sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO)
156
+ win_w, win_h = self.w * scale, self.h * scale
157
+
158
+ window = sdl2.SDL_CreateWindow(
159
+ title,
160
+ sdl2.SDL_WINDOWPOS_CENTERED, sdl2.SDL_WINDOWPOS_CENTERED,
161
+ win_w, win_h,
162
+ sdl2.SDL_WINDOW_SHOWN)
163
+
164
+ renderer = sdl2.SDL_CreateRenderer(window, -1, sdl2.SDL_RENDERER_ACCELERATED)
165
+ sdl2.SDL_SetHint(sdl2.SDL_HINT_RENDER_SCALE_QUALITY, b"0")
166
+ texture = sdl2.SDL_CreateTexture(
167
+ renderer,
168
+ sdl2.SDL_PIXELFORMAT_RGB24,
169
+ sdl2.SDL_TEXTUREACCESS_STREAMING,
170
+ self.w, self.h)
171
+
172
+ event = sdl2.SDL_Event()
173
+ running = True
174
+ while running:
175
+ while sdl2.SDL_PollEvent(ctypes.byref(event)):
176
+ if event.type == sdl2.SDL_QUIT:
177
+ running = False
178
+ elif event.type == sdl2.SDL_KEYDOWN and \
179
+ event.key.keysym.sym == sdl2.SDLK_ESCAPE:
180
+ running = False
181
+
182
+ keystate = sdl2.SDL_GetKeyboardState(None)
183
+ self._prev = self._btn
184
+ self._btn = [bool(keystate[sc]) for sc in _SCANCODES]
185
+
186
+ update()
187
+ draw()
188
+
189
+ rgb = self.to_rgb()
190
+ sdl2.SDL_UpdateTexture(texture, None, ctypes.c_void_p(rgb.ctypes.data), self.w * 3)
191
+ sdl2.SDL_RenderClear(renderer)
192
+ sdl2.SDL_RenderCopy(renderer, texture, None, None)
193
+ sdl2.SDL_RenderPresent(renderer)
194
+ sdl2.SDL_Delay(16)
195
+
196
+ sdl2.SDL_DestroyTexture(texture)
197
+ sdl2.SDL_DestroyRenderer(renderer)
198
+ sdl2.SDL_DestroyWindow(window)
199
+ sdl2.SDL_Quit()
@@ -0,0 +1,69 @@
1
+ import ctypes
2
+ import wave
3
+ import numpy as np
4
+ import sdl2
5
+
6
+ SAMPLE_RATE = 22050 # audio samples per second
7
+
8
+
9
+ def load_wav(path, rate=SAMPLE_RATE):
10
+ """Load a .wav file and convert it to the engine's format
11
+ (mono, 16-bit, SAMPLE_RATE). Returns an int16 numpy array."""
12
+ wv = wave.open(path, "rb")
13
+ nch = wv.getnchannels()
14
+ width = wv.getsampwidth()
15
+ fr = wv.getframerate()
16
+ raw = wv.readframes(wv.getnframes())
17
+ wv.close()
18
+
19
+ # raw bytes -> float samples in -1..1, whatever the bit depth was
20
+ if width == 1:
21
+ a = (np.frombuffer(raw, np.uint8).astype(np.float32) - 128) / 128.0
22
+ elif width == 2:
23
+ a = np.frombuffer(raw, np.int16).astype(np.float32) / 32768.0
24
+ elif width == 4:
25
+ a = np.frombuffer(raw, np.int32).astype(np.float32) / 2147483648.0
26
+ else:
27
+ raise ValueError("unsupported WAV sample width: %d bytes" % width)
28
+
29
+ if nch > 1: # stereo -> mono: average the channels
30
+ a = a.reshape(-1, nch).mean(axis=1)
31
+
32
+ if fr != rate: # resample to our rate
33
+ m = len(a)
34
+ new = max(1, int(m * rate / fr))
35
+ a = np.interp(np.linspace(0, 1, new), np.linspace(0, 1, m), a)
36
+
37
+ return (a * 32767).astype(np.int16)
38
+
39
+
40
+ class Sound:
41
+ def __init__(self):
42
+ sdl2.SDL_InitSubSystem(sdl2.SDL_INIT_AUDIO)
43
+ spec = sdl2.SDL_AudioSpec(SAMPLE_RATE, sdl2.AUDIO_S16SYS, 1, 1024)
44
+ self.dev = sdl2.SDL_OpenAudioDevice(None, 0, spec, None, 0)
45
+ if self.dev:
46
+ sdl2.SDL_PauseAudioDevice(self.dev, 0)
47
+
48
+ def play(self, freq, ms, volume=0.3):
49
+ """Synthesize and play a square-wave beep."""
50
+ if not self.dev:
51
+ return
52
+ n = int(SAMPLE_RATE * ms / 1000)
53
+ t = np.arange(n)
54
+ phase = (freq * t / SAMPLE_RATE) % 1.0
55
+ square = np.where(phase < 0.5, 1.0, -1.0)
56
+ fade = max(1, int(SAMPLE_RATE * 0.005))
57
+ if n > 2 * fade:
58
+ square[:fade] *= np.linspace(0, 1, fade)
59
+ square[-fade:] *= np.linspace(1, 0, fade)
60
+ data = (square * volume * 32767).astype(np.int16)
61
+ sdl2.SDL_QueueAudio(self.dev, data.ctypes.data_as(ctypes.c_void_p), data.nbytes)
62
+
63
+ def play_clip(self, clip, volume=1.0):
64
+ """Play a clip loaded with load_wav()."""
65
+ if not self.dev or clip is None:
66
+ return
67
+ if volume != 1.0:
68
+ clip = (clip.astype(np.float32) * volume).astype(np.int16)
69
+ sdl2.SDL_QueueAudio(self.dev, clip.ctypes.data_as(ctypes.c_void_p), clip.nbytes)
@@ -0,0 +1,31 @@
1
+ import numpy as np
2
+ import os
3
+ from PIL import Image
4
+ from .palette import PALETTE
5
+
6
+ TRANSPARENT = 255
7
+
8
+ def make_sprite(rows):
9
+ h = len(rows)
10
+ w = len(rows[0])
11
+ s = np.full((h, w), TRANSPARENT, dtype=np.uint8)
12
+ for j, row in enumerate(rows):
13
+ for i, ch in enumerate(row):
14
+ if ch != '.':
15
+ s[j, i] = int(ch, 16)
16
+ return s
17
+
18
+ def load_sprite(path):
19
+ img = Image.open(path).convert("RGBA")
20
+ arr = np.array(img)
21
+ rgb = arr[:, :, :3].astype(np.int32)
22
+ alpha = arr[:, :, 3]
23
+
24
+ pal = PALETTE.astype(np.int32)
25
+
26
+ diff = rgb[:, :, None, :] - pal[None, None, :, :]
27
+ dist = (diff * diff).sum(axis=3)
28
+ idx = dist.argmin(axis=2).astype(np.uint8)
29
+
30
+ idx[alpha < 128] = TRANSPARENT
31
+ return idx
@@ -0,0 +1,65 @@
1
+ from .sprites import make_sprite
2
+
3
+ TILE = 8
4
+
5
+ WALL = make_sprite([
6
+ "55555555",
7
+ "56666665",
8
+ "56666665",
9
+ "56666665",
10
+ "56666665",
11
+ "56666665",
12
+ "56666665",
13
+ "55555555",
14
+ ])
15
+
16
+ COIN = make_sprite([
17
+ "........",
18
+ "..aaaa..",
19
+ ".aaaaaa.",
20
+ ".aaaaaa.",
21
+ ".aaaaaa.",
22
+ ".aaaaaa.",
23
+ "..aaaa..",
24
+ "........",
25
+ ])
26
+
27
+ TILES = {
28
+ 'W': WALL,
29
+ 'C': COIN,
30
+ }
31
+
32
+ SOLID = {'W'}
33
+
34
+ class Tilemap:
35
+ def __init__(self, rows, tiles, solid):
36
+ self.rows = rows
37
+ self.tiles = tiles
38
+ self.solid = solid
39
+ self.h = len(rows)
40
+ self.w = len(rows[0])
41
+
42
+ def get(self, col, row):
43
+ if 0 <= row < self.h and 0 <= col < self.w:
44
+ return self.rows[row][col]
45
+
46
+ def set(self, col, row, ch):
47
+ line = self.rows[row]
48
+ self.rows[row] = line[:col] + ch + line[col + 1:]
49
+
50
+ def draw(self, screen):
51
+ for row in range(self.h):
52
+ for col in range(self.w):
53
+ sprite = self.tiles.get(self.rows[row][col])
54
+ if sprite is not None:
55
+ screen.spr(sprite, col * TILE, row * TILE)
56
+
57
+ def solid_at(self, x, y):
58
+ return self.get(x // TILE, y // TILE) in self.solid
59
+
60
+ def box_hits_solid(self, x, y, w, h):
61
+ return (self.solid_at(x, y) or
62
+ self.solid_at(x + w - 1, y) or
63
+ self.solid_at(x, y + h - 1) or
64
+ self.solid_at(x + w - 1, y + h - 1))
65
+
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: olympus-engine
3
+ Version: 0.1.0
4
+ Summary: A from-scratch 2D game engine for Python
5
+ Author: Nithil Gadde
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/nithilgadde/Olympus
8
+ Project-URL: Repository, https://github.com/nithilgadde/Olympus
9
+ Project-URL: Issues, https://github.com/nithilgadde/Olympus/issues
10
+ Keywords: game-engine,fantasy-console,pico-8,pixel-art,gamedev,retro,2d,sdl2
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Topic :: Games/Entertainment
14
+ Classifier: Topic :: Multimedia :: Graphics
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Operating System :: OS Independent
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: numpy
28
+ Requires-Dist: pysdl2
29
+ Requires-Dist: pysdl2-dll
30
+ Requires-Dist: pillow
31
+ Dynamic: license-file
32
+
33
+ # Olympus
34
+
35
+ A from-scratch 2D fantasy-console game engine for Python. Build tiny pixel games on a 128×128 screen with a fixed 16-color palette and a few lines of code.
36
+
37
+ ![Olympus demo](https://raw.githubusercontent.com/nithilgadde/Olympus/main/docs/demo.gif)
38
+
39
+ ## Try it
40
+
41
+ ```bash
42
+ pip install olympus-engine
43
+ olympus-editor # draw sprites with the mouse, export to code
44
+ ```
45
+
46
+ Then make a tiny game in about 15 lines — see **Quick start** below.
47
+
48
+ ## Quick start
49
+
50
+ Make a game in about 15 lines:
51
+
52
+ ```python
53
+ import olympus as ol
54
+
55
+ screen = ol.Screen(128, 128)
56
+ x, y = 60, 60
57
+
58
+ def update():
59
+ global x, y
60
+ if screen.btn(ol.LEFT): x -= 1
61
+ if screen.btn(ol.RIGHT): x += 1
62
+ if screen.btn(ol.UP): y -= 1
63
+ if screen.btn(ol.DOWN): y += 1
64
+
65
+ def draw():
66
+ screen.cls(ol.BLACK)
67
+ screen.rectfill(x, y, x + 7, y + 7, ol.RED)
68
+ screen.text("HELLO OLYMPUS", 18, 40, ol.WHITE)
69
+
70
+ screen.run(update, draw)
71
+ ```
72
+
73
+ You write `update` (game logic) and `draw` (pixels); the engine runs them 60 times a second and handles the window, input, and rendering.
74
+
75
+ ## Features
76
+
77
+ - **128×128 indexed framebuffer** with a fixed 16-color palette — the classic fantasy-console look.
78
+ - **Hand-written rasterizers**: pixels, lines (Bresenham), rectangles, and circles (midpoint), outlined or filled.
79
+ - **Sprites** you can hand-draw as text or load from a PNG — loaded images are automatically quantized down to the 16-color palette.
80
+ - **Animation, tilemaps, and AABB collision** — cycle frames for walk cycles, build levels as grids of characters, and check what's solid.
81
+ - **A built-in pixel font** for scores, labels, and menus.
82
+ - **Sound**: square-wave synthesis for retro blips, plus loading and playing your own `.wav` files.
83
+ - **A built-in sprite editor** (`olympus-editor`) — draw with the mouse and export straight to engine code, with undo, save/load, and onion-skinning.
84
+
85
+ ## Running locally
86
+
87
+ Requirements:
88
+
89
+ - **Python 3.9+**
90
+ - No system libraries to install by hand — `pysdl2-dll` ships the SDL2 binaries, and `pip` installs everything else (numpy, PySDL2, Pillow) automatically.
91
+
92
+ From the project root (the folder with `pyproject.toml`):
93
+
94
+ ```bash
95
+ python -m venv venv
96
+ source venv/bin/activate # Windows: venv\Scripts\activate
97
+ pip install -e .
98
+
99
+ python examples/demo.py # run the demo game
100
+ olympus-editor # open the sprite editor
101
+ ```
102
+
103
+ ## How it works
104
+
105
+ Olympus is "from scratch" in a specific, deliberate way: **no graphics or game library does the drawing.** The screen is a NumPy array of palette *indices* (not RGB), and every shape is rasterized by hand — Bresenham lines, midpoint circles, a sprite blitter, a pixel font, and square-wave audio synthesized sample by sample. SDL2 is only used to open a window, present the finished pixel buffer, and read input. It is **not** a wrapper around pygame.
106
+
107
+ A couple of decisions worth calling out:
108
+
109
+ - **The framebuffer stores color indices, not RGB.** Drawing just writes small integers (0–15) into a 2D array, which is fast and keeps the palette constraint baked into the design. The conversion to real RGB happens once per frame, with a single NumPy lookup, right before handing the buffer to SDL.
110
+ - **Loading a PNG means quantizing it.** Because the framebuffer is indexed, imported images can't be copied pixel-for-pixel — every pixel is snapped to its nearest palette color in a vectorized NumPy pass. That preserves the console's look no matter what art you throw at it.
111
+
112
+ ## Credits
113
+
114
+ Built with [PySDL2](https://github.com/py-sdl/py-sdl2) (windowing, input, audio output), [NumPy](https://numpy.org/) (the framebuffer and rasterization), and [Pillow](https://python-pillow.org/) (PNG decoding). Inspired by [PICO-8](https://www.lexaloffle.com/pico-8.php) and the fantasy-console idea.
115
+
116
+ Created by Nithil Gadde · MIT License
@@ -0,0 +1,19 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ olympus/__init__.py
5
+ olympus/anim.py
6
+ olympus/collide.py
7
+ olympus/editor.py
8
+ olympus/font.py
9
+ olympus/palette.py
10
+ olympus/screen.py
11
+ olympus/sound.py
12
+ olympus/sprites.py
13
+ olympus/tilemap.py
14
+ olympus_engine.egg-info/PKG-INFO
15
+ olympus_engine.egg-info/SOURCES.txt
16
+ olympus_engine.egg-info/dependency_links.txt
17
+ olympus_engine.egg-info/entry_points.txt
18
+ olympus_engine.egg-info/requires.txt
19
+ olympus_engine.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ olympus-editor = olympus.editor:main
@@ -0,0 +1,4 @@
1
+ numpy
2
+ pysdl2
3
+ pysdl2-dll
4
+ pillow
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "olympus-engine"
7
+ version = "0.1.0"
8
+ description = "A from-scratch 2D game engine for Python"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{name = "Nithil Gadde"}]
14
+ keywords = ["game-engine", "fantasy-console", "pico-8", "pixel-art", "gamedev", "retro", "2d", "sdl2"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Topic :: Games/Entertainment",
19
+ "Topic :: Multimedia :: Graphics",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ "Operating System :: OS Independent",
29
+ ]
30
+ dependencies = [
31
+ "numpy",
32
+ "pysdl2",
33
+ "pysdl2-dll",
34
+ "pillow",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/nithilgadde/Olympus"
39
+ Repository = "https://github.com/nithilgadde/Olympus"
40
+ Issues = "https://github.com/nithilgadde/Olympus/issues"
41
+
42
+ [project.scripts]
43
+ olympus-editor = "olympus.editor:main"
44
+
45
+ [tool.setuptools]
46
+ packages = ["olympus"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+