videogame.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. import builtins
  2. import os
  3. import time
  4. from typing import Tuple, Type
  5. import pygame
  6. import jogai
  7. import jogai._globals as _globals
  8. import jogai.settings as settings
  9. from jogai import character, group, logger, scene, utils, widgets
  10. from jogai.translations import gettext as _
  11. from jogai.utils import _get_events, _get_keys
  12. # --------------- #
  13. # GLOBALS #
  14. # --------------- #
  15. running = True
  16. _predefined_attributes_allowed = [
  17. _('caption'),
  18. _('gravity'),
  19. ]
  20. # ------------------ #
  21. # FUNCTIONS #
  22. # ------------------ #
  23. def _load_scene(data: dict) -> 'scene.Scene':
  24. """
  25. Load scene from data schema.
  26. Parameters:
  27. data: input schema
  28. Returns:
  29. An object containing the scene
  30. """
  31. if _('scene') in data:
  32. return scene.Scene(data[_('scene')])
  33. return scene.Scene()
  34. def _load_attributes(data: dict):
  35. """
  36. Load attributes from data schema.
  37. Parameters:
  38. data: input schema
  39. """
  40. if not isinstance(data, dict):
  41. raise TypeError(
  42. _('The attributes in the input schema must be a dict.')
  43. )
  44. for attribute in data:
  45. # utils._convert_function_arguments_to_string()
  46. if attribute in _predefined_attributes_allowed:
  47. eval(f'set_{attribute}(data["{attribute}"])')
  48. def _load_characters(data: dict) -> list:
  49. """
  50. Load characters from data schema.
  51. Parameters:
  52. data: input schema
  53. Returns:
  54. A list containing all loaded characters
  55. """
  56. data_characters = {}
  57. if not _('characters') in data:
  58. return []
  59. data_characters = data[_('characters')]
  60. characters = []
  61. for character_name, character_data in data_characters.items():
  62. if 'filename' in character_data:
  63. character_filename = character_data['filename']
  64. else:
  65. character_filename = f'{character_name}.png'
  66. loaded_character = character.Character(
  67. character_filename,
  68. character_name,
  69. )
  70. loaded_character._load_data(character_data)
  71. characters.append(loaded_character)
  72. return characters
  73. def simulate_key_press(key: int | str, down: bool = True):
  74. """
  75. Post a key press simulation to pygame event module.
  76. Parameters:
  77. key: Key to simulate
  78. down: Keystroke direction. True for KEYDOWN, False for KEYUP
  79. Examples:
  80. >>> simulate_key_press('a') # doctest: +SKIP
  81. """
  82. key = utils.eval_key(key)
  83. if isinstance(down, bool) and not down:
  84. event_type = pygame.KEYUP
  85. else:
  86. event_type = pygame.KEYDOWN
  87. key_event = pygame.event.Event(
  88. event_type,
  89. key=key,
  90. )
  91. pygame.event.post(key_event)
  92. def key_pressed(key: int | str) -> bool:
  93. """
  94. Check if a key has been pressed.
  95. Parameters:
  96. key: The key to be checked
  97. Returns:
  98. True if the key is pressed, False otherwise.
  99. Examples:
  100. >>> key_pressed('a')
  101. False
  102. """
  103. key = utils.eval_key(key)
  104. return _get_keys()[key]
  105. def add_character(new_character: 'character.Character'):
  106. """
  107. Add a new character to the videogame.
  108. Parameters:
  109. new_character: the character to add
  110. Examples:
  111. >>> prepare()
  112. >>> p = character.Character('protagonist.png')
  113. >>> add_character(p)
  114. """
  115. if not isinstance(new_character, character.Character):
  116. raise TypeError(_('Only Character instances can be added.'))
  117. _globals.vg_characters.append(new_character)
  118. def add_chronometer(new_chronometer: 'widgets.Chronometer'):
  119. """
  120. Add a new chronometer to the videogame.
  121. Parameters:
  122. new_chronometer: the chronometer to be added
  123. Examples:
  124. >>> chrono = widgets.Chronometer(30)
  125. >>> add_chronometer(chrono) # doctest: +SKIP
  126. """
  127. if not isinstance(new_chronometer, widgets.Chronometer):
  128. raise TypeError(_('Only Chronometer instances can be added.'))
  129. _globals.vg_chronometers.append(new_chronometer)
  130. def add_group(new_group: 'group.SquareGroup'):
  131. """
  132. Add a new group to the videogame.
  133. Parameters:
  134. new_group: the character to be added
  135. Examples:
  136. >>> p = character.Character('protagonist.png')
  137. >>> rectangle = group.RectangleGroup(p, 4, 2)
  138. >>> add_group(rectangle) # doctest: +SKIP
  139. """
  140. if not isinstance(new_group, group.Group):
  141. raise TypeError(_('Only Group instances can be added.'))
  142. _globals.vg_groups.append(new_group)
  143. def add_levelbar(new_levelbar: 'widgets.LevelBar'):
  144. """
  145. Add a new level bar to the videogame.
  146. Parameters:
  147. new_levelbar: the level bar to be added
  148. Examples:
  149. >>> lb = widgets.LevelBar(3)
  150. >>> add_levelbar(lb) # doctest: +SKIP
  151. """
  152. if not isinstance(new_levelbar, widgets.LevelBar):
  153. raise TypeError(_('Only LevelBar instances can be added.'))
  154. _globals.vg_levelbars.append(new_levelbar)
  155. def add_scoreboard(new_text: 'widgets.Text'):
  156. """
  157. Add a new scoreboard to the videogame.
  158. Parameters:
  159. new_scoreboard: the scoreboard to be added
  160. Examples:
  161. >>> sc = widgets.Scoreboard('Test')
  162. >>> add_scoreboard(sc) # doctest: +SKIP
  163. """
  164. if not isinstance(new_scoreboard, widgets.Scoreboard):
  165. raise TypeError(_('Only Scoreboard instances can be added.'))
  166. _globals.vg_scobreboards.append(new_scoreboard)
  167. def add_text(new_text: 'widgets.Text'):
  168. """
  169. Add a new text to the videogame.
  170. Parameters:
  171. new_text: the text to be added
  172. Examples:
  173. >>> text = widgets.Text('Test')
  174. >>> add_text(text) # doctest: +SKIP
  175. """
  176. if not isinstance(new_text, widgets.Text):
  177. raise TypeError(_('Only Text instances can be added.'))
  178. _globals.vg_texts.append(new_text)
  179. def init():
  180. """
  181. Start the pygame engine if it is not started.
  182. Examples:
  183. >>> init()
  184. """
  185. if not pygame.get_init():
  186. pygame.init()
  187. _globals._window = pygame.display.set_mode(
  188. (settings.WINDOW_WIDTH, settings.WINDOW_HEIGHT)
  189. )
  190. def is_running() -> bool:
  191. """
  192. Check if the game is running. Useful in the main loop.
  193. Returns:
  194. True if the game is running. False otherwise.
  195. Examples:
  196. >>> state = is_running()
  197. """
  198. return running
  199. def listen_events():
  200. """
  201. Listen for events coming from the keyboard and window clicks.
  202. Examples:
  203. >>> simulate_key_press('ESCAPE')
  204. >>> listen_events()
  205. """
  206. _globals._events = _get_events()
  207. _globals._keys = _get_keys()
  208. for event in _globals._events:
  209. match event.type:
  210. case pygame.KEYDOWN:
  211. if event.key == pygame.K_ESCAPE:
  212. quit()
  213. case pygame.QUIT:
  214. quit()
  215. def get_color(x: int, y: int) -> Tuple[int, int, int]:
  216. """
  217. Get the RGB components for a point in the game window.
  218. Parameters:
  219. x: Position on the abscissa axis
  220. y: Position on the ordinate axis
  221. Returns:
  222. A tuple of integers with the RGB components
  223. Examples:
  224. >>> color = get_color(100, 200)
  225. """
  226. if not isinstance(x, int) or not isinstance(y, int):
  227. raise TypeError(
  228. _('The coordinates of the point must be a non-negative integers.')
  229. )
  230. if not (0 <= x <= settings.WINDOW_WIDTH) or not (
  231. 0 <= y <= settings.WINDOW_HEIGHT
  232. ):
  233. raise ValueError(
  234. _('The coordinates of the point must be a non-negative integers.')
  235. )
  236. return tuple(_globals._window.get_at((x, y))[:-1])
  237. def paint(
  238. sprite: Type['scene.Scene']
  239. | Type['character.Character']
  240. | Type['widgets.Text'],
  241. ):
  242. """
  243. Paint multiple sprite types on the main window.
  244. Parameters:
  245. sprite: The element to be painted.
  246. """
  247. # TODO: check sprite type
  248. if hasattr(sprite, 'background'):
  249. _globals._window.fill(sprite.background)
  250. if hasattr(sprite, 'image') and hasattr(sprite, 'rect'):
  251. _globals._window.blit(sprite.image, sprite.rect)
  252. def paint_group(group: Type['group.SquareGroup']):
  253. """
  254. Paint a group of sprites on the main window.
  255. Parameters:
  256. group: The group of sprites to be painted.
  257. """
  258. group.draw(_globals._window)
  259. def set_caption(caption: str):
  260. """
  261. Set the caption for the videogame.
  262. Parameters:
  263. caption: Caption to be setted
  264. Examples:
  265. >>> set_caption('My videogame')
  266. """
  267. if not isinstance(caption, str):
  268. raise TypeError(_('The caption for the videogame must be a string.'))
  269. pygame.display.set_caption(caption)
  270. def set_gravity(gravity: int):
  271. """
  272. Set the gravity in the videogame.
  273. Parameters:
  274. gravity: Gravity force to be setted.
  275. Examples:
  276. >>> set_gravity(5)
  277. """
  278. if not isinstance(gravity, int):
  279. raise TypeError(_('The gravity must be an integer.'))
  280. _globals._gravity = gravity
  281. def set_marks(
  282. x_gap: int = 0,
  283. y_gap: int = 0,
  284. x_color: tuple = (255, 180, 30),
  285. y_color: tuple = (0, 0, 255),
  286. width: int = 1,
  287. debug: bool = False,
  288. ):
  289. """
  290. Set distance mark lines and position information along both axis.
  291. Parameters:
  292. x_gap: The gap distance on abscissa axis.
  293. y_gap: The gap distance on ordinate axis.
  294. x_color: The line color on abscissa axis.
  295. y_color: The line color on ordinate axis.
  296. width: The line width.
  297. debug: If True, extra detailed information is shown.
  298. """
  299. _globals.marks = utils.Marks(x_gap, y_gap, x_color, y_color, width, debug)
  300. def prepare(data_filename: str = ''):
  301. """
  302. Prepare the game environment before entering the main loop.
  303. Examples:
  304. >>> init()
  305. >>> prepare()
  306. """
  307. if not _globals._window:
  308. init()
  309. pygame.key.set_repeat(settings.KEY_DELAY, settings.KEY_INTERVAL)
  310. if data_filename:
  311. vg_data = utils._load_yaml_from_filename(data_filename)
  312. else:
  313. vg_data = _globals._default_data
  314. _globals.vg_scene = _load_scene(vg_data)
  315. _globals.vg_characters = _load_characters(vg_data)
  316. _globals.vg_attributes = _load_attributes(vg_data)
  317. if _globals.vg_characters:
  318. _globals.character_focus = _globals.vg_characters[0]
  319. # TODO: Check and filter variable names added to builtins
  320. setattr(builtins, 'vgscene', _globals.vg_scene)
  321. for vg_character in _globals.vg_characters:
  322. setattr(builtins, vg_character.name, vg_character)
  323. def stop():
  324. """
  325. Finish the infinite videogame loop.
  326. """
  327. global running
  328. running = False
  329. def update():
  330. """
  331. Refresh the display surface and listen events.
  332. """
  333. if _globals.vg_scene:
  334. if _globals.vg_characters:
  335. _globals.vg_scene.set_position(_globals.character_focus.x)
  336. paint(_globals.vg_scene)
  337. if _globals.vg_characters:
  338. for pos, vg_character in enumerate(_globals.vg_characters):
  339. tracking = (
  340. True if vg_character == _globals.character_focus else False
  341. )
  342. vg_character._update_rect_position(tracking=tracking)
  343. paint(vg_character)
  344. vg_character.update()
  345. if _globals.vg_chronometers:
  346. for chronometers_item in _globals.vg_chronometers:
  347. paint(chronometers_item)
  348. chronometers_item.update()
  349. if _globals.vg_groups:
  350. for group_item in _globals.vg_groups:
  351. for sprite_item in group_item.sprites():
  352. sprite_item._update_rect_position()
  353. paint(sprite_item)
  354. sprite_item.update()
  355. if _globals.vg_levelbars:
  356. for levelbar_item in _globals.vg_levelbars:
  357. levelbar_item.draw()
  358. if _globals.vg_scoreboards:
  359. for scoreboard_item in _globals.vg_scoreboards:
  360. paint(scoreboard_item)
  361. if _globals.vg_texts:
  362. for texts_item in _globals.vg_texts:
  363. paint(texts_item)
  364. if _globals.marks:
  365. _globals.marks.draw()
  366. pygame.display.flip()
  367. _globals._clock.tick(settings.FPS)
  368. listen_events()
  369. def quit(wait: int = 0):
  370. """
  371. Exit the game.
  372. Parameters:
  373. wait: Seconds to wait before exiting
  374. Examples:
  375. >>> quit(1)
  376. """
  377. if isinstance(wait, int) and wait > 0:
  378. time.sleep(wait)
  379. pygame.quit()
  380. stop()