mini-arcade-core 0.8.0__tar.gz → 0.8.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mini-arcade-core
3
- Version: 0.8.0
3
+ Version: 0.8.1
4
4
  Summary: Tiny scene-based game loop core for small arcade games.
5
5
  License: Copyright (c) 2025 Santiago Rincón
6
6
 
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "mini-arcade-core"
7
- version = "0.8.0"
7
+ version = "0.8.1"
8
8
  description = "Tiny scene-based game loop core for small arcade games."
9
9
  authors = [
10
10
  { name = "Santiago Rincon", email = "rincores@gmail.com" },
@@ -40,6 +40,12 @@ class GameConfig:
40
40
  backend: Backend | None = None
41
41
 
42
42
 
43
+ @dataclass
44
+ class _StackEntry:
45
+ scene: "Scene"
46
+ as_overlay: bool = False
47
+
48
+
43
49
  class Game:
44
50
  """Core game object responsible for managing the main loop and active scene."""
45
51
 
@@ -59,6 +65,16 @@ class Game:
59
65
  "GameConfig.backend must be set to a Backend instance"
60
66
  )
61
67
  self.backend: Backend = config.backend
68
+ self._scene_stack: list[_StackEntry] = []
69
+
70
+ def current_scene(self) -> "Scene | None":
71
+ """
72
+ Get the currently active scene.
73
+
74
+ :return: The active Scene instance, or None if no scene is active.
75
+ :rtype: Scene | None
76
+ """
77
+ return self._scene_stack[-1].scene if self._scene_stack else None
62
78
 
63
79
  def change_scene(self, scene: Scene):
64
80
  """
@@ -68,10 +84,59 @@ class Game:
68
84
  :param scene: The new scene to activate.
69
85
  :type scene: Scene
70
86
  """
71
- if self._current_scene is not None:
72
- self._current_scene.on_exit()
73
- self._current_scene = scene
74
- self._current_scene.on_enter()
87
+ while self._scene_stack:
88
+ entry = self._scene_stack.pop()
89
+ entry.scene.on_exit()
90
+
91
+ self._scene_stack.append(_StackEntry(scene=scene, as_overlay=False))
92
+ scene.on_enter()
93
+
94
+ def push_scene(self, scene: "Scene", as_overlay: bool = False):
95
+ """
96
+ Push a scene on top of the current one.
97
+ If as_overlay=True, underlying scene(s) may still be drawn but never updated.
98
+ """
99
+ top = self.current_scene()
100
+ if top is not None:
101
+ top.on_pause()
102
+
103
+ self._scene_stack.append(
104
+ _StackEntry(scene=scene, as_overlay=as_overlay)
105
+ )
106
+ scene.on_enter()
107
+
108
+ def pop_scene(self) -> "Scene | None":
109
+ """Pop the top scene. If stack becomes empty, quit."""
110
+ if not self._scene_stack:
111
+ return None
112
+
113
+ popped = self._scene_stack.pop()
114
+ popped.scene.on_exit()
115
+
116
+ top = self.current_scene()
117
+ if top is None:
118
+ self.quit()
119
+ return popped.scene
120
+
121
+ top.on_resume()
122
+ return popped.scene
123
+
124
+ def _visible_stack(self) -> list["Scene"]:
125
+ """
126
+ Return the list of scenes that should be drawn (base + overlays).
127
+ We draw from the top-most non-overlay scene upward.
128
+ """
129
+ if not self._scene_stack:
130
+ return []
131
+
132
+ # find top-most base scene (as_overlay=False)
133
+ base_idx = 0
134
+ for i in range(len(self._scene_stack) - 1, -1, -1):
135
+ if not self._scene_stack[i].as_overlay:
136
+ base_idx = i
137
+ break
138
+
139
+ return [e.scene for e in self._scene_stack[base_idx:]]
75
140
 
76
141
  def quit(self):
77
142
  """Request that the main loop stops."""
@@ -104,24 +169,27 @@ class Game:
104
169
  dt = now - last_time
105
170
  last_time = now
106
171
 
107
- scene = self._current_scene
108
- if scene is None:
172
+ top = self.current_scene()
173
+ if top is None:
109
174
  break
110
175
 
111
176
  for ev in backend.poll_events():
112
- scene.handle_event(ev)
177
+ top.handle_event(ev)
113
178
 
114
- scene.update(dt)
179
+ top.update(dt)
115
180
 
116
181
  backend.begin_frame()
117
- scene.draw(backend)
182
+ for scene in self._visible_stack():
183
+ scene.draw(backend)
118
184
  backend.end_frame()
119
185
 
120
186
  if target_dt > 0 and dt < target_dt:
121
187
  sleep(target_dt - dt)
122
188
 
