mini-arcade-core 0.8.0__tar.gz → 0.9.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mini-arcade-core
3
- Version: 0.8.0
3
+ Version: 0.9.0
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.9.0"
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" },
@@ -7,7 +7,7 @@ from __future__ import annotations
7
7
 
8
8
  from dataclasses import dataclass
9
9
  from enum import Enum, auto
10
- from typing import Iterable, Protocol, Tuple, Union
10
+ from typing import Iterable, Optional, Protocol, Tuple, Union
11
11
 
12
12
  Color = Union[Tuple[int, int, int], Tuple[int, int, int, int]]
13
13
 
@@ -20,14 +20,33 @@ class EventType(Enum):
20
20
  :cvar QUIT: User requested to quit the game.
21
21
  :cvar KEYDOWN: A key was pressed.
22
22
  :cvar KEYUP: A key was released.
23
+ :cvar MOUSEMOTION: The mouse was moved.
24
+ :cvar MOUSEBUTTONDOWN: A mouse button was pressed.
25
+ :cvar MOUSEBUTTONUP: A mouse button was released.
26
+ :cvar MOUSEWHEEL: The mouse wheel was scrolled.
27
+ :cvar WINDOWRESIZED: The window was resized.
28
+ :cvar TEXTINPUT: Text input event (for IME support).
23
29
  """
24
30
 
25
31
  UNKNOWN = auto()
26
32
  QUIT = auto()
33
+
27
34
  KEYDOWN = auto()
28
35
  KEYUP = auto()
29
36
 
37
+ # Mouse
38
+ MOUSEMOTION = auto()
39
+ MOUSEBUTTONDOWN = auto()
40
+ MOUSEBUTTONUP = auto()
41
+ MOUSEWHEEL = auto()
42
+
43
+ # Window / text
44
+ WINDOWRESIZED = auto()
45
+ TEXTINPUT = auto()
46
+
30
47
 
48
+ # Justification: Simple data container for now
49
+ # pylint: disable=too-many-instance-attributes
31
50
  @dataclass(frozen=True)
32
51
  class Event:
33
52
  """
@@ -39,10 +58,43 @@ class Event:
39
58
 
40
59
  :ivar type (EventType): The type of event.
41
60
  :ivar key (int | None): The key code associated with the event, if any.
61
+ :ivar scancode (int | None): The hardware scancode of the key, if any.
62
+ :ivar mod (int | None): Modifier keys bitmask, if any.
63
+ :ivar repeat (bool | None): Whether this key event is a repeat, if any.
64
+ :ivar x (int | None): Mouse X position, if any.
65
+ :ivar y (int | None): Mouse Y position, if any.
66
+ :ivar dx (int | None): Mouse delta X, if any.
67
+ :ivar dy (int | None): Mouse delta Y, if any.
68
+ :ivar button (int | None): Mouse button number, if any.
69
+ :ivar wheel (Tuple[int, int] | None): Mouse wheel scroll (x, y), if any.
70
+ :ivar size (Tuple[int, int] | None): New window size (width, height), if any.
71
+ :ivar text (str | None): Text input, if any.
42
72
  """
43
73
 
44
74
  type: EventType
45
- key: int | None = None
75
+ key: Optional[int] = None
76
+
77
+ # Keyboard extras (optional)
78
+ scancode: Optional[int] = None
79
+ mod: Optional[int] = None
80
+ repeat: Optional[bool] = None
81
+
82
+ # Mouse (optional)
83
+ x: Optional[int] = None
84
+ y: Optional[int] = None
85
+ dx: Optional[int] = None
86
+ dy: Optional[int] = None
87
+ button: Optional[int] = None
88
+ wheel: Optional[Tuple[int, int]] = None # (wheel_x, wheel_y)
89
+
90
+ # Window (optional)
91
+ size: Optional[Tuple[int, int]] = None # (width, height)
92
+
93
+ # Text input (optional)
94
+ text: Optional[str] = None
95
+
96
+
97
+ # pylint: enable=too-many-instance-attributes
46
98
 
47
99
 
48
100
  class Backend(Protocol):
@@ -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
+ )