videogame.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. # jogai - Python Game Library
  2. #
  3. # This library is free software; you can redistribute it and/or
  4. # modify it under the terms of the GNU Library General Public
  5. # License as published by the Free Software Foundation; either
  6. # version 2 of the License, or (at your option) any later version.
  7. #
  8. # This library is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  11. # Library General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU Library General Public
  14. # License along with this library; if not, write to the Free
  15. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  16. #
  17. # Ramón Palleiro Rodríguez
  18. # Zero! Factorial Studio
  19. # info@zero.gal
  20. import builtins
  21. import os
  22. import time
  23. from collections import OrderedDict
  24. from typing import Any, Tuple, Type
  25. import pygame
  26. import jogai
  27. import jogai._globals as _globals
  28. from jogai import character, groups, logger, scene, texts, utils, widgets
  29. from jogai.settings import settings
  30. from jogai.translations import gettext as _
  31. from jogai.utils import _get_events, _get_keys
  32. _PREDEFINED_ATTRIBUTES_ALLOWED = (
  33. _('caption'),
  34. _('characters'),
  35. _('gravity'),
  36. _('marks'),
  37. _('scene'),
  38. _('texts'),
  39. )
  40. # --------------- #
  41. # GLOBALS #
  42. # --------------- #
  43. running = True
  44. # ------------------ #
  45. # FUNCTIONS #
  46. # ------------------ #
  47. def _calculate_initial_collisions():
  48. for loaded_character in _globals.vg_characters:
  49. loaded_character.current_collisions = (
  50. loaded_character.calculate_collisions(
  51. (loaded_character.x, loaded_character.y)
  52. )
  53. )
  54. def _calculate_initial_touch():
  55. for loaded_character in _globals.vg_characters:
  56. loaded_character.current_touch = loaded_character.calculate_touch(
  57. (loaded_character.x, loaded_character.y)
  58. )
  59. def _load_data(data: dict):
  60. """
  61. Loads data from the schema.
  62. Parameters:
  63. data: input schema
  64. """
  65. if not isinstance(data, dict):
  66. raise TypeError(_('the attributes in the input schema must be a dict'))
  67. for name, value in data.items():
  68. # utils._convert_function_arguments_to_string()
  69. if name in _PREDEFINED_ATTRIBUTES_ALLOWED:
  70. if isinstance(value, str):
  71. value = f'"{value}"'
  72. eval(_('set_{}({})').format(name, value))
  73. else:
  74. add_attribute(name, value)
  75. def simulate_key_press(key: int | str, down: bool = True):
  76. """
  77. Posts a key press simulation to pygame event module.
  78. Parameters:
  79. key: key to simulate
  80. down: keystroke direction. True for KEYDOWN, False for KEYUP
  81. Examples:
  82. >>> simulate_key_press('a') # doctest: +SKIP
  83. """
  84. key = utils.eval_key(key)
  85. if isinstance(down, bool) and not down:
  86. event_type = pygame.KEYUP
  87. else:
  88. event_type = pygame.KEYDOWN
  89. key_event = pygame.event.Event(
  90. event_type,
  91. key=key,
  92. )
  93. pygame.event.post(key_event)
  94. def key_pressed(key: int | str) -> bool:
  95. """
  96. Checks if a key has been pressed.
  97. Parameters:
  98. key: the key to be checked
  99. Returns:
  100. True if the key is pressed, False otherwise.
  101. Examples:
  102. >>> key_pressed('a')
  103. False
  104. """
  105. key = utils.eval_key(key)
  106. return _get_keys()[key]
  107. def add_attribute(name: str, value: Any):
  108. """
  109. Adds a new attribute to the videogame.
  110. Parameters:
  111. name: a name for this attribute
  112. value: a value for this attribute
  113. Examples:
  114. >>> add_attribute('test': 'foo') # doctest: +SKIP
  115. """
  116. _globals.vg_attributes.update({name: value})
  117. def add_character(new_character: 'character.Character'):
  118. """
  119. Adds a new character to the videogame.
  120. Parameters:
  121. new_character: the character to add
  122. Examples:
  123. >>> p = character.Character('protagonist.png')
  124. >>> add_character(p) # doctest: +SKIP
  125. """
  126. if not isinstance(new_character, character.Character):
  127. raise TypeError(_('only Character instances can be added'))
  128. _globals.vg_characters.append(new_character)
  129. def add_chronometer(new_chronometer: 'texts.Chronometer'):
  130. """
  131. Adds a new chronometer to the videogame.
  132. Parameters:
  133. new_chronometer: the chronometer to be added
  134. Examples:
  135. >>> chrono = texts.Chronometer(30)
  136. >>> add_chronometer(chrono) # doctest: +SKIP
  137. """
  138. if not isinstance(new_chronometer, texts.Chronometer):
  139. raise TypeError(_('only Chronometer instances can be added'))
  140. _globals.vg_chronometers.append(new_chronometer)
  141. def add_group(new_group: 'group.SquareGroup'):
  142. """
  143. Adds a new group to the videogame.
  144. Parameters:
  145. new_group: the character to be added
  146. Examples:
  147. >>> p = character.Character('protagonist.png')
  148. >>> rectangle = group.RectangleGroup(p, 4, 2)
  149. >>> add_group(rectangle) # doctest: +SKIP
  150. """
  151. if not isinstance(new_group, groups.Group):
  152. raise TypeError(_('only Group instances can be added'))
  153. _globals.vg_groups.append(new_group)
  154. for vg_character in new_group.sprites():
  155. if vg_character in _globals.vg_characters:
  156. _globals.vg_characters.remove(vg_character)
  157. def add_levelbar(new_levelbar: 'widgets.LevelBar'):
  158. """
  159. Adds a new level bar to the videogame.
  160. Parameters:
  161. new_levelbar: the level bar to be added
  162. Examples:
  163. >>> lb = widgets.LevelBar(3)
  164. >>> add_levelbar(lb) # doctest: +SKIP
  165. """
  166. if not isinstance(new_levelbar, widgets.LevelBar):
  167. raise TypeError(_('only LevelBar instances can be added'))
  168. _globals.vg_levelbars.append(new_levelbar)
  169. def add_scoreboard(new_scoreboard: 'texts.Scoreboard'):
  170. """
  171. Adds a new scoreboard to the videogame.
  172. Parameters:
  173. new_scoreboard: the scoreboard to be added
  174. Examples:
  175. >>> sc = texts.Scoreboard('Test')
  176. >>> add_scoreboard(sc) # doctest: +SKIP
  177. """
  178. if not isinstance(new_scoreboard, texts.Scoreboard):
  179. raise TypeError(_('only Scoreboard instances can be added'))
  180. _globals.vg_scoreboards.append(new_scoreboard)
  181. def add_text(new_text: 'texts.Text'):
  182. """
  183. Adds a new text to the videogame.
  184. Parameters:
  185. new_text: the text to be added
  186. Examples:
  187. >>> text = texts.Text('Test')
  188. >>> add_text(text) # doctest: +SKIP
  189. """
  190. if not isinstance(new_text, texts.Text):
  191. raise TypeError(_('only Text instances can be added'))
  192. _globals.vg_texts.append(new_text)
  193. def init():
  194. """
  195. Starts the pygame engine if it is not started.
  196. Examples:
  197. >>> init()
  198. """
  199. if not pygame.get_init():
  200. pygame.init()
  201. _globals._window = pygame.display.set_mode(
  202. (settings.WINDOW_WIDTH, settings.WINDOW_HEIGHT)
  203. )
  204. pygame.key.set_repeat(settings.KEY_DELAY, settings.KEY_INTERVAL)
  205. def is_running() -> bool:
  206. """
  207. Checks if the game is running. Useful in the main loop.
  208. Returns:
  209. True if the game is running. False otherwise.
  210. Examples:
  211. >>> state = is_running()
  212. """
  213. return running
  214. def listen_events():
  215. """
  216. Listens for events coming from the keyboard and window clicks.
  217. Examples:
  218. >>> simulate_key_press('ESCAPE')
  219. >>> listen_events()
  220. """
  221. _globals._events = _get_events()
  222. _globals._keys = _get_keys()
  223. for event in _globals._events:
  224. match event.type:
  225. case pygame.KEYDOWN:
  226. if event.key == pygame.K_ESCAPE:
  227. quit()
  228. case pygame.QUIT:
  229. quit()
  230. def get_color(x: int, y: int) -> Tuple[int, int, int]:
  231. """
  232. Gets the RGB components for a point in the game window.
  233. Parameters:
  234. x: Position on the abscissa axis
  235. y: Position on the ordinate axis
  236. Returns:
  237. A tuple of integers with the RGB components
  238. Examples:
  239. >>> color = get_color(100, 200)
  240. """
  241. if not type(x) == int or not type(y) == int:
  242. raise TypeError(
  243. _('the coordinates of the point must be a non-negative integers')
  244. )
  245. if not (0 <= x <= settings.WINDOW_WIDTH) or not (
  246. 0 <= y <= settings.WINDOW_HEIGHT
  247. ):
  248. raise ValueError(
  249. _('the coordinates of the point must be a non-negative integers')
  250. )
  251. if _globals._window:
  252. return tuple(_globals._window.get_at((x, y))[:-1])
  253. return None
  254. def paint(
  255. sprite: Type['scene.Scene']
  256. | Type['character.Character']
  257. | Type['texts.Text'],
  258. ):
  259. """
  260. Paints multiple sprite types on the main window.
  261. Parameters:
  262. sprite: the element to be painted
  263. """
  264. # TODO: check sprite type and show error if no image or no rect
  265. if sprite.image and sprite.rect:
  266. _globals._window.blit(sprite.image, sprite.rect)
  267. def paint_character(sprite: Type['character.Character']):
  268. """
  269. Paints a character on the main window.
  270. Parameters:
  271. sprite: the element to be painted
  272. """
  273. if sprite.visible:
  274. paint(sprite)
  275. def paint_group(group: Type['groups.Group']):
  276. """
  277. Paints a group of sprites on the main window.
  278. Parameters:
  279. group: the group of sprites to be painted.
  280. """
  281. for sprite_item in group.characters:
  282. sprite_item._update_rect_position()
  283. paint(sprite_item)
  284. def paint_scene(sprite: Type['scene.Scene']):
  285. """
  286. Paints a scene on the main window.
  287. Parameters:
  288. sprite: the element to be painted
  289. """
  290. # TODO: check sprite type
  291. if hasattr(sprite, 'background'):
  292. _globals._window.fill(sprite.background)
  293. paint(sprite)
  294. def paint_text(lines: Type['texts.Text'] | list):
  295. """
  296. Paints a text on the main window.
  297. Parameters:
  298. text: the element to be painted
  299. """
  300. if isinstance(lines, jogai.texts.Text):
  301. lines = [lines]
  302. for line in lines:
  303. if line.visible:
  304. paint(line)
  305. def set_caption(caption: str):
  306. """
  307. Sets the caption for the videogame.
  308. Parameters:
  309. caption: caption to be setted
  310. Examples:
  311. >>> set_caption('My videogame')
  312. """
  313. if not isinstance(caption, str):
  314. raise TypeError(_('the caption for the videogame must be a string'))
  315. _globals.vg_title = caption
  316. pygame.display.set_caption(caption)
  317. def set_characters(data_characters: dict):
  318. (
  319. _globals.vg_characters,
  320. _globals.vg_groups,
  321. ) = character._load_characters_and_groups(data_characters)
  322. def set_gravity(gravity: int):
  323. """
  324. Sets the gravity in the videogame.
  325. Parameters:
  326. gravity: Gravity force to be setted.
  327. Examples:
  328. >>> set_gravity(5)
  329. """
  330. if not type(gravity) == int:
  331. raise TypeError(_('the gravity must be an integer'))
  332. _globals._gravity = gravity
  333. def set_marks(
  334. x_gap: int = settings.MARKS_DEFAULT_X_GAP,
  335. y_gap: int = settings.MARKS_DEFAULT_Y_GAP,
  336. x_color: tuple = settings.MARKS_DEFAULT_X_COLOR,
  337. y_color: tuple = settings.MARKS_DEFAULT_Y_COLOR,
  338. width: int = settings.MARKS_DEFAULT_WIDTH,
  339. debug: bool = False,
  340. ):
  341. """
  342. Sets distance mark lines and position information along both axis.
  343. Parameters:
  344. x_gap: The gap distance on abscissa axis.
  345. y_gap: The gap distance on ordinate axis.
  346. x_color: The line color on abscissa axis.
  347. y_color: The line color on ordinate axis.
  348. width: The line width.
  349. debug: If True, extra detailed information is shown.
  350. """
  351. _globals.marks = utils.Marks(x_gap, y_gap, x_color, y_color, width, debug)
  352. def set_scene(data_scene):
  353. _globals.vg_scene = scene._load_scene(data_scene)
  354. def set_texts(data_texts):
  355. _globals.vg_texts = texts._load_texts(data_texts)
  356. def prepare(environment_name: str):
  357. """
  358. Decorator function to prepare the game environments before entering the main loop.
  359. Examples:
  360. >>> init()
  361. >>> prepare()
  362. """
  363. def decorator(environment):
  364. _globals.environments.update({environment_name: environment})
  365. return decorator
  366. def load_environment(
  367. new_environment: str,
  368. items_to_transfer: list | dict = [],
  369. to_export: bool = False,
  370. ):
  371. if isinstance(items_to_transfer, dict):
  372. items_to_transfer = [
  373. items_to_transfer,
  374. ]
  375. if not to_export:
  376. if not new_environment in _globals.environments.keys():
  377. raise KeyError(_('the environment name does not exist'))
  378. if not _globals._window:
  379. init()
  380. _globals.vg_chronometers = []
  381. _globals.vg_levelbars = []
  382. _globals.vg_scoreboards = []
  383. vg_data = utils._load_yaml_from_filename(f'{new_environment}.yaml')
  384. _load_data(vg_data)
  385. character_names = [c.name for c in _globals.vg_characters]
  386. for item in items_to_transfer:
  387. match item:
  388. case character.Character():
  389. if item.name in character_names:
  390. character_names.pop(character_names.index(item.name))
  391. add_character(item)
  392. case groups.Group():
  393. add_group(item)
  394. case texts.Chronometer():
  395. add_chronometer(item)
  396. case texts.Scoreboard():
  397. add_scoreboard(item)
  398. case texts.Text():
  399. add_text(item)
  400. case widgets.LevelBar():
  401. add_levelbar(item)
  402. case dict():
  403. for name, value in item.items():
  404. add_attribute(name, value)
  405. if _globals.vg_characters:
  406. _calculate_initial_collisions()
  407. _globals.character_focus = _globals.vg_characters[0]
  408. builtins.__dict__.update({_('current_scene'): _globals.vg_scene})
  409. for vg_text in _globals.vg_texts:
  410. # Check if character names exist in the predefined builtins
  411. # if not vg_character.name in _globals.predefined_builtins:
  412. # if not vg_text.name in builtins.__dict__.keys():
  413. builtins.__dict__[vg_text.name] = vg_text
  414. for vg_character in _globals.vg_characters:
  415. # Check if character names exist in the predefined builtins
  416. # if not vg_character.name in _globals.predefined_builtins:
  417. # if not vg_character.name in builtins.__dict__.keys():
  418. builtins.__dict__[vg_character.name] = vg_character
  419. for vg_group in _globals.vg_groups:
  420. # Check if group names exist in the predefined builtins
  421. # if not vg_group.name in _globals.predefined_builtins:
  422. # if not vg_group.name in builtins.__dict__.keys():
  423. builtins.__dict__[vg_group.name] = vg_group
  424. for name, value in _globals.vg_attributes.items():
  425. builtins.__dict__[name] = value
  426. _globals.current_environment = new_environment
  427. def stop():
  428. """
  429. Finishes the infinite videogame loop.
  430. """
  431. global running
  432. running = False
  433. def update(to_quit: bool = False):
  434. """
  435. Updates the display surface and listen events.
  436. """
  437. if _globals.vg_scene:
  438. if _globals.vg_characters:
  439. _globals.vg_scene._set_abscissa_position(
  440. _globals.character_focus.x
  441. )
  442. paint_scene(_globals.vg_scene)
  443. if _globals.vg_characters:
  444. for pos, vg_character in enumerate(_globals.vg_characters):
  445. tracking = (
  446. True if vg_character == _globals.character_focus else False
  447. )
  448. vg_character._update_rect_position(tracking=tracking)
  449. paint_character(vg_character)
  450. vg_character.update()
  451. if _globals.vg_groups:
  452. for vg_group in _globals.vg_groups:
  453. paint_group(vg_group)
  454. vg_group.update()
  455. if _globals.vg_levelbars:
  456. for levelbar_item in _globals.vg_levelbars:
  457. levelbar_item.draw()
  458. if _globals.vg_scoreboards:
  459. for scoreboard_item in _globals.vg_scoreboards:
  460. scoreboard_item.draw()
  461. if _globals.vg_chronometers:
  462. for chronometers_item in _globals.vg_chronometers:
  463. chronometers_item.draw()
  464. chronometers_item.update()
  465. if _globals.vg_texts:
  466. for texts_item in _globals.vg_texts:
  467. if texts_item.visible:
  468. texts_item._update_rect_position()
  469. texts_item.draw()
  470. if _globals.visible_inventory:
  471. _globals.visible_inventory.draw()
  472. if _globals.marks:
  473. _globals.marks.draw()
  474. pygame.display.flip()
  475. _globals._clock.tick(settings.FPS)
  476. listen_events()
  477. if not to_quit:
  478. _globals.environments[_globals.current_environment]()
  479. def quit(wait: int = settings.DEFAULT_TIME_TO_QUIT):
  480. """
  481. Exits the game.
  482. Parameters:
  483. wait: Seconds to wait before exiting
  484. Examples:
  485. >>> quit(1)
  486. """
  487. update(to_quit=True)
  488. if type(wait) == int and wait > 0:
  489. time.sleep(wait)
  490. pygame.quit()
  491. stop()