videogame.py 11 KB

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