Merge branch 'master' into map_generation

# Conflicts:
#	squirrelbattle/game.py
#	squirrelbattle/interfaces.py
#	squirrelbattle/tests/game_test.py
This commit is contained in:
Yohann D'ANELLO
2021-01-08 17:02:10 +01:00
49 changed files with 2848 additions and 593 deletions

View File

@ -3,7 +3,7 @@
from json import JSONDecodeError
from random import randint
from typing import Any, Optional
from typing import Any, Optional, List
import curses
import json
import os
@ -23,7 +23,8 @@ class Game:
"""
The game object controls all actions in the game.
"""
map: Map
maps: List[Map]
map_index: int
player: Player
screen: Any
# display_actions is a display interface set by the bootstrapper
@ -31,10 +32,11 @@ class Game:
def __init__(self) -> None:
"""
Init the game.
Initiates the game.
"""
self.state = GameMode.MAINMENU
self.waiting_for_friendly_key = False
self.is_in_store_menu = True
self.settings = Settings()
self.settings.load_settings()
self.settings.write_settings()
@ -49,8 +51,11 @@ class Game:
def new_game(self) -> None:
"""
Create a new game on the screen.
Creates a new game on the screen.
"""
# TODO generate a new map procedurally
self.maps = []
self.map_index = 0
self.map = broguelike.Generator().run()
self.map.logs = self.logs
self.logs.clear()
@ -60,20 +65,44 @@ class Game:
self.map.spawn_random_entities(randint(3, 10))
self.inventory_menu.update_player(self.player)
def run(self, screen: Any) -> None:
@property
def map(self) -> Map:
"""
Return the current map where the user is.
"""
return self.maps[self.map_index]
@map.setter
def map(self, m: Map) -> None:
"""
Redefine the current map.
"""
if len(self.maps) == self.map_index:
# Insert new map
self.maps.append(m)
# Redefine the current map
self.maps[self.map_index] = m
def run(self, screen: Any) -> None: # pragma no cover
"""
Main infinite loop.
We wait for the player's action, then we do what that should be done
when the given key gets pressed.
We wait for the player's action, then we do what should be done
when a key gets pressed.
"""
while True: # pragma no cover
screen.refresh()
while True:
screen.erase()
screen.refresh()
screen.noutrefresh()
self.display_actions(DisplayActions.REFRESH)
key = screen.getkey()
curses.doupdate()
try:
key = screen.getkey()
except KeyboardInterrupt:
exit(0)
return
if key == "KEY_MOUSE":
_ignored1, x, y, _ignored2, _ignored3 = curses.getmouse()
self.display_actions(DisplayActions.MOUSE, y, x)
_ignored1, x, y, _ignored2, attr = curses.getmouse()
self.display_actions(DisplayActions.MOUSE, y, x, attr)
else:
self.handle_key_pressed(
KeyValues.translate_key(key, self.settings), key)
@ -81,7 +110,7 @@ class Game:
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str = '')\
-> None:
"""
Indicates what should be done when the given key is pressed,
Indicates what should be done when a given key is pressed,
according to the current game state.
"""
if self.message:
@ -103,36 +132,95 @@ class Game:
self.settings_menu.handle_key_pressed(key, raw_key, self)
elif self.state == GameMode.STORE:
self.handle_key_pressed_store(key)
elif self.state == GameMode.CREDITS:
self.state = GameMode.MAINMENU
self.display_actions(DisplayActions.REFRESH)
def handle_key_pressed_play(self, key: KeyValues) -> None:
def handle_key_pressed_play(self, key: KeyValues) -> None: # noqa: C901
"""
In play mode, arrows or zqsd move the main character.
"""
if key == KeyValues.UP:
if self.player.move_up():
self.map.tick()
self.map.tick(self.player)
elif key == KeyValues.DOWN:
if self.player.move_down():
self.map.tick()
self.map.tick(self.player)
elif key == KeyValues.LEFT:
if self.player.move_left():
self.map.tick()
self.map.tick(self.player)
elif key == KeyValues.RIGHT:
if self.player.move_right():
self.map.tick()
self.map.tick(self.player)
elif key == KeyValues.INVENTORY:
self.state = GameMode.INVENTORY
self.display_actions(DisplayActions.UPDATE)
elif key == KeyValues.USE and self.player.equipped_main:
if self.player.equipped_main:
self.player.equipped_main.use()
if self.player.equipped_secondary:
self.player.equipped_secondary.use()
elif key == KeyValues.SPACE:
self.state = GameMode.MAINMENU
elif key == KeyValues.CHAT:
# Wait for the direction of the friendly entity
self.waiting_for_friendly_key = True
elif key == KeyValues.WAIT:
self.map.tick(self.player)
elif key == KeyValues.LADDER:
self.handle_ladder()
def handle_ladder(self) -> None:
"""
The player pressed the ladder key to switch map
"""
# On a ladder, we switch level
y, x = self.player.y, self.player.x
if not self.map.tiles[y][x].is_ladder():
return
# We move up on the ladder of the beginning,
# down at the end of the stage
move_down = y != self.map.start_y and x != self.map.start_x
old_map = self.map
self.map_index += 1 if move_down else -1
if self.map_index == -1:
self.map_index = 0
return
while self.map_index >= len(self.maps):
# TODO: generate a new map
self.maps.append(Map.load(ResourceManager.get_asset_path(
"example_map_2.txt")))
new_map = self.map
new_map.floor = self.map_index
old_map.remove_entity(self.player)
new_map.add_entity(self.player)
if move_down:
self.player.move(self.map.start_y, self.map.start_x)
self.logs.add_message(
_("The player climbs down to the floor {floor}.")
.format(floor=-self.map_index))
else:
# Find the ladder of the end of the game
ladder_y, ladder_x = -1, -1
for y in range(self.map.height):
for x in range(self.map.width):
if (y, x) != (self.map.start_y, self.map.start_x) \
and self.map.tiles[y][x].is_ladder():
ladder_y, ladder_x = y, x
break
self.player.move(ladder_y, ladder_x)
self.logs.add_message(
_("The player climbs up the floor {floor}.")
.format(floor=-self.map_index))
self.display_actions(DisplayActions.UPDATE)
def handle_friendly_entity_chat(self, key: KeyValues) -> None:
"""
If the player is talking to a friendly entity, we get the direction
where the entity is, then we interact with it.
If the player tries to talk to a friendly entity, the game waits for
a directional key to be pressed, verifies there is a friendly entity
in that direction and then lets the player interact with it.
"""
if not self.waiting_for_friendly_key:
return
@ -160,7 +248,9 @@ class Game:
self.logs.add_message(msg)
if entity.is_merchant():
self.state = GameMode.STORE
self.is_in_store_menu = True
self.store_menu.update_merchant(entity)
self.display_actions(DisplayActions.UPDATE)
def handle_key_pressed_inventory(self, key: KeyValues) -> None:
"""
@ -189,26 +279,37 @@ class Game:
"""
In a store menu, we can buy items or close the menu.
"""
if key == KeyValues.SPACE:
menu = self.store_menu if self.is_in_store_menu else self.inventory_menu
if key == KeyValues.SPACE or key == KeyValues.INVENTORY:
self.state = GameMode.PLAY
elif key == KeyValues.UP:
self.store_menu.go_up()
menu.go_up()
elif key == KeyValues.DOWN:
self.store_menu.go_down()
if self.store_menu.values and not self.player.dead:
menu.go_down()
elif key == KeyValues.LEFT:
self.is_in_store_menu = False
self.display_actions(DisplayActions.UPDATE)
elif key == KeyValues.RIGHT:
self.is_in_store_menu = True
self.display_actions(DisplayActions.UPDATE)
if menu.values and not self.player.dead:
if key == KeyValues.ENTER:
item = self.store_menu.validate()
flag = item.be_sold(self.player, self.store_menu.merchant)
item = menu.validate()
owner = self.store_menu.merchant if self.is_in_store_menu \
else self.player
buyer = self.player if self.is_in_store_menu \
else self.store_menu.merchant
flag = item.be_sold(buyer, owner)
if not flag:
self.message = _("You do not have enough money")
self.display_actions(DisplayActions.UPDATE)
self.message = _("The buyer does not have enough money")
self.display_actions(DisplayActions.UPDATE)
# Ensure that the cursor has a good position
self.store_menu.position = min(self.store_menu.position,
len(self.store_menu.values) - 1)
menu.position = min(menu.position, len(menu.values) - 1)
def handle_key_pressed_main_menu(self, key: KeyValues) -> None:
"""
In the main menu, we can navigate through options.
In the main menu, we can navigate through different options.
"""
if key == KeyValues.DOWN:
self.main_menu.go_down()
@ -233,16 +334,18 @@ class Game:
def save_state(self) -> dict:
"""
Saves the game to a dictionary
Saves the game to a dictionary.
"""
return self.map.save_state()
return dict(map_index=self.map_index,
maps=[m.save_state() for m in self.maps])
def load_state(self, d: dict) -> None:
"""
Loads the game from a dictionary
Loads the game from a dictionary.
"""
try:
self.map.load_state(d)
self.map_index = d["map_index"]
self.maps = [Map().load_state(map_dict) for map_dict in d["maps"]]
except KeyError:
self.message = _("Some keys are missing in your save file.\n"
"Your save seems to be corrupt. It got deleted.")
@ -259,11 +362,13 @@ class Game:
return
self.player = players[0]
self.map.compute_visibility(self.player.y, self.player.x,
self.player.vision)
self.display_actions(DisplayActions.UPDATE)
def load_game(self) -> None:
"""
Loads the game from a file
Loads the game from a file.
"""
file_path = ResourceManager.get_config_path("save.json")
if os.path.isfile(file_path):
@ -280,7 +385,7 @@ class Game:
def save_game(self) -> None:
"""
Saves the game to a file
Saves the game to a file.
"""
with open(ResourceManager.get_config_path("save.json"), "w") as f:
f.write(json.dumps(self.save_state()))