Rename dungeonbattle to squirrelbattle
This commit is contained in:
0
squirrelbattle/__init__.py
Normal file
0
squirrelbattle/__init__.py
Normal file
11
squirrelbattle/assets/ascii_art.txt
Normal file
11
squirrelbattle/assets/ascii_art.txt
Normal file
@ -0,0 +1,11 @@
|
||||
██████ █████ █ ██ ██▓ ██▀███ ██▀███ ▓█████ ██▓ ▄▄▄▄ ▄▄▄ ▄▄▄█████▓▄▄▄█████▓ ██▓ ▓█████
|
||||
▒██ ▒ ▒██▓ ██▒ ██ ▓██▒▓██▒▓██ ▒ ██▒▓██ ▒ ██▒▓█ ▀ ▓██▒ ▓█████▄ ▒████▄ ▓ ██▒ ▓▒▓ ██▒ ▓▒▓██▒ ▓█ ▀
|
||||
░ ▓██▄ ▒██▒ ██░▓██ ▒██░▒██▒▓██ ░▄█ ▒▓██ ░▄█ ▒▒███ ▒██░ ▒██▒ ▄██▒██ ▀█▄ ▒ ▓██░ ▒░▒ ▓██░ ▒░▒██░ ▒███
|
||||
▒ ██▒░██ █▀ ░▓▓█ ░██░░██░▒██▀▀█▄ ▒██▀▀█▄ ▒▓█ ▄ ▒██░ ▒██░█▀ ░██▄▄▄▄██░ ▓██▓ ░ ░ ▓██▓ ░ ▒██░ ▒▓█ ▄
|
||||
▒██████▒▒░▒███▒█▄ ▒▒█████▓ ░██░░██▓ ▒██▒░██▓ ▒██▒░▒████▒░██████▒ ░▓█ ▀█▓ ▓█ ▓██▒ ▒██▒ ░ ▒██▒ ░ ░██████▒░▒████▒
|
||||
▒ ▒▓▒ ▒ ░░░ ▒▒░ ▒ ░▒▓▒ ▒ ▒ ░▓ ░ ▒▓ ░▒▓░░ ▒▓ ░▒▓░░░ ▒░ ░░ ▒░▓ ░ ░▒▓███▀▒ ▒▒ ▓▒█░ ▒ ░░ ▒ ░░ ░ ▒░▓ ░░░ ▒░ ░
|
||||
░ ░▒ ░ ░ ░ ▒░ ░ ░░▒░ ░ ░ ▒ ░ ░▒ ░ ▒░ ░▒ ░ ▒░ ░ ░ ░░ ░ ▒ ░ ▒░▒ ░ ▒ ▒▒ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░
|
||||
░ ░ ░ ░ ░ ░░░ ░ ░ ▒ ░ ░░ ░ ░░ ░ ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░
|
||||
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
|
||||
░
|
||||
|
18
squirrelbattle/assets/example_map.txt
Normal file
18
squirrelbattle/assets/example_map.txt
Normal file
@ -0,0 +1,18 @@
|
||||
1 6
|
||||
####### #############
|
||||
#.....# #...........#
|
||||
#.....# #####...........#
|
||||
#.....# #...............#
|
||||
#.##### #.###...........#
|
||||
#.# #.# #...........#
|
||||
#.# #.# #############
|
||||
#.# #.#
|
||||
#.#### #.#
|
||||
#....# #.#
|
||||
####.###################.#
|
||||
#.....................# #################
|
||||
#.....................# #...............#
|
||||
#.....................#######...............#
|
||||
#...........................................#
|
||||
#.....................#######...............#
|
||||
####################### #################
|
41
squirrelbattle/assets/example_map_2.txt
Normal file
41
squirrelbattle/assets/example_map_2.txt
Normal file
@ -0,0 +1,41 @@
|
||||
1 17
|
||||
########### #########
|
||||
#.........# #.......#
|
||||
#.........# ############.......#
|
||||
#.........###############..........#.......##############
|
||||
#.........#........................#....................#
|
||||
#.........#.............#..........#.......#............#
|
||||
########.########.............#..................#............#
|
||||
#.........# #.............####.#######.......#............#
|
||||
#.........# #.............##.........######################
|
||||
#.........# #####.##########.........# ###########
|
||||
#.........# #......# #.........# #.........#
|
||||
########.##########......# #.........# #.........#
|
||||
#...........##......# #.........# #.........#
|
||||
#...........##......# #.........# #.........#
|
||||
#...........##......# #.........# ################.######
|
||||
#...........##......# #.........# #.................############
|
||||
#...........##......# ########.########.......#.........#..........#
|
||||
#...........##......# #...............#.......#.........#..........#
|
||||
#...........######### #...............#.......#.........#..........#
|
||||
#...........# #...............#.......#....................#
|
||||
#####.####### #.......................#.........#..........#
|
||||
#.........# #...............###################..........#
|
||||
#.........############ #...............# #..........#
|
||||
#.........#..........# #...............# ############
|
||||
#....................#####.###########.#############
|
||||
########.#########...................# #.............#
|
||||
#........# #..........#........# #.............#########
|
||||
#........# ######.##########........# #.............#.......#
|
||||
#........# #..........# #........# #.....................#
|
||||
#........# #..........# #........# #.............#.......#
|
||||
#........# #..........# #........# #.............#.......#
|
||||
#........# #..........# #........# #.............#.......#
|
||||
#........# #..........#########.##### #.............#.......#
|
||||
#........# #..........#.........# ##########.############.#######
|
||||
#........# #..........#.........# #..............# #..........#
|
||||
########## #..........#.........# #..............# #..........#
|
||||
############.........# #..............# #..........#
|
||||
#.........# #..............# #..........#
|
||||
########### #..............# #..........#
|
||||
################ ############
|
21
squirrelbattle/bootstrap.py
Normal file
21
squirrelbattle/bootstrap.py
Normal file
@ -0,0 +1,21 @@
|
||||
from squirrelbattle.game import Game
|
||||
from squirrelbattle.display.display_manager import DisplayManager
|
||||
from squirrelbattle.term_manager import TermManager
|
||||
|
||||
|
||||
class Bootstrap:
|
||||
"""
|
||||
The bootstrap object is used to bootstrap the game so that it starts
|
||||
properly.
|
||||
(It was initially created to avoid circular imports between the Game and
|
||||
Display classes)
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def run_game():
|
||||
with TermManager() as term_manager: # pragma: no cover
|
||||
game = Game()
|
||||
game.new_game()
|
||||
display = DisplayManager(term_manager.screen, game)
|
||||
game.display_actions = display.handle_display_action
|
||||
game.run(term_manager.screen)
|
0
squirrelbattle/display/__init__.py
Normal file
0
squirrelbattle/display/__init__.py
Normal file
52
squirrelbattle/display/display.py
Normal file
52
squirrelbattle/display/display.py
Normal file
@ -0,0 +1,52 @@
|
||||
import curses
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from squirrelbattle.display.texturepack import TexturePack
|
||||
from squirrelbattle.tests.screen import FakePad
|
||||
|
||||
|
||||
class Display:
|
||||
x: int
|
||||
y: int
|
||||
width: int
|
||||
height: int
|
||||
pad: Any
|
||||
|
||||
def __init__(self, screen: Any, pack: Optional[TexturePack] = None):
|
||||
self.screen = screen
|
||||
self.pack = pack or TexturePack.get_pack("ascii")
|
||||
|
||||
def newpad(self, height: int, width: int) -> Union[FakePad, Any]:
|
||||
return curses.newpad(height, width) if self.screen else FakePad()
|
||||
|
||||
def init_pair(self, number: int, foreground: int, background: int) -> None:
|
||||
return curses.init_pair(number, foreground, background) \
|
||||
if self.screen else None
|
||||
|
||||
def color_pair(self, number: int) -> int:
|
||||
return curses.color_pair(number) if self.screen else 0
|
||||
|
||||
def resize(self, y: int, x: int, height: int, width: int,
|
||||
resize_pad: bool = True) -> None:
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
if hasattr(self, "pad") and resize_pad:
|
||||
self.pad.resize(self.height - 1, self.width - 1)
|
||||
|
||||
def refresh(self, *args, resize_pad: bool = True) -> None:
|
||||
if len(args) == 4:
|
||||
self.resize(*args, resize_pad)
|
||||
self.display()
|
||||
|
||||
def display(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def rows(self) -> int:
|
||||
return curses.LINES if self.screen else 42
|
||||
|
||||
@property
|
||||
def cols(self) -> int:
|
||||
return curses.COLS if self.screen else 42
|
70
squirrelbattle/display/display_manager.py
Normal file
70
squirrelbattle/display/display_manager.py
Normal file
@ -0,0 +1,70 @@
|
||||
import curses
|
||||
from squirrelbattle.display.mapdisplay import MapDisplay
|
||||
from squirrelbattle.display.statsdisplay import StatsDisplay
|
||||
from squirrelbattle.display.menudisplay import SettingsMenuDisplay, \
|
||||
MainMenuDisplay
|
||||
from squirrelbattle.display.texturepack import TexturePack
|
||||
from typing import Any
|
||||
from squirrelbattle.game import Game, GameMode
|
||||
from squirrelbattle.enums import DisplayActions
|
||||
|
||||
|
||||
class DisplayManager:
|
||||
|
||||
def __init__(self, screen: Any, g: Game):
|
||||
self.game = g
|
||||
self.screen = screen
|
||||
pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK)
|
||||
self.mapdisplay = MapDisplay(screen, pack)
|
||||
self.statsdisplay = StatsDisplay(screen, pack)
|
||||
self.mainmenudisplay = MainMenuDisplay(self.game.main_menu,
|
||||
screen, pack)
|
||||
self.settingsmenudisplay = SettingsMenuDisplay(screen, pack)
|
||||
self.displays = [self.statsdisplay, self.mapdisplay,
|
||||
self.mainmenudisplay, self.settingsmenudisplay]
|
||||
self.update_game_components()
|
||||
|
||||
def handle_display_action(self, action: DisplayActions) -> None:
|
||||
if action == DisplayActions.REFRESH:
|
||||
self.refresh()
|
||||
elif action == DisplayActions.UPDATE:
|
||||
self.update_game_components()
|
||||
|
||||
def update_game_components(self) -> None:
|
||||
for d in self.displays:
|
||||
d.pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK)
|
||||
self.mapdisplay.update_map(self.game.map)
|
||||
self.statsdisplay.update_player(self.game.player)
|
||||
self.settingsmenudisplay.update_menu(self.game.settings_menu)
|
||||
|
||||
def refresh(self) -> None:
|
||||
if self.game.state == GameMode.PLAY:
|
||||
# The map pad has already the good size
|
||||
self.mapdisplay.refresh(0, 0, self.rows * 4 // 5, self.cols,
|
||||
resize_pad=False)
|
||||
self.statsdisplay.refresh(self.rows * 4 // 5, 0,
|
||||
self.rows // 5, self.cols)
|
||||
if self.game.state == GameMode.MAINMENU:
|
||||
self.mainmenudisplay.refresh(0, 0, self.rows, self.cols)
|
||||
if self.game.state == GameMode.SETTINGS:
|
||||
self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols - 1)
|
||||
self.resize_window()
|
||||
|
||||
def resize_window(self) -> bool:
|
||||
"""
|
||||
If the window got resized, ensure that the screen size got updated.
|
||||
"""
|
||||
y, x = self.screen.getmaxyx() if self.screen else (0, 0)
|
||||
if self.screen and curses.is_term_resized(self.rows,
|
||||
self.cols): # pragma: nocover
|
||||
curses.resizeterm(y, x)
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def rows(self) -> int:
|
||||
return curses.LINES if self.screen else 42
|
||||
|
||||
@property
|
||||
def cols(self) -> int:
|
||||
return curses.COLS if self.screen else 42
|
39
squirrelbattle/display/mapdisplay.py
Normal file
39
squirrelbattle/display/mapdisplay.py
Normal file
@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python
|
||||
from squirrelbattle.interfaces import Map
|
||||
from .display import Display
|
||||
|
||||
|
||||
class MapDisplay(Display):
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
def update_map(self, m: Map) -> None:
|
||||
self.map = m
|
||||
self.pad = self.newpad(m.height, self.pack.tile_width * m.width + 1)
|
||||
|
||||
def update_pad(self) -> None:
|
||||
self.init_pair(1, self.pack.tile_fg_color, self.pack.tile_bg_color)
|
||||
self.init_pair(2, self.pack.entity_fg_color, self.pack.entity_bg_color)
|
||||
self.pad.addstr(0, 0, self.map.draw_string(self.pack),
|
||||
self.color_pair(1))
|
||||
for e in self.map.entities:
|
||||
self.pad.addstr(e.y, self.pack.tile_width * e.x,
|
||||
self.pack[e.name.upper()], self.color_pair(2))
|
||||
|
||||
def display(self) -> None:
|
||||
y, x = self.map.currenty, self.pack.tile_width * self.map.currentx
|
||||
deltay, deltax = (self.height // 2) + 1, (self.width // 2) + 1
|
||||
pminrow, pmincol = y - deltay, x - deltax
|
||||
sminrow, smincol = max(-pminrow, 0), max(-pmincol, 0)
|
||||
deltay, deltax = self.height - deltay, self.width - deltax
|
||||
smaxrow = self.map.height - (y + deltay) + self.height - 1
|
||||
smaxrow = min(smaxrow, self.height - 1)
|
||||
smaxcol = self.pack.tile_width * self.map.width - \
|
||||
(x + deltax) + self.width - 1
|
||||
smaxcol = min(smaxcol, self.width - 1)
|
||||
pminrow = max(0, min(self.map.height, pminrow))
|
||||
pmincol = max(0, min(self.pack.tile_width * self.map.width, pmincol))
|
||||
self.pad.clear()
|
||||
self.update_pad()
|
||||
self.pad.refresh(pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol)
|
94
squirrelbattle/display/menudisplay.py
Normal file
94
squirrelbattle/display/menudisplay.py
Normal file
@ -0,0 +1,94 @@
|
||||
from typing import List
|
||||
|
||||
from squirrelbattle.menus import Menu, MainMenu
|
||||
from .display import Display
|
||||
|
||||
|
||||
class MenuDisplay(Display):
|
||||
position: int
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
self.menubox = self.newpad(self.rows, self.cols)
|
||||
|
||||
def update_menu(self, menu: Menu) -> None:
|
||||
self.menu = menu
|
||||
self.trueheight = len(self.values)
|
||||
self.truewidth = max([len(a) for a in self.values])
|
||||
|
||||
# Menu values are printed in pad
|
||||
self.pad = self.newpad(self.trueheight, self.truewidth + 2)
|
||||
for i in range(self.trueheight):
|
||||
self.pad.addstr(i, 0, " " + self.values[i])
|
||||
|
||||
def update_pad(self) -> None:
|
||||
for i in range(self.trueheight):
|
||||
self.pad.addstr(i, 0, " " + self.values[i])
|
||||
# set a marker on the selected line
|
||||
self.pad.addstr(self.menu.position, 0, ">")
|
||||
|
||||
def display(self) -> None:
|
||||
cornery = 0 if self.height - 2 >= self.menu.position - 1 \
|
||||
else self.trueheight - self.height + 2 \
|
||||
if self.height - 2 >= self.trueheight - self.menu.position else 0
|
||||
|
||||
# Menu box
|
||||
self.menubox.addstr(0, 0, "┏" + "━" * (self.width - 2) + "┓")
|
||||
for i in range(1, self.height - 1):
|
||||
self.menubox.addstr(i, 0, "┃" + " " * (self.width - 2) + "┃")
|
||||
self.menubox.addstr(self.height - 1, 0,
|
||||
"┗" + "━" * (self.width - 2) + "┛")
|
||||
|
||||
self.menubox.refresh(0, 0, self.y, self.x,
|
||||
self.height + self.y,
|
||||
self.width + self.x)
|
||||
self.update_pad()
|
||||
self.pad.refresh(cornery, 0, self.y + 1, self.x + 2,
|
||||
self.height - 2 + self.y,
|
||||
self.width - 2 + self.x)
|
||||
|
||||
@property
|
||||
def preferred_width(self) -> int:
|
||||
return self.truewidth + 6
|
||||
|
||||
@property
|
||||
def preferred_height(self) -> int:
|
||||
return self.trueheight + 2
|
||||
|
||||
@property
|
||||
def values(self) -> List[str]:
|
||||
return [str(a) for a in self.menu.values]
|
||||
|
||||
|
||||
class SettingsMenuDisplay(MenuDisplay):
|
||||
@property
|
||||
def values(self) -> List[str]:
|
||||
return [a[1][1] + (" : "
|
||||
+ ("?" if self.menu.waiting_for_key else a[1][0])
|
||||
if a[1][0] else "") for a in self.menu.values]
|
||||
|
||||
|
||||
class MainMenuDisplay(Display):
|
||||
def __init__(self, menu: MainMenu, *args):
|
||||
super().__init__(*args)
|
||||
self.menu = menu
|
||||
|
||||
with open("squirrelbattle/assets/ascii_art.txt", "r") as file:
|
||||
self.title = file.read().split("\n")
|
||||
|
||||
self.pad = self.newpad(max(self.rows, len(self.title) + 30),
|
||||
max(len(self.title[0]) + 5, self.cols))
|
||||
|
||||
self.menudisplay = MenuDisplay(self.screen, self.pack)
|
||||
self.menudisplay.update_menu(self.menu)
|
||||
|
||||
def display(self) -> None:
|
||||
for i in range(len(self.title)):
|
||||
self.pad.addstr(4 + i, max(self.width // 2
|
||||
- len(self.title[0]) // 2 - 1, 0), self.title[i])
|
||||
self.pad.refresh(0, 0, self.y, self.x, self.height, self.width)
|
||||
menuwidth = min(self.menudisplay.preferred_width, self.width)
|
||||
menuy, menux = len(self.title) + 8, self.width // 2 - menuwidth // 2 - 1
|
||||
self.menudisplay.refresh(
|
||||
menuy, menux, min(self.menudisplay.preferred_height,
|
||||
self.height - menuy), menuwidth)
|
52
squirrelbattle/display/statsdisplay.py
Normal file
52
squirrelbattle/display/statsdisplay.py
Normal file
@ -0,0 +1,52 @@
|
||||
import curses
|
||||
|
||||
from .display import Display
|
||||
|
||||
from squirrelbattle.entities.player import Player
|
||||
|
||||
|
||||
class StatsDisplay(Display):
|
||||
player: Player
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.pad = self.newpad(self.rows, self.cols)
|
||||
self.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
|
||||
|
||||
def update_player(self, p: Player) -> None:
|
||||
self.player = p
|
||||
|
||||
def update_pad(self) -> None:
|
||||
string = ""
|
||||
for _ in range(self.width - 1):
|
||||
string = string + "-"
|
||||
self.pad.addstr(0, 0, string)
|
||||
string2 = "Player -- LVL {} EXP {}/{} HP {}/{}"\
|
||||
.format(self.player.level, self.player.current_xp,
|
||||
self.player.max_xp, self.player.health,
|
||||
self.player.maxhealth)
|
||||
for _ in range(self.width - len(string2) - 1):
|
||||
string2 = string2 + " "
|
||||
self.pad.addstr(1, 0, string2)
|
||||
string3 = "Stats : STR {} INT {} CHR {} DEX {} CON {}"\
|
||||
.format(self.player.strength,
|
||||
self.player.intelligence, self.player.charisma,
|
||||
self.player.dexterity, self.player.constitution)
|
||||
for _ in range(self.width - len(string3) - 1):
|
||||
string3 = string3 + " "
|
||||
self.pad.addstr(2, 0, string3)
|
||||
|
||||
inventory_str = "Inventaire : " + "".join(
|
||||
self.pack[item.name.upper()] for item in self.player.inventory)
|
||||
self.pad.addstr(3, 0, inventory_str)
|
||||
|
||||
if self.player.dead:
|
||||
self.pad.addstr(4, 0, "VOUS ÊTES MORT",
|
||||
curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT
|
||||
| self.color_pair(3))
|
||||
|
||||
def display(self) -> None:
|
||||
self.pad.clear()
|
||||
self.update_pad()
|
||||
self.pad.refresh(0, 0, self.y, self.x,
|
||||
4 + self.y, self.width + self.x)
|
75
squirrelbattle/display/texturepack.py
Normal file
75
squirrelbattle/display/texturepack.py
Normal file
@ -0,0 +1,75 @@
|
||||
import curses
|
||||
from typing import Any
|
||||
|
||||
|
||||
class TexturePack:
|
||||
_packs = dict()
|
||||
|
||||
name: str
|
||||
tile_width: int
|
||||
tile_fg_color: int
|
||||
tile_bg_color: int
|
||||
entity_fg_color: int
|
||||
entity_bg_color: int
|
||||
EMPTY: str
|
||||
WALL: str
|
||||
FLOOR: str
|
||||
PLAYER: str
|
||||
|
||||
ASCII_PACK: "TexturePack"
|
||||
SQUIRREL_PACK: "TexturePack"
|
||||
|
||||
def __init__(self, name: str, **kwargs):
|
||||
self.name = name
|
||||
self.__dict__.update(**kwargs)
|
||||
TexturePack._packs[name] = self
|
||||
|
||||
def __getitem__(self, item: str) -> Any:
|
||||
return self.__dict__[item]
|
||||
|
||||
@classmethod
|
||||
def get_pack(cls, name: str) -> "TexturePack":
|
||||
return cls._packs[name.lower()]
|
||||
|
||||
@classmethod
|
||||
def get_next_pack_name(cls, name: str) -> str:
|
||||
return "squirrel" if name == "ascii" else "ascii"
|
||||
|
||||
|
||||
TexturePack.ASCII_PACK = TexturePack(
|
||||
name="ascii",
|
||||
tile_width=1,
|
||||
tile_fg_color=curses.COLOR_WHITE,
|
||||
tile_bg_color=curses.COLOR_BLACK,
|
||||
entity_fg_color=curses.COLOR_WHITE,
|
||||
entity_bg_color=curses.COLOR_BLACK,
|
||||
EMPTY=' ',
|
||||
WALL='#',
|
||||
FLOOR='.',
|
||||
PLAYER='@',
|
||||
HEDGEHOG='*',
|
||||
HEART='❤',
|
||||
BOMB='o',
|
||||
RABBIT='Y',
|
||||
BEAVER='_',
|
||||
TEDDY_BEAR='8',
|
||||
)
|
||||
|
||||
TexturePack.SQUIRREL_PACK = TexturePack(
|
||||
name="squirrel",
|
||||
tile_width=2,
|
||||
tile_fg_color=curses.COLOR_WHITE,
|
||||
tile_bg_color=curses.COLOR_BLACK,
|
||||
entity_fg_color=curses.COLOR_WHITE,
|
||||
entity_bg_color=curses.COLOR_WHITE,
|
||||
EMPTY=' ',
|
||||
WALL='🧱',
|
||||
FLOOR='██',
|
||||
PLAYER='🐿 ️',
|
||||
HEDGEHOG='🦔',
|
||||
HEART='💜',
|
||||
BOMB='💣',
|
||||
RABBIT='🐇',
|
||||
BEAVER='🦫',
|
||||
TEDDY_BEAR='🧸',
|
||||
)
|
0
squirrelbattle/entities/__init__.py
Normal file
0
squirrelbattle/entities/__init__.py
Normal file
109
squirrelbattle/entities/items.py
Normal file
109
squirrelbattle/entities/items.py
Normal file
@ -0,0 +1,109 @@
|
||||
from typing import Optional
|
||||
|
||||
from .player import Player
|
||||
from ..interfaces import Entity, FightingEntity, Map
|
||||
|
||||
|
||||
class Item(Entity):
|
||||
"""
|
||||
A class for items
|
||||
"""
|
||||
held: bool
|
||||
held_by: Optional[Player]
|
||||
|
||||
def __init__(self, held: bool = False, held_by: Optional[Player] = None,
|
||||
*args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.held = held
|
||||
self.held_by = held_by
|
||||
|
||||
def drop(self, y: int, x: int) -> None:
|
||||
"""
|
||||
The item is dropped from the inventory onto the floor
|
||||
"""
|
||||
if self.held:
|
||||
self.held_by.inventory.remove(self)
|
||||
self.held = False
|
||||
self.held_by = None
|
||||
self.map.add_entity(self)
|
||||
self.move(y, x)
|
||||
|
||||
def hold(self, player: "Player") -> None:
|
||||
"""
|
||||
The item is taken from the floor and put into the inventory
|
||||
"""
|
||||
self.held = True
|
||||
self.held_by = player
|
||||
self.map.remove_entity(self)
|
||||
player.inventory.append(self)
|
||||
|
||||
def save_state(self) -> dict:
|
||||
"""
|
||||
Saves the state of the entity into a dictionary
|
||||
"""
|
||||
d = super().save_state()
|
||||
d["held"] = self.held
|
||||
return d
|
||||
|
||||
|
||||
class Heart(Item):
|
||||
"""
|
||||
A heart item to return health to the player
|
||||
"""
|
||||
healing: int
|
||||
|
||||
def __init__(self, healing: int = 5, *args, **kwargs):
|
||||
super().__init__(name="heart", *args, **kwargs)
|
||||
self.healing = healing
|
||||
|
||||
def hold(self, player: "Player") -> None:
|
||||
"""
|
||||
When holding a heart, heal the player and don't put item in inventory.
|
||||
"""
|
||||
player.health = min(player.maxhealth, player.health + self.healing)
|
||||
self.map.remove_entity(self)
|
||||
|
||||
def save_state(self) -> dict:
|
||||
"""
|
||||
Saves the state of the header into a dictionary
|
||||
"""
|
||||
d = super().save_state()
|
||||
d["healing"] = self.healing
|
||||
return d
|
||||
|
||||
|
||||
class Bomb(Item):
|
||||
"""
|
||||
A bomb item intended to deal damage to enemies at long range
|
||||
"""
|
||||
damage: int = 5
|
||||
exploding: bool
|
||||
|
||||
def __init__(self, damage: int = 5, exploding: bool = False,
|
||||
*args, **kwargs):
|
||||
super().__init__(name="bomb", *args, **kwargs)
|
||||
self.damage = damage
|
||||
self.exploding = exploding
|
||||
|
||||
def drop(self, x: int, y: int) -> None:
|
||||
super().drop(x, y)
|
||||
self.exploding = True
|
||||
|
||||
def act(self, m: Map) -> None:
|
||||
"""
|
||||
Special exploding action of the bomb
|
||||
"""
|
||||
if self.exploding:
|
||||
for e in m.entities.copy():
|
||||
if abs(e.x - self.x) + abs(e.y - self.y) <= 1 and \
|
||||
isinstance(e, FightingEntity):
|
||||
e.take_damage(self, self.damage)
|
||||
|
||||
def save_state(self) -> dict:
|
||||
"""
|
||||
Saves the state of the bomb into a dictionary
|
||||
"""
|
||||
d = super().save_state()
|
||||
d["exploding"] = self.exploding
|
||||
d["damage"] = self.damage
|
||||
return d
|
92
squirrelbattle/entities/monsters.py
Normal file
92
squirrelbattle/entities/monsters.py
Normal file
@ -0,0 +1,92 @@
|
||||
from random import choice
|
||||
|
||||
from .player import Player
|
||||
from ..interfaces import FightingEntity, Map
|
||||
|
||||
|
||||
class Monster(FightingEntity):
|
||||
"""
|
||||
The class for all monsters in the dungeon.
|
||||
A monster must override this class, and the parameters are given
|
||||
in the __init__ function.
|
||||
An example of the specification of a monster that has a strength of 4
|
||||
and 20 max HP:
|
||||
|
||||
class MyMonster(Monster):
|
||||
def __init__(self, strength: int = 4, maxhealth: int = 20,
|
||||
*args, **kwargs) -> None:
|
||||
super().__init__(name="my_monster", strength=strength,
|
||||
maxhealth=maxhealth, *args, **kwargs)
|
||||
|
||||
With that way, attributes can be overwritten when the entity got created.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def act(self, m: Map) -> None:
|
||||
"""
|
||||
By default, a monster will move randomly where it is possible
|
||||
And if a player is close to the monster, the monster run on the player.
|
||||
"""
|
||||
target = None
|
||||
for entity in m.entities:
|
||||
if self.distance_squared(entity) <= 25 and \
|
||||
isinstance(entity, Player):
|
||||
target = entity
|
||||
break
|
||||
|
||||
# A Dijkstra algorithm has ran that targets the player.
|
||||
# With that way, monsters can simply follow the path.
|
||||
# If they can't move and they are already close to the player,
|
||||
# They hit.
|
||||
if target and (self.y, self.x) in target.paths:
|
||||
# Move to target player
|
||||
next_y, next_x = target.paths[(self.y, self.x)]
|
||||
moved = self.check_move(next_y, next_x, True)
|
||||
if not moved and self.distance_squared(target) <= 1:
|
||||
self.hit(target)
|
||||
else:
|
||||
for _ in range(100):
|
||||
if choice([self.move_up, self.move_down,
|
||||
self.move_left, self.move_right])():
|
||||
break
|
||||
|
||||
|
||||
class Beaver(Monster):
|
||||
"""
|
||||
A beaver monster
|
||||
"""
|
||||
def __init__(self, strength: int = 2, maxhealth: int = 20,
|
||||
*args, **kwargs) -> None:
|
||||
super().__init__(name="beaver", strength=strength,
|
||||
maxhealth=maxhealth, *args, **kwargs)
|
||||
|
||||
|
||||
class Hedgehog(Monster):
|
||||
"""
|
||||
A really mean hedgehog monster
|
||||
"""
|
||||
def __init__(self, strength: int = 3, maxhealth: int = 10,
|
||||
*args, **kwargs) -> None:
|
||||
super().__init__(name="hedgehog", strength=strength,
|
||||
maxhealth=maxhealth, *args, **kwargs)
|
||||
|
||||
|
||||
class Rabbit(Monster):
|
||||
"""
|
||||
A rabbit monster
|
||||
"""
|
||||
def __init__(self, strength: int = 1, maxhealth: int = 15,
|
||||
*args, **kwargs) -> None:
|
||||
super().__init__(name="rabbit", strength=strength,
|
||||
maxhealth=maxhealth, *args, **kwargs)
|
||||
|
||||
|
||||
class TeddyBear(Monster):
|
||||
"""
|
||||
A cute teddybear monster
|
||||
"""
|
||||
def __init__(self, strength: int = 0, maxhealth: int = 50,
|
||||
*args, **kwargs) -> None:
|
||||
super().__init__(name="teddy_bear", strength=strength,
|
||||
maxhealth=maxhealth, *args, **kwargs)
|
115
squirrelbattle/entities/player.py
Normal file
115
squirrelbattle/entities/player.py
Normal file
@ -0,0 +1,115 @@
|
||||
from random import randint
|
||||
from typing import Dict, Tuple
|
||||
|
||||
from ..interfaces import FightingEntity
|
||||
|
||||
|
||||
class Player(FightingEntity):
|
||||
"""
|
||||
The class of the player
|
||||
"""
|
||||
current_xp: int = 0
|
||||
max_xp: int = 10
|
||||
inventory: list
|
||||
paths: Dict[Tuple[int, int], Tuple[int, int]]
|
||||
|
||||
def __init__(self, maxhealth: int = 20, strength: int = 5,
|
||||
intelligence: int = 1, charisma: int = 1, dexterity: int = 1,
|
||||
constitution: int = 1, level: int = 1, current_xp: int = 0,
|
||||
max_xp: int = 10, *args, **kwargs) -> None:
|
||||
super().__init__(name="player", maxhealth=maxhealth, strength=strength,
|
||||
intelligence=intelligence, charisma=charisma,
|
||||
dexterity=dexterity, constitution=constitution,
|
||||
level=level, *args, **kwargs)
|
||||
self.current_xp = current_xp
|
||||
self.max_xp = max_xp
|
||||
self.inventory = list()
|
||||
self.paths = dict()
|
||||
|
||||
def move(self, y: int, x: int) -> None:
|
||||
"""
|
||||
When the player moves, move the camera of the map.
|
||||
"""
|
||||
super().move(y, x)
|
||||
self.map.currenty = y
|
||||
self.map.currentx = x
|
||||
self.recalculate_paths()
|
||||
|
||||
def level_up(self) -> None:
|
||||
"""
|
||||
Add levels to the player as much as it is possible.
|
||||
"""
|
||||
while self.current_xp > self.max_xp:
|
||||
self.level += 1
|
||||
self.current_xp -= self.max_xp
|
||||
self.max_xp = self.level * 10
|
||||
self.health = self.maxhealth
|
||||
# TODO Remove it, that's only fun
|
||||
self.map.spawn_random_entities(randint(3 * self.level,
|
||||
10 * self.level))
|
||||
|
||||
def add_xp(self, xp: int) -> None:
|
||||
"""
|
||||
Add some experience to the player.
|
||||
If the required amount is reached, level up.
|
||||
"""
|
||||
self.current_xp += xp
|
||||
self.level_up()
|
||||
|
||||
# noinspection PyTypeChecker,PyUnresolvedReferences
|
||||
def check_move(self, y: int, x: int, move_if_possible: bool = False) \
|
||||
-> bool:
|
||||
"""
|
||||
If the player tries to move but a fighting entity is there,
|
||||
the player fights this entity.
|
||||
It rewards some XP if it is dead.
|
||||
"""
|
||||
# Don't move if we are dead
|
||||
if self.dead:
|
||||
return False
|
||||
for entity in self.map.entities:
|
||||
if entity.y == y and entity.x == x:
|
||||
if entity.is_fighting_entity():
|
||||
self.hit(entity)
|
||||
if entity.dead:
|
||||
self.add_xp(randint(3, 7))
|
||||
return True
|
||||
elif entity.is_item():
|
||||
entity.hold(self)
|
||||
return super().check_move(y, x, move_if_possible)
|
||||
|
||||
def recalculate_paths(self, max_distance: int = 8) -> None:
|
||||
"""
|
||||
Use Dijkstra algorithm to calculate best paths
|
||||
for monsters to go to the player.
|
||||
"""
|
||||
queue = [(self.y, self.x)]
|
||||
visited = []
|
||||
distances = {(self.y, self.x): 0}
|
||||
predecessors = {}
|
||||
while queue:
|
||||
y, x = queue.pop(0)
|
||||
visited.append((y, x))
|
||||
if distances[(y, x)] >= max_distance:
|
||||
continue
|
||||
for diff_y, diff_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
|
||||
new_y, new_x = y + diff_y, x + diff_x
|
||||
if not 0 <= new_y < self.map.height or \
|
||||
not 0 <= new_x < self.map.width or \
|
||||
not self.map.tiles[y][x].can_walk() or \
|
||||
(new_y, new_x) in visited or \
|
||||
(new_y, new_x) in queue:
|
||||
continue
|
||||
predecessors[(new_y, new_x)] = (y, x)
|
||||
distances[(new_y, new_x)] = distances[(y, x)] + 1
|
||||
queue.append((new_y, new_x))
|
||||
self.paths = predecessors
|
||||
|
||||
def save_state(self) -> dict:
|
||||
"""
|
||||
Saves the state of the entity into a dictionary
|
||||
"""
|
||||
d = super().save_state()
|
||||
d["current_xp"] = self.current_xp
|
||||
d["max_xp"] = self.max_xp
|
||||
return d
|
60
squirrelbattle/enums.py
Normal file
60
squirrelbattle/enums.py
Normal file
@ -0,0 +1,60 @@
|
||||
from enum import Enum, auto
|
||||
from typing import Optional
|
||||
|
||||
from squirrelbattle.settings import Settings
|
||||
|
||||
# This file contains a few useful enumeration classes used elsewhere in the code
|
||||
|
||||
|
||||
class DisplayActions(Enum):
|
||||
"""
|
||||
Display actions options for the callable displayaction Game uses
|
||||
It just calls the same action on the display object displayaction refers to.
|
||||
"""
|
||||
REFRESH = auto()
|
||||
UPDATE = auto()
|
||||
|
||||
|
||||
class GameMode(Enum):
|
||||
"""
|
||||
Game mode options
|
||||
"""
|
||||
MAINMENU = auto()
|
||||
PLAY = auto()
|
||||
SETTINGS = auto()
|
||||
INVENTORY = auto()
|
||||
|
||||
|
||||
class KeyValues(Enum):
|
||||
"""
|
||||
Key values options used in the game
|
||||
"""
|
||||
UP = auto()
|
||||
DOWN = auto()
|
||||
LEFT = auto()
|
||||
RIGHT = auto()
|
||||
ENTER = auto()
|
||||
SPACE = auto()
|
||||
|
||||
@staticmethod
|
||||
def translate_key(key: str, settings: Settings) -> Optional["KeyValues"]:
|
||||
"""
|
||||
Translate the raw string key into an enum value that we can use.
|
||||
"""
|
||||
if key in (settings.KEY_DOWN_SECONDARY,
|
||||
settings.KEY_DOWN_PRIMARY):
|
||||
return KeyValues.DOWN
|
||||
elif key in (settings.KEY_LEFT_PRIMARY,
|
||||
settings.KEY_LEFT_SECONDARY):
|
||||
return KeyValues.LEFT
|
||||
elif key in (settings.KEY_RIGHT_PRIMARY,
|
||||
settings.KEY_RIGHT_SECONDARY):
|
||||
return KeyValues.RIGHT
|
||||
elif key in (settings.KEY_UP_PRIMARY,
|
||||
settings.KEY_UP_SECONDARY):
|
||||
return KeyValues.UP
|
||||
elif key == settings.KEY_ENTER:
|
||||
return KeyValues.ENTER
|
||||
elif key == ' ':
|
||||
return KeyValues.SPACE
|
||||
return None
|
147
squirrelbattle/game.py
Normal file
147
squirrelbattle/game.py
Normal file
@ -0,0 +1,147 @@
|
||||
from random import randint
|
||||
from typing import Any, Optional
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .entities.player import Player
|
||||
from .enums import GameMode, KeyValues, DisplayActions
|
||||
from .interfaces import Map
|
||||
from .settings import Settings
|
||||
from . import menus
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class Game:
|
||||
"""
|
||||
The game object controls all actions in the game.
|
||||
"""
|
||||
map: Map
|
||||
player: Player
|
||||
# display_actions is a display interface set by the bootstrapper
|
||||
display_actions: Callable[[DisplayActions], None]
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Init the game.
|
||||
"""
|
||||
self.state = GameMode.MAINMENU
|
||||
self.main_menu = menus.MainMenu()
|
||||
self.settings_menu = menus.SettingsMenu()
|
||||
self.settings = Settings()
|
||||
self.settings.load_settings()
|
||||
self.settings.write_settings()
|
||||
self.settings_menu.update_values(self.settings)
|
||||
|
||||
def new_game(self) -> None:
|
||||
"""
|
||||
Create a new game on the screen.
|
||||
"""
|
||||
# TODO generate a new map procedurally
|
||||
self.map = Map.load("squirrelbattle/assets/example_map_2.txt")
|
||||
self.player = Player()
|
||||
self.map.add_entity(self.player)
|
||||
self.player.move(self.map.start_y, self.map.start_x)
|
||||
self.map.spawn_random_entities(randint(3, 10))
|
||||
|
||||
def run(self, screen: Any) -> None:
|
||||
"""
|
||||
Main infinite loop.
|
||||
We wait for the player's action, then we do what that should be done
|
||||
when the given key gets pressed.
|
||||
"""
|
||||
while True: # pragma no cover
|
||||
screen.clear()
|
||||
screen.refresh()
|
||||
self.display_actions(DisplayActions.REFRESH)
|
||||
key = screen.getkey()
|
||||
self.handle_key_pressed(
|
||||
KeyValues.translate_key(key, self.settings), key)
|
||||
|
||||
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str = '')\
|
||||
-> None:
|
||||
"""
|
||||
Indicates what should be done when the given key is pressed,
|
||||
according to the current game state.
|
||||
"""
|
||||
if self.state == GameMode.PLAY:
|
||||
self.handle_key_pressed_play(key)
|
||||
elif self.state == GameMode.MAINMENU:
|
||||
self.handle_key_pressed_main_menu(key)
|
||||
elif self.state == GameMode.SETTINGS:
|
||||
self.settings_menu.handle_key_pressed(key, raw_key, self)
|
||||
self.display_actions(DisplayActions.REFRESH)
|
||||
|
||||
def handle_key_pressed_play(self, key: KeyValues) -> None:
|
||||
"""
|
||||
In play mode, arrows or zqsd move the main character.
|
||||
"""
|
||||
if key == KeyValues.UP:
|
||||
if self.player.move_up():
|
||||
self.map.tick()
|
||||
elif key == KeyValues.DOWN:
|
||||
if self.player.move_down():
|
||||
self.map.tick()
|
||||
elif key == KeyValues.LEFT:
|
||||
if self.player.move_left():
|
||||
self.map.tick()
|
||||
elif key == KeyValues.RIGHT:
|
||||
if self.player.move_right():
|
||||
self.map.tick()
|
||||
elif key == KeyValues.SPACE:
|
||||
self.state = GameMode.MAINMENU
|
||||
|
||||
def handle_key_pressed_main_menu(self, key: KeyValues) -> None:
|
||||
"""
|
||||
In the main menu, we can navigate through options.
|
||||
"""
|
||||
if key == KeyValues.DOWN:
|
||||
self.main_menu.go_down()
|
||||
if key == KeyValues.UP:
|
||||
self.main_menu.go_up()
|
||||
if key == KeyValues.ENTER:
|
||||
option = self.main_menu.validate()
|
||||
if option == menus.MainMenuValues.START:
|
||||
self.new_game()
|
||||
self.display_actions(DisplayActions.UPDATE)
|
||||
self.state = GameMode.PLAY
|
||||
if option == menus.MainMenuValues.RESUME:
|
||||
self.state = GameMode.PLAY
|
||||
elif option == menus.MainMenuValues.SAVE:
|
||||
self.save_game()
|
||||
elif option == menus.MainMenuValues.LOAD:
|
||||
self.load_game()
|
||||
elif option == menus.MainMenuValues.SETTINGS:
|
||||
self.state = GameMode.SETTINGS
|
||||
elif option == menus.MainMenuValues.EXIT:
|
||||
sys.exit(0)
|
||||
|
||||
def save_state(self) -> dict:
|
||||
"""
|
||||
Saves the game to a dictionary
|
||||
"""
|
||||
return self.map.save_state()
|
||||
|
||||
def load_state(self, d: dict) -> None:
|
||||
"""
|
||||
Loads the game from a dictionary
|
||||
"""
|
||||
self.map.load_state(d)
|
||||
# noinspection PyTypeChecker
|
||||
self.player = self.map.find_entities(Player)[0]
|
||||
self.display_actions(DisplayActions.UPDATE)
|
||||
|
||||
def load_game(self) -> None:
|
||||
"""
|
||||
Loads the game from a file
|
||||
"""
|
||||
if os.path.isfile("save.json"):
|
||||
with open("save.json", "r") as f:
|
||||
self.load_state(json.loads(f.read()))
|
||||
|
||||
def save_game(self) -> None:
|
||||
"""
|
||||
Saves the game to a file
|
||||
"""
|
||||
with open("save.json", "w") as f:
|
||||
f.write(json.dumps(self.save_state()))
|
399
squirrelbattle/interfaces.py
Normal file
399
squirrelbattle/interfaces.py
Normal file
@ -0,0 +1,399 @@
|
||||
#!/usr/bin/env python
|
||||
from enum import Enum, auto
|
||||
from math import sqrt
|
||||
from random import choice, randint
|
||||
from typing import List, Optional
|
||||
|
||||
from squirrelbattle.display.texturepack import TexturePack
|
||||
|
||||
|
||||
class Map:
|
||||
"""
|
||||
Object that represents a Map with its width, height
|
||||
and tiles, that have their custom properties.
|
||||
"""
|
||||
width: int
|
||||
height: int
|
||||
start_y: int
|
||||
start_x: int
|
||||
tiles: List[List["Tile"]]
|
||||
entities: List["Entity"]
|
||||
# coordinates of the point that should be
|
||||
# on the topleft corner of the screen
|
||||
currentx: int
|
||||
currenty: int
|
||||
|
||||
def __init__(self, width: int, height: int, tiles: list,
|
||||
start_y: int, start_x: int):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.start_y = start_y
|
||||
self.start_x = start_x
|
||||
self.tiles = tiles
|
||||
self.entities = []
|
||||
|
||||
def add_entity(self, entity: "Entity") -> None:
|
||||
"""
|
||||
Register a new entity in the map.
|
||||
"""
|
||||
self.entities.append(entity)
|
||||
entity.map = self
|
||||
|
||||
def remove_entity(self, entity: "Entity") -> None:
|
||||
"""
|
||||
Unregister an entity from the map.
|
||||
"""
|
||||
self.entities.remove(entity)
|
||||
|
||||
def find_entities(self, entity_class: type) -> list:
|
||||
return [entity for entity in self.entities
|
||||
if isinstance(entity, entity_class)]
|
||||
|
||||
def is_free(self, y: int, x: int) -> bool:
|
||||
"""
|
||||
Indicates that the case at the coordinates (y, x) is empty.
|
||||
"""
|
||||
return 0 <= y < self.height and 0 <= x < self.width and \
|
||||
self.tiles[y][x].can_walk() and \
|
||||
not any(entity.x == x and entity.y == y for entity in self.entities)
|
||||
|
||||
@staticmethod
|
||||
def load(filename: str) -> "Map":
|
||||
"""
|
||||
Read a file that contains the content of a map, and build a Map object.
|
||||
"""
|
||||
with open(filename, "r") as f:
|
||||
file = f.read()
|
||||
return Map.load_from_string(file)
|
||||
|
||||
@staticmethod
|
||||
def load_from_string(content: str) -> "Map":
|
||||
"""
|
||||
Load a map represented by its characters and build a Map object.
|
||||
"""
|
||||
lines = content.split("\n")
|
||||
first_line = lines[0]
|
||||
start_y, start_x = map(int, first_line.split(" "))
|
||||
lines = [line for line in lines[1:] if line]
|
||||
height = len(lines)
|
||||
width = len(lines[0])
|
||||
tiles = [[Tile.from_ascii_char(c)
|
||||
for x, c in enumerate(line)] for y, line in enumerate(lines)]
|
||||
|
||||
return Map(width, height, tiles, start_y, start_x)
|
||||
|
||||
@staticmethod
|
||||
def load_dungeon_from_string(content: str) -> List[List["Tile"]]:
|
||||
"""
|
||||
Transforms a string into the list of corresponding tiles
|
||||
"""
|
||||
lines = content.split("\n")
|
||||
tiles = [[Tile.from_ascii_char(c)
|
||||
for x, c in enumerate(line)] for y, line in enumerate(lines)]
|
||||
return tiles
|
||||
|
||||
def draw_string(self, pack: TexturePack) -> str:
|
||||
"""
|
||||
Draw the current map as a string object that can be rendered
|
||||
in the window.
|
||||
"""
|
||||
return "\n".join("".join(tile.char(pack) for tile in line)
|
||||
for line in self.tiles)
|
||||
|
||||
def spawn_random_entities(self, count: int) -> None:
|
||||
"""
|
||||
Put randomly {count} hedgehogs on the map, where it is available.
|
||||
"""
|
||||
for _ in range(count):
|
||||
y, x = 0, 0
|
||||
while True:
|
||||
y, x = randint(0, self.height - 1), randint(0, self.width - 1)
|
||||
tile = self.tiles[y][x]
|
||||
if tile.can_walk():
|
||||
break
|
||||
entity = choice(Entity.get_all_entity_classes())()
|
||||
entity.move(y, x)
|
||||
self.add_entity(entity)
|
||||
|
||||
def tick(self) -> None:
|
||||
"""
|
||||
Trigger all entity events.
|
||||
"""
|
||||
for entity in self.entities:
|
||||
entity.act(self)
|
||||
|
||||
def save_state(self) -> dict:
|
||||
"""
|
||||
Saves the map's attributes to a dictionary
|
||||
"""
|
||||
d = dict()
|
||||
d["width"] = self.width
|
||||
d["height"] = self.height
|
||||
d["start_y"] = self.start_y
|
||||
d["start_x"] = self.start_x
|
||||
d["currentx"] = self.currentx
|
||||
d["currenty"] = self.currenty
|
||||
d["entities"] = []
|
||||
for enti in self.entities:
|
||||
d["entities"].append(enti.save_state())
|
||||
d["map"] = self.draw_string(TexturePack.ASCII_PACK)
|
||||
return d
|
||||
|
||||
def load_state(self, d: dict) -> None:
|
||||
"""
|
||||
Loads the map's attributes from a dictionary
|
||||
"""
|
||||
self.width = d["width"]
|
||||
self.height = d["height"]
|
||||
self.start_y = d["start_y"]
|
||||
self.start_x = d["start_x"]
|
||||
self.currentx = d["currentx"]
|
||||
self.currenty = d["currenty"]
|
||||
self.tiles = self.load_dungeon_from_string(d["map"])
|
||||
self.entities = []
|
||||
dictclasses = Entity.get_all_entity_classes_in_a_dict()
|
||||
for entisave in d["entities"]:
|
||||
self.add_entity(dictclasses[entisave["type"]](**entisave))
|
||||
|
||||
|
||||
class Tile(Enum):
|
||||
"""
|
||||
The internal representation of the tiles of the map
|
||||
"""
|
||||
EMPTY = auto()
|
||||
WALL = auto()
|
||||
FLOOR = auto()
|
||||
|
||||
@staticmethod
|
||||
def from_ascii_char(ch: str) -> "Tile":
|
||||
"""
|
||||
Maps an ascii character to its equivalent in the texture pack
|
||||
"""
|
||||
for tile in Tile:
|
||||
if tile.char(TexturePack.ASCII_PACK) == ch:
|
||||
return tile
|
||||
raise ValueError(ch)
|
||||
|
||||
def char(self, pack: TexturePack) -> str:
|
||||
"""
|
||||
Translates a Tile to the corresponding character according
|
||||
to the texture pack
|
||||
"""
|
||||
return getattr(pack, self.name)
|
||||
|
||||
def is_wall(self) -> bool:
|
||||
"""
|
||||
Is this Tile a wall?
|
||||
"""
|
||||
return self == Tile.WALL
|
||||
|
||||
def can_walk(self) -> bool:
|
||||
"""
|
||||
Check if an entity (player or not) can move in this tile.
|
||||
"""
|
||||
return not self.is_wall() and self != Tile.EMPTY
|
||||
|
||||
|
||||
class Entity:
|
||||
"""
|
||||
An Entity object represents any entity present on the map
|
||||
"""
|
||||
y: int
|
||||
x: int
|
||||
name: str
|
||||
map: Map
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
def __init__(self, y: int = 0, x: int = 0, name: Optional[str] = None,
|
||||
map: Optional[Map] = None, *ignored, **ignored2):
|
||||
self.y = y
|
||||
self.x = x
|
||||
self.name = name
|
||||
self.map = map
|
||||
|
||||
def check_move(self, y: int, x: int, move_if_possible: bool = False)\
|
||||
-> bool:
|
||||
"""
|
||||
Checks if moving to (y,x) is authorized
|
||||
"""
|
||||
free = self.map.is_free(y, x)
|
||||
if free and move_if_possible:
|
||||
self.move(y, x)
|
||||
return free
|
||||
|
||||
def move(self, y: int, x: int) -> bool:
|
||||
"""
|
||||
Moves an entity to (y,x) coordinates
|
||||
"""
|
||||
self.y = y
|
||||
self.x = x
|
||||
return True
|
||||
|
||||
def move_up(self, force: bool = False) -> bool:
|
||||
"""
|
||||
Moves the entity up one tile, if possible
|
||||
"""
|
||||
return self.move(self.y - 1, self.x) if force else \
|
||||
self.check_move(self.y - 1, self.x, True)
|
||||
|
||||
def move_down(self, force: bool = False) -> bool:
|
||||
"""
|
||||
Moves the entity down one tile, if possible
|
||||
"""
|
||||
return self.move(self.y + 1, self.x) if force else \
|
||||
self.check_move(self.y + 1, self.x, True)
|
||||
|
||||
def move_left(self, force: bool = False) -> bool:
|
||||
"""
|
||||
Moves the entity left one tile, if possible
|
||||
"""
|
||||
return self.move(self.y, self.x - 1) if force else \
|
||||
self.check_move(self.y, self.x - 1, True)
|
||||
|
||||
def move_right(self, force: bool = False) -> bool:
|
||||
"""
|
||||
Moves the entity right one tile, if possible
|
||||
"""
|
||||
return self.move(self.y, self.x + 1) if force else \
|
||||
self.check_move(self.y, self.x + 1, True)
|
||||
|
||||
def act(self, m: Map) -> None:
|
||||
"""
|
||||
Define the action of the entity that is ran each tick.
|
||||
By default, does nothing.
|
||||
"""
|
||||
pass
|
||||
|
||||
def distance_squared(self, other: "Entity") -> int:
|
||||
"""
|
||||
Get the square of the distance to another entity.
|
||||
Useful to check distances since square root takes time.
|
||||
"""
|
||||
return (self.y - other.y) ** 2 + (self.x - other.x) ** 2
|
||||
|
||||
def distance(self, other: "Entity") -> float:
|
||||
"""
|
||||
Get the cartesian distance to another entity.
|
||||
"""
|
||||
return sqrt(self.distance_squared(other))
|
||||
|
||||
def is_fighting_entity(self) -> bool:
|
||||
"""
|
||||
Is this entity a fighting entity?
|
||||
"""
|
||||
return isinstance(self, FightingEntity)
|
||||
|
||||
def is_item(self) -> bool:
|
||||
"""
|
||||
Is this entity an item?
|
||||
"""
|
||||
from squirrelbattle.entities.items import Item
|
||||
return isinstance(self, Item)
|
||||
|
||||
@staticmethod
|
||||
def get_all_entity_classes():
|
||||
"""
|
||||
Returns all entities subclasses
|
||||
"""
|
||||
from squirrelbattle.entities.items import Heart, Bomb
|
||||
from squirrelbattle.entities.monsters import Beaver, Hedgehog, \
|
||||
Rabbit, TeddyBear
|
||||
return [Beaver, Bomb, Heart, Hedgehog, Rabbit, TeddyBear]
|
||||
|
||||
@staticmethod
|
||||
def get_all_entity_classes_in_a_dict() -> dict:
|
||||
"""
|
||||
Returns all entities subclasses in a dictionary
|
||||
"""
|
||||
from squirrelbattle.entities.player import Player
|
||||
from squirrelbattle.entities.monsters import Beaver, Hedgehog, Rabbit, \
|
||||
TeddyBear
|
||||
from squirrelbattle.entities.items import Bomb, Heart
|
||||
return {
|
||||
"Beaver": Beaver,
|
||||
"Bomb": Bomb,
|
||||
"Heart": Heart,
|
||||
"Hedgehog": Hedgehog,
|
||||
"Rabbit": Rabbit,
|
||||
"TeddyBear": TeddyBear,
|
||||
"Player": Player,
|
||||
}
|
||||
|
||||
def save_state(self) -> dict:
|
||||
"""
|
||||
Saves the coordinates of the entity
|
||||
"""
|
||||
d = dict()
|
||||
d["x"] = self.x
|
||||
d["y"] = self.y
|
||||
d["type"] = self.__class__.__name__
|
||||
return d
|
||||
|
||||
|
||||
class FightingEntity(Entity):
|
||||
"""
|
||||
A FightingEntity is an entity that can fight, and thus has a health,
|
||||
level and stats
|
||||
"""
|
||||
maxhealth: int
|
||||
health: int
|
||||
strength: int
|
||||
intelligence: int
|
||||
charisma: int
|
||||
dexterity: int
|
||||
constitution: int
|
||||
level: int
|
||||
|
||||
def __init__(self, maxhealth: int = 0, health: Optional[int] = None,
|
||||
strength: int = 0, intelligence: int = 0, charisma: int = 0,
|
||||
dexterity: int = 0, constitution: int = 0, level: int = 0,
|
||||
*args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.maxhealth = maxhealth
|
||||
self.health = maxhealth if health is None else health
|
||||
self.strength = strength
|
||||
self.intelligence = intelligence
|
||||
self.charisma = charisma
|
||||
self.dexterity = dexterity
|
||||
self.constitution = constitution
|
||||
self.level = level
|
||||
|
||||
@property
|
||||
def dead(self) -> bool:
|
||||
return self.health <= 0
|
||||
|
||||
def hit(self, opponent: "FightingEntity") -> None:
|
||||
"""
|
||||
Deals damage to the opponent, based on the stats
|
||||
"""
|
||||
opponent.take_damage(self, self.strength)
|
||||
|
||||
def take_damage(self, attacker: "Entity", amount: int) -> None:
|
||||
"""
|
||||
Take damage from the attacker, based on the stats
|
||||
"""
|
||||
self.health -= amount
|
||||
if self.health <= 0:
|
||||
self.die()
|
||||
|
||||
def die(self) -> None:
|
||||
"""
|
||||
If a fighting entity has no more health, it dies and is removed
|
||||
"""
|
||||
self.map.remove_entity(self)
|
||||
|
||||
def keys(self) -> list:
|
||||
"""
|
||||
Returns a fighting entities specific attributes
|
||||
"""
|
||||
return ["maxhealth", "health", "level", "strength",
|
||||
"intelligence", "charisma", "dexterity", "constitution"]
|
||||
|
||||
def save_state(self) -> dict:
|
||||
"""
|
||||
Saves the state of the entity into a dictionary
|
||||
"""
|
||||
d = super().save_state()
|
||||
for name in self.keys():
|
||||
d[name] = getattr(self, name)
|
||||
return d
|
107
squirrelbattle/menus.py
Normal file
107
squirrelbattle/menus.py
Normal file
@ -0,0 +1,107 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Optional
|
||||
|
||||
from .display.texturepack import TexturePack
|
||||
from .enums import GameMode, KeyValues, DisplayActions
|
||||
from .settings import Settings
|
||||
|
||||
|
||||
class Menu:
|
||||
"""
|
||||
A Menu object is the logical representation of a menu in the game
|
||||
"""
|
||||
values: list
|
||||
|
||||
def __init__(self):
|
||||
self.position = 0
|
||||
|
||||
def go_up(self) -> None:
|
||||
"""
|
||||
Moves the pointer of the menu on the previous value
|
||||
"""
|
||||
self.position = max(0, self.position - 1)
|
||||
|
||||
def go_down(self) -> None:
|
||||
"""
|
||||
Moves the pointer of the menu on the next value
|
||||
"""
|
||||
self.position = min(len(self.values) - 1, self.position + 1)
|
||||
|
||||
def validate(self) -> Any:
|
||||
"""
|
||||
Selects the value that is pointed by the menu pointer
|
||||
"""
|
||||
return self.values[self.position]
|
||||
|
||||
|
||||
class MainMenuValues(Enum):
|
||||
"""
|
||||
Values of the main menu
|
||||
"""
|
||||
START = 'Nouvelle partie'
|
||||
RESUME = 'Continuer'
|
||||
SAVE = 'Sauvegarder'
|
||||
LOAD = 'Charger'
|
||||
SETTINGS = 'Paramètres'
|
||||
EXIT = 'Quitter'
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class MainMenu(Menu):
|
||||
"""
|
||||
A special instance of a menu : the main menu
|
||||
"""
|
||||
values = [e for e in MainMenuValues]
|
||||
|
||||
|
||||
class SettingsMenu(Menu):
|
||||
"""
|
||||
A special instance of a menu : the settings menu
|
||||
"""
|
||||
waiting_for_key: bool = False
|
||||
|
||||
def update_values(self, settings: Settings) -> None:
|
||||
self.values = list(settings.__dict__.items())
|
||||
self.values.append(("RETURN", ["", "Retour"]))
|
||||
|
||||
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str,
|
||||
game: Any) -> None:
|
||||
"""
|
||||
In the setting menu, we van select a setting and change it
|
||||
"""
|
||||
if not self.waiting_for_key:
|
||||
# Navigate normally through the menu.
|
||||
if key == KeyValues.SPACE or \
|
||||
key == KeyValues.ENTER and \
|
||||
self.position == len(self.values) - 1:
|
||||
# Go back
|
||||
game.display_actions(DisplayActions.UPDATE)
|
||||
game.state = GameMode.MAINMENU
|
||||
if key == KeyValues.DOWN:
|
||||
self.go_down()
|
||||
if key == KeyValues.UP:
|
||||
self.go_up()
|
||||
if key == KeyValues.ENTER and self.position < len(self.values) - 1:
|
||||
# Change a setting
|
||||
option = self.values[self.position][0]
|
||||
if option == "TEXTURE_PACK":
|
||||
game.settings.TEXTURE_PACK = \
|
||||
TexturePack.get_next_pack_name(
|
||||
game.settings.TEXTURE_PACK)
|
||||
game.settings.write_settings()
|
||||
self.update_values(game.settings)
|
||||
else:
|
||||
self.waiting_for_key = True
|
||||
self.update_values(game.settings)
|
||||
else:
|
||||
option = self.values[self.position][0]
|
||||
# Don't use an already mapped key
|
||||
if any(getattr(game.settings, opt) == raw_key
|
||||
for opt in game.settings.settings_keys if opt != option):
|
||||
return
|
||||
setattr(game.settings, option, raw_key)
|
||||
game.settings.write_settings()
|
||||
self.waiting_for_key = False
|
||||
self.update_values(game.settings)
|
93
squirrelbattle/settings.py
Normal file
93
squirrelbattle/settings.py
Normal file
@ -0,0 +1,93 @@
|
||||
import json
|
||||
import os
|
||||
from typing import Any, Generator
|
||||
|
||||
|
||||
class Settings:
|
||||
"""
|
||||
This class stores the settings of the game.
|
||||
Settings can be get by using for example settings.TEXTURE_PACK directly.
|
||||
The comment can be get by using settings.get_comment('TEXTURE_PACK').
|
||||
We can define the setting by simply use settings.TEXTURE_PACK = 'new_key'
|
||||
"""
|
||||
def __init__(self):
|
||||
self.KEY_UP_PRIMARY = \
|
||||
['z', 'Touche principale pour aller vers le haut']
|
||||
self.KEY_UP_SECONDARY = \
|
||||
['KEY_UP', 'Touche secondaire pour aller vers le haut']
|
||||
self.KEY_DOWN_PRIMARY = \
|
||||
['s', 'Touche principale pour aller vers le bas']
|
||||
self.KEY_DOWN_SECONDARY = \
|
||||
['KEY_DOWN', 'Touche secondaire pour aller vers le bas']
|
||||
self.KEY_LEFT_PRIMARY = \
|
||||
['q', 'Touche principale pour aller vers la gauche']
|
||||
self.KEY_LEFT_SECONDARY = \
|
||||
['KEY_LEFT', 'Touche secondaire pour aller vers la gauche']
|
||||
self.KEY_RIGHT_PRIMARY = \
|
||||
['d', 'Touche principale pour aller vers la droite']
|
||||
self.KEY_RIGHT_SECONDARY = \
|
||||
['KEY_RIGHT', 'Touche secondaire pour aller vers la droite']
|
||||
self.KEY_ENTER = \
|
||||
['\n', 'Touche pour valider un menu']
|
||||
self.TEXTURE_PACK = ['ascii', 'Pack de textures utilisé']
|
||||
|
||||
def __getattribute__(self, item: str) -> Any:
|
||||
superattribute = super().__getattribute__(item)
|
||||
if item.isupper() and item in self.settings_keys:
|
||||
return superattribute[0]
|
||||
return superattribute
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
if name in self.settings_keys:
|
||||
object.__getattribute__(self, name)[0] = value
|
||||
return
|
||||
return super().__setattr__(name, value)
|
||||
|
||||
def get_comment(self, item: str) -> str:
|
||||
"""
|
||||
Retrieve the comment of a setting.
|
||||
"""
|
||||
if item in self.settings_keys:
|
||||
return object.__getattribute__(self, item)[1]
|
||||
for key in self.settings_keys:
|
||||
if getattr(self, key) == item:
|
||||
return object.__getattribute__(self, key)[1]
|
||||
|
||||
@property
|
||||
def settings_keys(self) -> Generator[str, Any, None]:
|
||||
"""
|
||||
Get the list of all parameters.
|
||||
"""
|
||||
return (key for key in self.__dict__)
|
||||
|
||||
def loads_from_string(self, json_str: str) -> None:
|
||||
"""
|
||||
Dump settings
|
||||
"""
|
||||
d = json.loads(json_str)
|
||||
for key in d:
|
||||
setattr(self, key, d[key])
|
||||
|
||||
def dumps_to_string(self) -> str:
|
||||
"""
|
||||
Dump settings
|
||||
"""
|
||||
d = dict()
|
||||
for key in self.settings_keys:
|
||||
d[key] = getattr(self, key)
|
||||
return json.dumps(d, indent=4)
|
||||
|
||||
def load_settings(self) -> None:
|
||||
"""
|
||||
Loads the settings from a file
|
||||
"""
|
||||
if os.path.isfile("settings.json"):
|
||||
with open("settings.json", "r") as f:
|
||||
self.loads_from_string(f.read())
|
||||
|
||||
def write_settings(self) -> None:
|
||||
"""
|
||||
Dumps the settings into a file
|
||||
"""
|
||||
with open("settings.json", "w") as f:
|
||||
f.write(self.dumps_to_string())
|
33
squirrelbattle/term_manager.py
Normal file
33
squirrelbattle/term_manager.py
Normal file
@ -0,0 +1,33 @@
|
||||
import curses
|
||||
from types import TracebackType
|
||||
|
||||
|
||||
class TermManager: # pragma: no cover
|
||||
"""
|
||||
The TermManager object initializes the terminal, returns a screen object and
|
||||
de-initializes the terminal after use
|
||||
"""
|
||||
def __init__(self):
|
||||
self.screen = curses.initscr()
|
||||
# convert escapes sequences to curses abstraction
|
||||
self.screen.keypad(True)
|
||||
# stop printing typed keys to the terminal
|
||||
curses.noecho()
|
||||
# send keys through without having to press <enter>
|
||||
curses.cbreak()
|
||||
# make cursor invisible
|
||||
curses.curs_set(False)
|
||||
# Enable colors
|
||||
curses.start_color()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type: type, exc_value: Exception,
|
||||
exc_traceback: TracebackType) -> None:
|
||||
# restore the terminal to its original state
|
||||
self.screen.keypad(False)
|
||||
curses.echo()
|
||||
curses.nocbreak()
|
||||
curses.curs_set(True)
|
||||
curses.endwin()
|
0
squirrelbattle/tests/__init__.py
Normal file
0
squirrelbattle/tests/__init__.py
Normal file
174
squirrelbattle/tests/entities_test.py
Normal file
174
squirrelbattle/tests/entities_test.py
Normal file
@ -0,0 +1,174 @@
|
||||
import unittest
|
||||
|
||||
from squirrelbattle.entities.items import Bomb, Heart, Item
|
||||
from squirrelbattle.entities.monsters import Beaver, Hedgehog, Rabbit, TeddyBear
|
||||
from squirrelbattle.entities.player import Player
|
||||
from squirrelbattle.interfaces import Entity, Map
|
||||
|
||||
|
||||
class TestEntities(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
"""
|
||||
Load example map that can be used in tests.
|
||||
"""
|
||||
self.map = Map.load("squirrelbattle/assets/example_map.txt")
|
||||
self.player = Player()
|
||||
self.map.add_entity(self.player)
|
||||
self.player.move(self.map.start_y, self.map.start_x)
|
||||
|
||||
def test_basic_entities(self) -> None:
|
||||
"""
|
||||
Test some random stuff with basic entities.
|
||||
"""
|
||||
entity = Entity()
|
||||
entity.move(42, 64)
|
||||
self.assertEqual(entity.y, 42)
|
||||
self.assertEqual(entity.x, 64)
|
||||
self.assertIsNone(entity.act(self.map))
|
||||
|
||||
other_entity = Entity()
|
||||
other_entity.move(45, 68)
|
||||
self.assertEqual(entity.distance_squared(other_entity), 25)
|
||||
self.assertEqual(entity.distance(other_entity), 5)
|
||||
|
||||
def test_fighting_entities(self) -> None:
|
||||
"""
|
||||
Test some random stuff with fighting entities.
|
||||
"""
|
||||
entity = Beaver()
|
||||
self.map.add_entity(entity)
|
||||
self.assertEqual(entity.maxhealth, 20)
|
||||
self.assertEqual(entity.maxhealth, entity.health)
|
||||
self.assertEqual(entity.strength, 2)
|
||||
for _ in range(9):
|
||||
self.assertIsNone(entity.hit(entity))
|
||||
self.assertFalse(entity.dead)
|
||||
self.assertIsNone(entity.hit(entity))
|
||||
self.assertTrue(entity.dead)
|
||||
|
||||
entity = Rabbit()
|
||||
self.map.add_entity(entity)
|
||||
entity.move(15, 44)
|
||||
# Move randomly
|
||||
self.map.tick()
|
||||
self.assertFalse(entity.y == 15 and entity.x == 44)
|
||||
|
||||
# Move to the player
|
||||
entity.move(3, 6)
|
||||
self.map.tick()
|
||||
self.assertTrue(entity.y == 2 and entity.x == 6)
|
||||
|
||||
# Rabbit should fight
|
||||
old_health = self.player.health
|
||||
self.map.tick()
|
||||
self.assertTrue(entity.y == 2 and entity.x == 6)
|
||||
self.assertEqual(old_health - entity.strength, self.player.health)
|
||||
|
||||
# Fight the rabbit
|
||||
old_health = entity.health
|
||||
self.player.move_down()
|
||||
self.assertEqual(entity.health, old_health - self.player.strength)
|
||||
self.assertFalse(entity.dead)
|
||||
old_health = entity.health
|
||||
self.player.move_down()
|
||||
self.assertEqual(entity.health, old_health - self.player.strength)
|
||||
self.assertFalse(entity.dead)
|
||||
old_health = entity.health
|
||||
self.player.move_down()
|
||||
self.assertEqual(entity.health, old_health - self.player.strength)
|
||||
self.assertTrue(entity.dead)
|
||||
self.assertGreaterEqual(self.player.current_xp, 3)
|
||||
|
||||
def test_items(self) -> None:
|
||||
"""
|
||||
Test some random stuff with items.
|
||||
"""
|
||||
item = Item()
|
||||
self.map.add_entity(item)
|
||||
self.assertFalse(item.held)
|
||||
item.hold(self.player)
|
||||
self.assertTrue(item.held)
|
||||
item.drop(2, 6)
|
||||
self.assertEqual(item.y, 2)
|
||||
self.assertEqual(item.x, 6)
|
||||
|
||||
# Pick up item
|
||||
self.player.move_down()
|
||||
self.assertTrue(item.held)
|
||||
self.assertEqual(item.held_by, self.player)
|
||||
self.assertIn(item, self.player.inventory)
|
||||
self.assertNotIn(item, self.map.entities)
|
||||
|
||||
def test_bombs(self) -> None:
|
||||
"""
|
||||
Test some random stuff with bombs.
|
||||
"""
|
||||
item = Bomb()
|
||||
hedgehog = Hedgehog()
|
||||
teddy_bear = TeddyBear()
|
||||
self.map.add_entity(item)
|
||||
self.map.add_entity(hedgehog)
|
||||
self.map.add_entity(teddy_bear)
|
||||
hedgehog.health = 2
|
||||
teddy_bear.health = 2
|
||||
hedgehog.move(41, 42)
|
||||
teddy_bear.move(42, 41)
|
||||
item.act(self.map)
|
||||
self.assertFalse(hedgehog.dead)
|
||||
self.assertFalse(teddy_bear.dead)
|
||||
item.drop(42, 42)
|
||||
self.assertEqual(item.y, 42)
|
||||
self.assertEqual(item.x, 42)
|
||||
item.act(self.map)
|
||||
self.assertTrue(hedgehog.dead)
|
||||
self.assertTrue(teddy_bear.dead)
|
||||
bomb_state = item.save_state()
|
||||
self.assertEqual(bomb_state["damage"], item.damage)
|
||||
|
||||
def test_hearts(self) -> None:
|
||||
"""
|
||||
Test some random stuff with hearts.
|
||||
"""
|
||||
item = Heart()
|
||||
self.map.add_entity(item)
|
||||
item.move(2, 6)
|
||||
self.player.health -= 2 * item.healing
|
||||
self.player.move_down()
|
||||
self.assertNotIn(item, self.map.entities)
|
||||
self.assertEqual(self.player.health,
|
||||
self.player.maxhealth - item.healing)
|
||||
heart_state = item.save_state()
|
||||
self.assertEqual(heart_state["healing"], item.healing)
|
||||
|
||||
def test_players(self) -> None:
|
||||
"""
|
||||
Test some random stuff with players.
|
||||
"""
|
||||
player = Player()
|
||||
self.map.add_entity(player)
|
||||
player.move(1, 6)
|
||||
self.assertEqual(player.strength, 5)
|
||||
self.assertEqual(player.health, player.maxhealth)
|
||||
self.assertEqual(player.maxhealth, 20)
|
||||
|
||||
# Test movements and ensure that collisions are working
|
||||
self.assertFalse(player.move_up())
|
||||
self.assertTrue(player.move_left())
|
||||
self.assertFalse(player.move_left())
|
||||
for i in range(8):
|
||||
self.assertTrue(player.move_down())
|
||||
self.assertFalse(player.move_down())
|
||||
self.assertTrue(player.move_right())
|
||||
self.assertTrue(player.move_right())
|
||||
self.assertTrue(player.move_right())
|
||||
self.assertFalse(player.move_right())
|
||||
self.assertTrue(player.move_down())
|
||||
self.assertTrue(player.move_down())
|
||||
|
||||
player.add_xp(70)
|
||||
self.assertEqual(player.current_xp, 10)
|
||||
self.assertEqual(player.max_xp, 40)
|
||||
self.assertEqual(player.level, 4)
|
||||
|
||||
player_state = player.save_state()
|
||||
self.assertEqual(player_state["current_xp"], 10)
|
279
squirrelbattle/tests/game_test.py
Normal file
279
squirrelbattle/tests/game_test.py
Normal file
@ -0,0 +1,279 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from squirrelbattle.bootstrap import Bootstrap
|
||||
from squirrelbattle.display.display import Display
|
||||
from squirrelbattle.display.display_manager import DisplayManager
|
||||
from squirrelbattle.entities.player import Player
|
||||
from squirrelbattle.game import Game, KeyValues, GameMode
|
||||
from squirrelbattle.menus import MainMenuValues
|
||||
from squirrelbattle.settings import Settings
|
||||
|
||||
|
||||
class TestGame(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
"""
|
||||
Setup game.
|
||||
"""
|
||||
self.game = Game()
|
||||
self.game.new_game()
|
||||
display = DisplayManager(None, self.game)
|
||||
self.game.display_actions = display.handle_display_action
|
||||
|
||||
def test_load_game(self) -> None:
|
||||
"""
|
||||
Save a game and reload it.
|
||||
"""
|
||||
old_state = self.game.save_state()
|
||||
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.assertEqual(self.game.main_menu.validate(), MainMenuValues.SAVE)
|
||||
self.game.handle_key_pressed(KeyValues.ENTER) # Save game
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.assertEqual(self.game.main_menu.validate(), MainMenuValues.LOAD)
|
||||
self.game.handle_key_pressed(KeyValues.ENTER) # Load game
|
||||
|
||||
new_state = self.game.save_state()
|
||||
self.assertEqual(old_state, new_state)
|
||||
|
||||
def test_bootstrap_fail(self) -> None:
|
||||
"""
|
||||
Ensure that the test can't play the game,
|
||||
because there is no associated shell.
|
||||
Yeah, that's only for coverage.
|
||||
"""
|
||||
self.assertRaises(Exception, Bootstrap.run_game)
|
||||
self.assertEqual(os.getenv("TERM", "unknown"), "unknown")
|
||||
|
||||
def test_key_translation(self) -> None:
|
||||
"""
|
||||
Test key bindings.
|
||||
"""
|
||||
self.game.settings = Settings()
|
||||
|
||||
self.assertEqual(KeyValues.translate_key(
|
||||
self.game.settings.KEY_UP_PRIMARY, self.game.settings),
|
||||
KeyValues.UP)
|
||||
self.assertEqual(KeyValues.translate_key(
|
||||
self.game.settings.KEY_UP_SECONDARY, self.game.settings),
|
||||
KeyValues.UP)
|
||||
self.assertEqual(KeyValues.translate_key(
|
||||
self.game.settings.KEY_DOWN_PRIMARY, self.game.settings),
|
||||
KeyValues.DOWN)
|
||||
self.assertEqual(KeyValues.translate_key(
|
||||
self.game.settings.KEY_DOWN_SECONDARY, self.game.settings),
|
||||
KeyValues.DOWN)
|
||||
self.assertEqual(KeyValues.translate_key(
|
||||
self.game.settings.KEY_LEFT_PRIMARY, self.game.settings),
|
||||
KeyValues.LEFT)
|
||||
self.assertEqual(KeyValues.translate_key(
|
||||
self.game.settings.KEY_LEFT_SECONDARY, self.game.settings),
|
||||
KeyValues.LEFT)
|
||||
self.assertEqual(KeyValues.translate_key(
|
||||
self.game.settings.KEY_RIGHT_PRIMARY, self.game.settings),
|
||||
KeyValues.RIGHT)
|
||||
self.assertEqual(KeyValues.translate_key(
|
||||
self.game.settings.KEY_RIGHT_SECONDARY, self.game.settings),
|
||||
KeyValues.RIGHT)
|
||||
self.assertEqual(KeyValues.translate_key(
|
||||
self.game.settings.KEY_ENTER, self.game.settings),
|
||||
KeyValues.ENTER)
|
||||
self.assertEqual(KeyValues.translate_key(' ', self.game.settings),
|
||||
KeyValues.SPACE)
|
||||
self.assertEqual(KeyValues.translate_key('plop', self.game.settings),
|
||||
None)
|
||||
|
||||
def test_key_press(self) -> None:
|
||||
"""
|
||||
Press a key and see what is done.
|
||||
"""
|
||||
self.assertEqual(self.game.state, GameMode.MAINMENU)
|
||||
self.assertEqual(self.game.main_menu.validate(),
|
||||
MainMenuValues.START)
|
||||
self.game.handle_key_pressed(KeyValues.UP)
|
||||
self.assertEqual(self.game.main_menu.validate(),
|
||||
MainMenuValues.START)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.assertEqual(self.game.main_menu.validate(),
|
||||
MainMenuValues.RESUME)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.assertEqual(self.game.main_menu.validate(),
|
||||
MainMenuValues.SAVE)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.assertEqual(self.game.main_menu.validate(),
|
||||
MainMenuValues.LOAD)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.assertEqual(self.game.main_menu.validate(),
|
||||
MainMenuValues.SETTINGS)
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertEqual(self.game.state, GameMode.SETTINGS)
|
||||
|
||||
self.game.handle_key_pressed(KeyValues.SPACE)
|
||||
self.assertEqual(self.game.state, GameMode.MAINMENU)
|
||||
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.assertEqual(self.game.main_menu.validate(),
|
||||
MainMenuValues.EXIT)
|
||||
self.assertRaises(SystemExit, self.game.handle_key_pressed,
|
||||
KeyValues.ENTER)
|
||||
|
||||
self.game.handle_key_pressed(KeyValues.UP)
|
||||
self.assertEqual(self.game.main_menu.validate(),
|
||||
MainMenuValues.SETTINGS)
|
||||
self.game.handle_key_pressed(KeyValues.UP)
|
||||
self.assertEqual(self.game.main_menu.validate(),
|
||||
MainMenuValues.LOAD)
|
||||
self.game.handle_key_pressed(KeyValues.UP)
|
||||
self.assertEqual(self.game.main_menu.validate(),
|
||||
MainMenuValues.SAVE)
|
||||
self.game.handle_key_pressed(KeyValues.UP)
|
||||
self.assertEqual(self.game.main_menu.validate(),
|
||||
MainMenuValues.RESUME)
|
||||
self.game.handle_key_pressed(KeyValues.UP)
|
||||
self.assertEqual(self.game.main_menu.validate(),
|
||||
MainMenuValues.START)
|
||||
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertEqual(self.game.state, GameMode.PLAY)
|
||||
|
||||
# Kill entities
|
||||
for entity in self.game.map.entities.copy():
|
||||
if not isinstance(entity, Player):
|
||||
self.game.map.remove_entity(entity)
|
||||
|
||||
y, x = self.game.player.y, self.game.player.x
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
new_y, new_x = self.game.player.y, self.game.player.x
|
||||
self.assertEqual(new_y, y + 1)
|
||||
self.assertEqual(new_x, x)
|
||||
|
||||
y, x = new_y, new_x
|
||||
self.game.handle_key_pressed(KeyValues.RIGHT)
|
||||
new_y, new_x = self.game.player.y, self.game.player.x
|
||||
self.assertEqual(new_y, y)
|
||||
self.assertEqual(new_x, x + 1)
|
||||
|
||||
y, x = self.game.player.y, self.game.player.x
|
||||
self.game.handle_key_pressed(KeyValues.UP)
|
||||
new_y, new_x = self.game.player.y, self.game.player.x
|
||||
self.assertEqual(new_y, y - 1)
|
||||
self.assertEqual(new_x, x)
|
||||
|
||||
y, x = self.game.player.y, self.game.player.x
|
||||
self.game.handle_key_pressed(KeyValues.LEFT)
|
||||
new_y, new_x = self.game.player.y, self.game.player.x
|
||||
self.assertEqual(new_y, y)
|
||||
self.assertEqual(new_x, x - 1)
|
||||
|
||||
self.game.handle_key_pressed(KeyValues.SPACE)
|
||||
self.assertEqual(self.game.state, GameMode.MAINMENU)
|
||||
|
||||
def test_new_game(self) -> None:
|
||||
"""
|
||||
Ensure that the start button starts a new game.
|
||||
"""
|
||||
old_map = self.game.map
|
||||
old_player = self.game.player
|
||||
self.game.handle_key_pressed(KeyValues.ENTER) # Start new game
|
||||
new_map = self.game.map
|
||||
new_player = self.game.player
|
||||
# Ensure that
|
||||
self.assertNotEqual(old_map, new_map)
|
||||
self.assertNotEqual(old_player, new_player)
|
||||
|
||||
self.game.handle_key_pressed(KeyValues.SPACE)
|
||||
old_map = new_map
|
||||
old_player = new_player
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.ENTER) # Resume game
|
||||
new_map = self.game.map
|
||||
new_player = self.game.player
|
||||
self.assertEqual(old_map, new_map)
|
||||
self.assertEqual(old_player, new_player)
|
||||
|
||||
def test_settings_menu(self) -> None:
|
||||
"""
|
||||
Ensure that the settings menu is working properly.
|
||||
"""
|
||||
self.game.settings = Settings()
|
||||
|
||||
# Open settings menu
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertEqual(self.game.state, GameMode.SETTINGS)
|
||||
|
||||
# Define the "move up" key to 'w'
|
||||
self.assertFalse(self.game.settings_menu.waiting_for_key)
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertTrue(self.game.settings_menu.waiting_for_key)
|
||||
self.game.handle_key_pressed(None, 'w')
|
||||
self.assertFalse(self.game.settings_menu.waiting_for_key)
|
||||
self.assertEqual(self.game.settings.KEY_UP_PRIMARY, 'w')
|
||||
|
||||
# Navigate to "move left"
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.UP)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
|
||||
# Define the "move up" key to 'a'
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertTrue(self.game.settings_menu.waiting_for_key)
|
||||
# Can't used a mapped key
|
||||
self.game.handle_key_pressed(None, 's')
|
||||
self.assertTrue(self.game.settings_menu.waiting_for_key)
|
||||
self.game.handle_key_pressed(None, 'a')
|
||||
self.assertFalse(self.game.settings_menu.waiting_for_key)
|
||||
self.assertEqual(self.game.settings.KEY_LEFT_PRIMARY, 'a')
|
||||
|
||||
# Navigate to "texture pack"
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
|
||||
# Change texture pack
|
||||
self.assertEqual(self.game.settings.TEXTURE_PACK, "ascii")
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertEqual(self.game.settings.TEXTURE_PACK, "squirrel")
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertEqual(self.game.settings.TEXTURE_PACK, "ascii")
|
||||
|
||||
# Navigate to "back" button
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertEqual(self.game.state, GameMode.MAINMENU)
|
||||
|
||||
def test_dead_screen(self) -> None:
|
||||
"""
|
||||
Kill player and render dead screen.
|
||||
"""
|
||||
self.game.state = GameMode.PLAY
|
||||
# Kill player
|
||||
self.game.player.take_damage(self.game.player,
|
||||
self.game.player.health + 2)
|
||||
y, x = self.game.player.y, self.game.player.x
|
||||
for key in [KeyValues.UP, KeyValues.DOWN,
|
||||
KeyValues.LEFT, KeyValues.RIGHT]:
|
||||
self.game.handle_key_pressed(key)
|
||||
new_y, new_x = self.game.player.y, self.game.player.x
|
||||
self.assertEqual(new_y, y)
|
||||
self.assertEqual(new_x, x)
|
||||
|
||||
def test_not_implemented(self) -> None:
|
||||
"""
|
||||
Check that some functions are not implemented, only for coverage.
|
||||
"""
|
||||
self.assertRaises(NotImplementedError, Display.display, None)
|
35
squirrelbattle/tests/interfaces_test.py
Normal file
35
squirrelbattle/tests/interfaces_test.py
Normal file
@ -0,0 +1,35 @@
|
||||
import unittest
|
||||
|
||||
from squirrelbattle.display.texturepack import TexturePack
|
||||
from squirrelbattle.interfaces import Map, Tile
|
||||
|
||||
|
||||
class TestInterfaces(unittest.TestCase):
|
||||
def test_map(self) -> None:
|
||||
"""
|
||||
Create a map and check that it is well parsed.
|
||||
"""
|
||||
m = Map.load_from_string("0 0\n.#\n#.\n")
|
||||
self.assertEqual(m.width, 2)
|
||||
self.assertEqual(m.height, 2)
|
||||
self.assertEqual(m.draw_string(TexturePack.ASCII_PACK), ".#\n#.")
|
||||
|
||||
def test_load_map(self) -> None:
|
||||
"""
|
||||
Try to load a map from a file.
|
||||
"""
|
||||
m = Map.load("squirrelbattle/assets/example_map.txt")
|
||||
self.assertEqual(m.width, 52)
|
||||
self.assertEqual(m.height, 17)
|
||||
|
||||
def test_tiles(self) -> None:
|
||||
"""
|
||||
Test some things about tiles.
|
||||
"""
|
||||
self.assertFalse(Tile.FLOOR.is_wall())
|
||||
self.assertTrue(Tile.WALL.is_wall())
|
||||
self.assertFalse(Tile.EMPTY.is_wall())
|
||||
self.assertTrue(Tile.FLOOR.can_walk())
|
||||
self.assertFalse(Tile.WALL.can_walk())
|
||||
self.assertFalse(Tile.EMPTY.can_walk())
|
||||
self.assertRaises(ValueError, Tile.from_ascii_char, 'unknown')
|
17
squirrelbattle/tests/screen.py
Normal file
17
squirrelbattle/tests/screen.py
Normal file
@ -0,0 +1,17 @@
|
||||
class FakePad:
|
||||
"""
|
||||
In order to run tests, we simulate a fake curses pad that accepts functions
|
||||
but does nothing with them.
|
||||
"""
|
||||
def addstr(self, y: int, x: int, message: str, color: int = 0) -> None:
|
||||
pass
|
||||
|
||||
def refresh(self, pminrow: int, pmincol: int, sminrow: int,
|
||||
smincol: int, smaxrow: int, smaxcol: int) -> None:
|
||||
pass
|
||||
|
||||
def clear(self) -> None:
|
||||
pass
|
||||
|
||||
def resize(self, height: int, width: int) -> None:
|
||||
pass
|
32
squirrelbattle/tests/settings_test.py
Normal file
32
squirrelbattle/tests/settings_test.py
Normal file
@ -0,0 +1,32 @@
|
||||
import unittest
|
||||
|
||||
from squirrelbattle.settings import Settings
|
||||
|
||||
|
||||
class TestSettings(unittest.TestCase):
|
||||
def test_settings(self) -> None:
|
||||
"""
|
||||
Ensure that settings are well loaded.
|
||||
"""
|
||||
settings = Settings()
|
||||
self.assertEqual(settings.KEY_UP_PRIMARY, 'z')
|
||||
self.assertEqual(settings.KEY_DOWN_PRIMARY, 's')
|
||||
self.assertEqual(settings.KEY_LEFT_PRIMARY, 'q')
|
||||
self.assertEqual(settings.KEY_RIGHT_PRIMARY, 'd')
|
||||
self.assertEqual(settings.KEY_UP_SECONDARY, 'KEY_UP')
|
||||
self.assertEqual(settings.KEY_DOWN_SECONDARY, 'KEY_DOWN')
|
||||
self.assertEqual(settings.KEY_LEFT_SECONDARY, 'KEY_LEFT')
|
||||
self.assertEqual(settings.KEY_RIGHT_SECONDARY, 'KEY_RIGHT')
|
||||
self.assertEqual(settings.TEXTURE_PACK, 'ascii')
|
||||
self.assertEqual(settings.get_comment(settings.TEXTURE_PACK),
|
||||
settings.get_comment('TEXTURE_PACK'))
|
||||
self.assertEqual(settings.get_comment(settings.TEXTURE_PACK),
|
||||
'Pack de textures utilisé')
|
||||
|
||||
settings.TEXTURE_PACK = 'squirrel'
|
||||
self.assertEqual(settings.TEXTURE_PACK, 'squirrel')
|
||||
|
||||
settings.write_settings()
|
||||
settings.load_settings()
|
||||
|
||||
self.assertEqual(settings.TEXTURE_PACK, 'squirrel')
|
Reference in New Issue
Block a user