123
- if self._current_scene is not None:
124
- self._current_scene.on_exit()
189
+ # exit remaining scenes
190
+ while self._scene_stack:
191
+ entry = self._scene_stack.pop()
192
+ entry.scene.on_exit()
125
193
 
126
194
  @staticmethod
127
195
  def _convert_bmp_to_image(bmp_path: str, out_path: str) -> bool:
@@ -139,3 +139,9 @@ class Scene(ABC):
139
139
  :param surface: The backend surface to draw on.
140
140
  :type surface: Backend
141
141
  """
142
+
143
+ def on_pause(self):
144
+ """Called when the game is paused."""
145
+
146
+ def on_resume(self):
147
+ """Called when the game is resumed."""
@@ -0,0 +1,135 @@
1
+ """
2
+ Menu system for mini arcade core.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Callable, Sequence
9
+
10
+ from mini_arcade_core.backend import Backend, Color, Event, EventType
11
+
12
+ MenuAction = Callable[[], None]
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class MenuItem:
17
+ """
18
+ Represents a single item in a menu.
19
+
20
+ :ivar label (str): The text label of the menu item.
21
+ :ivar on_select (MenuAction): The action to perform when the item is selected.
22
+ """
23
+
24
+ label: str
25
+ on_select: MenuAction
26
+
27
+
28
+ @dataclass
29
+ class MenuStyle:
30
+ """
31
+ Styling options for the Menu.
32
+
33
+ :ivar normal (Color): Color for unselected items.
34
+ :ivar selected (Color): Color for the selected item.
35
+ :ivar line_height (int): Vertical spacing between items.
36
+ """
37
+
38
+ normal: Color = (220, 220, 220)
39
+ selected: Color = (255, 255, 0)
40
+ line_height: int = 28
41
+
42
+
43
+ class Menu:
44
+ """A simple text-based menu system."""
45
+
46
+ def __init__(
47
+ self,
48
+ items: Sequence[MenuItem],
49
+ *,
50
+ x: int = 40,
51
+ y: int = 40,
52
+ style: MenuStyle | None = None,
53
+ ):
54
+ """
55
+ :param items: Sequence of MenuItem instances to display.
56
+ type items: Sequence[MenuItem]
57
+
58
+ :param x: X coordinate for the menu's top-left corner.
59
+ :param y: Y coordinate for the menu's top-left corner.
60
+
61
+ :param style: Optional MenuStyle for customizing appearance.
62
+ :type style: MenuStyle | None
63
+ """
64
+ self.items = list(items)
65
+ self.x = x
66
+ self.y = y
67
+ self.style = style or MenuStyle()
68
+ self.selected_index = 0
69
+
70
+ def move_up(self):
71
+ """Move the selection up by one item, wrapping around if necessary."""
72
+ if self.items:
73
+ self.selected_index = (self.selected_index - 1) % len(self.items)
74
+
75
+ def move_down(self):
76
+ """Move the selection down by one item, wrapping around if necessary."""
77
+ if self.items:
78
+ self.selected_index = (self.selected_index + 1) % len(self.items)
79
+
80
+ def select(self):
81
+ """Select the currently highlighted item, invoking its action."""
82
+ if self.items:
83
+ self.items[self.selected_index].on_select()
84
+
85
+ def handle_event(
86
+ self,
87
+ event: Event,
88
+ *,
89
+ up_key: int,
90
+ down_key: int,
91
+ select_key: int,
92
+ ):
93
+ """
94
+ Handle an input event to navigate the menu.
95
+
96
+ :param event: The input event to handle.
97
+ :type event: Event
98
+
99
+ :param up_key: Key code for moving selection up.
100
+ type up_key: int
101
+
102
+ :param down_key: Key code for moving selection down.
103
+ :type down_key: int
104
+
105
+ :param select_key: Key code for selecting the current item.
106
+ :type select_key: int
107
+ """
108
+ if event.type != EventType.KEYDOWN or event.key is None:
109
+ return
110
+ if event.key == up_key:
111
+ self.move_up()
112
+ elif event.key == down_key:
113
+ self.move_down()
114
+ elif event.key == select_key:
115
+ self.select()
116
+
117
+ def draw(self, surface: Backend):
118
+ """
119
+ Draw the menu onto the given backend surface.
120
+
121
+ :param surface: The backend surface to draw on.
122
+ :type surface: Backend
123
+ """
124
+ for i, item in enumerate(self.items):
125
+ color = (
126
+ self.style.selected
127
+ if i == self.selected_index
128
+ else self.style.normal
129
+ )
130
+ surface.draw_text(
131
+ self.x,
132
+ self.y + i * self.style.line_height,
133
+ item.label,
134
+ color=color,
135
+ )