From 3f374d2558ba46963d3638fb0479db11139fffa0 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 10 Nov 2020 21:41:54 +0100 Subject: [PATCH 01/58] Spawn a random amount of squirrels on the map --- dungeonbattle/game.py | 2 ++ dungeonbattle/interfaces.py | 40 +++++++++++++++++++++----- dungeonbattle/tests/interfaces_test.py | 2 +- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index 3dc60cc..c393602 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -1,4 +1,5 @@ import sys +from random import randint from typing import Any from .entities.player import Player @@ -51,6 +52,7 @@ class Game: self.player = Player() self.player.move(1, 6) self.map.add_entity(self.player) + self.map.spawn_random_entities(randint(1, 5)) @staticmethod def load_game(filename: str) -> None: diff --git a/dungeonbattle/interfaces.py b/dungeonbattle/interfaces.py index bbcd25e..a0c39dc 100644 --- a/dungeonbattle/interfaces.py +++ b/dungeonbattle/interfaces.py @@ -1,5 +1,7 @@ #!/usr/bin/env python from enum import Enum, auto +from random import randint +from typing import List from dungeonbattle.display.texturepack import TexturePack @@ -11,7 +13,7 @@ class Map: """ width: int height: int - tiles: list + tiles: List[List["Tile"]] # coordinates of the point that should be # on the topleft corner of the screen currentx: int @@ -30,8 +32,16 @@ class Map: self.entities.append(entity) entity.map = self + 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): + def load(filename: str) -> "Map": """ Read a file that contains the content of a map, and build a Map object. """ @@ -40,7 +50,7 @@ class Map: return Map.load_from_string(file) @staticmethod - def load_from_string(content: str): + def load_from_string(content: str) -> "Map": """ Load a map represented by its characters and build a Map object. """ @@ -61,6 +71,22 @@ class Map: 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} squirrels 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 + from dungeonbattle.entities.monsters import Squirrel + squirrel = Squirrel() + squirrel.move(y, x) + self.add_entity(squirrel) + class Tile(Enum): EMPTY = auto() @@ -84,7 +110,7 @@ class Tile(Enum): """ Check if an entity (player or not) can move in this tile. """ - return not self.is_wall() + return not self.is_wall() and self != Tile.EMPTY class Entity: @@ -99,10 +125,10 @@ class Entity: def check_move(self, y: int, x: int, move_if_possible: bool = False)\ -> bool: - tile = self.map.tiles[y][x] - if tile.can_walk() and move_if_possible: + free = self.map.is_free(y, x) + if free and move_if_possible: self.move(y, x) - return tile.can_walk() + return free def move(self, y: int, x: int) -> None: self.y = y diff --git a/dungeonbattle/tests/interfaces_test.py b/dungeonbattle/tests/interfaces_test.py index c36e895..c82083c 100644 --- a/dungeonbattle/tests/interfaces_test.py +++ b/dungeonbattle/tests/interfaces_test.py @@ -31,5 +31,5 @@ class TestInterfaces(unittest.TestCase): self.assertFalse(Tile.EMPTY.is_wall()) self.assertTrue(Tile.FLOOR.can_walk()) self.assertFalse(Tile.WALL.can_walk()) - self.assertTrue(Tile.EMPTY.can_walk()) + self.assertFalse(Tile.EMPTY.can_walk()) self.assertRaises(ValueError, Tile.from_ascii_char, 'unknown') From 3f4c809db670348137a9c510521f737a491f52ed Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 10 Nov 2020 21:47:36 +0100 Subject: [PATCH 02/58] =?UTF-8?q?Monsters=20are=20hedgehogs=20=F0=9F=A6=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dungeonbattle/display/mapdisplay.py | 2 +- dungeonbattle/display/texturepack.py | 5 +++++ dungeonbattle/entities/items.py | 1 + dungeonbattle/entities/monsters.py | 3 ++- dungeonbattle/entities/player.py | 1 + dungeonbattle/interfaces.py | 11 ++++++----- dungeonbattle/tests/entities_test.py | 16 ++++++++-------- dungeonbattle/texturepack.py | 0 8 files changed, 24 insertions(+), 15 deletions(-) delete mode 100644 dungeonbattle/texturepack.py diff --git a/dungeonbattle/display/mapdisplay.py b/dungeonbattle/display/mapdisplay.py index 0a07ec0..e71ee2e 100644 --- a/dungeonbattle/display/mapdisplay.py +++ b/dungeonbattle/display/mapdisplay.py @@ -17,7 +17,7 @@ class MapDisplay(Display): def update_pad(self) -> None: self.pad.addstr(0, 0, self.map.draw_string(self.pack)) for e in self.map.entities: - self.pad.addstr(e.y, e.x, self.pack.PLAYER) + self.pad.addstr(e.y, e.x, self.pack[e.name.upper()]) def display(self) -> None: y, x = self.map.currenty, self.map.currentx diff --git a/dungeonbattle/display/texturepack.py b/dungeonbattle/display/texturepack.py index 6929a9c..9cc8ced 100644 --- a/dungeonbattle/display/texturepack.py +++ b/dungeonbattle/display/texturepack.py @@ -15,6 +15,9 @@ class TexturePack: self.__dict__.update(**kwargs) TexturePack._packs[name] = self + def __getitem__(self, item): + return self.__dict__[item] + @classmethod def get_pack(cls, name: str) -> "TexturePack": return cls._packs[name.lower()] @@ -26,6 +29,7 @@ TexturePack.ASCII_PACK = TexturePack( WALL='#', FLOOR='.', PLAYER='@', + HEDGEHOG='*', ) TexturePack.SQUIRREL_PACK = TexturePack( @@ -34,4 +38,5 @@ TexturePack.SQUIRREL_PACK = TexturePack( WALL='█', FLOOR='.', PLAYER='🐿️', + HEDGEHOG='🦔', ) diff --git a/dungeonbattle/entities/items.py b/dungeonbattle/entities/items.py index 3edec91..88c6127 100644 --- a/dungeonbattle/entities/items.py +++ b/dungeonbattle/entities/items.py @@ -17,6 +17,7 @@ class Item(Entity): class Bomb(Item): + name = "bomb" damage: int = 5 exploding: bool diff --git a/dungeonbattle/entities/monsters.py b/dungeonbattle/entities/monsters.py index 59db0e7..2246e50 100644 --- a/dungeonbattle/entities/monsters.py +++ b/dungeonbattle/entities/monsters.py @@ -6,6 +6,7 @@ class Monster(FightingEntity): pass -class Squirrel(Monster): +class Hedgehog(Monster): + name = "hedgehog" maxhealth = 10 strength = 3 diff --git a/dungeonbattle/entities/player.py b/dungeonbattle/entities/player.py index 9f01d0d..bef881c 100644 --- a/dungeonbattle/entities/player.py +++ b/dungeonbattle/entities/player.py @@ -2,6 +2,7 @@ from ..interfaces import FightingEntity class Player(FightingEntity): + name = "player" maxhealth: int = 20 strength: int = 5 intelligence: int = 1 diff --git a/dungeonbattle/interfaces.py b/dungeonbattle/interfaces.py index a0c39dc..82e9af2 100644 --- a/dungeonbattle/interfaces.py +++ b/dungeonbattle/interfaces.py @@ -14,6 +14,7 @@ class Map: width: int height: int tiles: List[List["Tile"]] + entities: List["Entity"] # coordinates of the point that should be # on the topleft corner of the screen currentx: int @@ -73,7 +74,7 @@ class Map: def spawn_random_entities(self, count: int) -> None: """ - Put randomly {count} squirrels on the map, where it is available. + Put randomly {count} hedgehogs on the map, where it is available. """ for _ in range(count): y, x = 0, 0 @@ -82,10 +83,10 @@ class Map: tile = self.tiles[y][x] if tile.can_walk(): break - from dungeonbattle.entities.monsters import Squirrel - squirrel = Squirrel() - squirrel.move(y, x) - self.add_entity(squirrel) + from dungeonbattle.entities.monsters import Hedgehog + hedgehog = Hedgehog() + hedgehog.move(y, x) + self.add_entity(hedgehog) class Tile(Enum): diff --git a/dungeonbattle/tests/entities_test.py b/dungeonbattle/tests/entities_test.py index 00ea33b..1d3a04b 100644 --- a/dungeonbattle/tests/entities_test.py +++ b/dungeonbattle/tests/entities_test.py @@ -1,7 +1,7 @@ import unittest from dungeonbattle.entities.items import Bomb, Item -from dungeonbattle.entities.monsters import Squirrel +from dungeonbattle.entities.monsters import Hedgehog from dungeonbattle.entities.player import Player from dungeonbattle.interfaces import Entity, Map @@ -27,7 +27,7 @@ class TestEntities(unittest.TestCase): """ Test some random stuff with fighting entities. """ - entity = Squirrel() + entity = Hedgehog() self.assertIsNone(entity.act(self.map)) self.assertEqual(entity.maxhealth, 10) self.assertEqual(entity.maxhealth, entity.health) @@ -58,18 +58,18 @@ class TestEntities(unittest.TestCase): Test some random stuff with bombs. """ item = Bomb() - squirrel = Squirrel() + hedgehog = Hedgehog() self.map.add_entity(item) - self.map.add_entity(squirrel) - squirrel.health = 2 - squirrel.move(41, 42) + self.map.add_entity(hedgehog) + hedgehog.health = 2 + hedgehog.move(41, 42) item.act(self.map) - self.assertFalse(squirrel.dead) + self.assertFalse(hedgehog.dead) item.drop(42, 42) self.assertEqual(item.y, 42) self.assertEqual(item.x, 42) item.act(self.map) - self.assertTrue(squirrel.dead) + self.assertTrue(hedgehog.dead) def test_players(self) -> None: """ diff --git a/dungeonbattle/texturepack.py b/dungeonbattle/texturepack.py deleted file mode 100644 index e69de29..0000000 From d5ef041f48b97be1cd61835546a8f95acb28bcf2 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 10 Nov 2020 22:01:57 +0100 Subject: [PATCH 03/58] Tiles can have multiple width according to the used texture pack for a better support of unicode --- dungeonbattle/display/mapdisplay.py | 10 ++++++---- dungeonbattle/display/texturepack.py | 13 ++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/dungeonbattle/display/mapdisplay.py b/dungeonbattle/display/mapdisplay.py index e71ee2e..85e8c37 100644 --- a/dungeonbattle/display/mapdisplay.py +++ b/dungeonbattle/display/mapdisplay.py @@ -12,12 +12,13 @@ class MapDisplay(Display): def update_map(self, m: Map) -> None: self.map = m - self.pad = self.newpad(m.height, m.width + 1) + self.pad = self.newpad(m.height, self.pack.tile_width * m.width + 1) def update_pad(self) -> None: self.pad.addstr(0, 0, self.map.draw_string(self.pack)) for e in self.map.entities: - self.pad.addstr(e.y, e.x, self.pack[e.name.upper()]) + self.pad.addstr(e.y, self.pack.tile_width * e.x, + self.pack[e.name.upper()]) def display(self) -> None: y, x = self.map.currenty, self.map.currentx @@ -27,10 +28,11 @@ class MapDisplay(Display): 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.map.width - (x + deltax) + self.width - 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.map.width, pmincol)) + 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) diff --git a/dungeonbattle/display/texturepack.py b/dungeonbattle/display/texturepack.py index 9cc8ced..95d8030 100644 --- a/dungeonbattle/display/texturepack.py +++ b/dungeonbattle/display/texturepack.py @@ -2,6 +2,7 @@ class TexturePack: _packs = dict() name: str + tile_width: int EMPTY: str WALL: str FLOOR: str @@ -25,6 +26,7 @@ class TexturePack: TexturePack.ASCII_PACK = TexturePack( name="ascii", + tile_width=1, EMPTY=' ', WALL='#', FLOOR='.', @@ -34,9 +36,10 @@ TexturePack.ASCII_PACK = TexturePack( TexturePack.SQUIRREL_PACK = TexturePack( name="squirrel", - EMPTY=' ', - WALL='█', - FLOOR='.', - PLAYER='🐿️', - HEDGEHOG='🦔', + tile_width=2, + EMPTY=' ', + WALL='██', + FLOOR='..', + PLAYER='🐿️ ', + HEDGEHOG='🦔 ', ) From 26196a7dca3e5b40d0268a8674eba2f53d9213ca Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 10 Nov 2020 22:02:41 +0100 Subject: [PATCH 04/58] Move the camera with the player --- dungeonbattle/entities/player.py | 8 ++++++++ dungeonbattle/game.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/dungeonbattle/entities/player.py b/dungeonbattle/entities/player.py index bef881c..2dce91e 100644 --- a/dungeonbattle/entities/player.py +++ b/dungeonbattle/entities/player.py @@ -13,6 +13,14 @@ class Player(FightingEntity): current_xp: int = 0 max_xp: int = 10 + 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 + def move_up(self) -> bool: return self.check_move(self.y - 1, self.x, True) diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index c393602..23cc598 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -50,8 +50,8 @@ class Game: self.map.currenty = 1 self.map.currentx = 6 self.player = Player() - self.player.move(1, 6) self.map.add_entity(self.player) + self.player.move(1, 6) self.map.spawn_random_entities(randint(1, 5)) @staticmethod From 1684647ea20307b2b7355f88bfa5490b0d12960f Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 10 Nov 2020 22:04:38 +0100 Subject: [PATCH 05/58] Missing type --- dungeonbattle/display/texturepack.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dungeonbattle/display/texturepack.py b/dungeonbattle/display/texturepack.py index 95d8030..fcd2435 100644 --- a/dungeonbattle/display/texturepack.py +++ b/dungeonbattle/display/texturepack.py @@ -1,3 +1,6 @@ +from typing import Any + + class TexturePack: _packs = dict() @@ -16,7 +19,7 @@ class TexturePack: self.__dict__.update(**kwargs) TexturePack._packs[name] = self - def __getitem__(self, item): + def __getitem__(self, item) -> Any: return self.__dict__[item] @classmethod From 2045a579074f743352c8bd2c5c4863c009fa2f07 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 10 Nov 2020 22:07:03 +0100 Subject: [PATCH 06/58] Don't add any additional space to unicode entities --- dungeonbattle/display/texturepack.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dungeonbattle/display/texturepack.py b/dungeonbattle/display/texturepack.py index fcd2435..adb1d49 100644 --- a/dungeonbattle/display/texturepack.py +++ b/dungeonbattle/display/texturepack.py @@ -43,6 +43,6 @@ TexturePack.SQUIRREL_PACK = TexturePack( EMPTY=' ', WALL='██', FLOOR='..', - PLAYER='🐿️ ', - HEDGEHOG='🦔 ', + PLAYER='🐿️', + HEDGEHOG='🦔', ) From ec6b90fba2d13f3f916d6ad80d694dfd794e44b7 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 10 Nov 2020 22:10:28 +0100 Subject: [PATCH 07/58] All entities can move, not only players --- dungeonbattle/entities/player.py | 12 ------------ dungeonbattle/interfaces.py | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/dungeonbattle/entities/player.py b/dungeonbattle/entities/player.py index 2dce91e..c9a6334 100644 --- a/dungeonbattle/entities/player.py +++ b/dungeonbattle/entities/player.py @@ -21,18 +21,6 @@ class Player(FightingEntity): self.map.currenty = y self.map.currentx = x - def move_up(self) -> bool: - return self.check_move(self.y - 1, self.x, True) - - def move_down(self) -> bool: - return self.check_move(self.y + 1, self.x, True) - - def move_left(self) -> bool: - return self.check_move(self.y, self.x - 1, True) - - def move_right(self) -> bool: - return self.check_move(self.y, self.x + 1, True) - def level_up(self) -> None: while self.current_xp > self.max_xp: self.level += 1 diff --git a/dungeonbattle/interfaces.py b/dungeonbattle/interfaces.py index 82e9af2..4c281e4 100644 --- a/dungeonbattle/interfaces.py +++ b/dungeonbattle/interfaces.py @@ -135,6 +135,22 @@ class Entity: self.y = y self.x = x + def move_up(self, force: bool = False) -> bool: + 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: + 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: + 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: + 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. From a8223aab2e9bdc019aabfb0ff33c9a3fa80f17dd Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 10 Nov 2020 22:30:55 +0100 Subject: [PATCH 08/58] Add possibility to define the background color of entities (black in ASCII, white in Unicode) --- dungeonbattle/display/display.py | 7 +++++++ dungeonbattle/display/mapdisplay.py | 7 +++++-- dungeonbattle/display/texturepack.py | 21 +++++++++++++++++---- dungeonbattle/term_manager.py | 2 ++ dungeonbattle/tests/screen.py | 2 +- 5 files changed, 32 insertions(+), 7 deletions(-) diff --git a/dungeonbattle/display/display.py b/dungeonbattle/display/display.py index 64314e9..1a2e0ca 100644 --- a/dungeonbattle/display/display.py +++ b/dungeonbattle/display/display.py @@ -19,6 +19,13 @@ class Display: 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) -> None: self.x = x self.y = y diff --git a/dungeonbattle/display/mapdisplay.py b/dungeonbattle/display/mapdisplay.py index 85e8c37..8ab7be7 100644 --- a/dungeonbattle/display/mapdisplay.py +++ b/dungeonbattle/display/mapdisplay.py @@ -15,10 +15,13 @@ class MapDisplay(Display): self.pad = self.newpad(m.height, self.pack.tile_width * m.width + 1) def update_pad(self) -> None: - self.pad.addstr(0, 0, self.map.draw_string(self.pack)) + 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.pack[e.name.upper()], self.color_pair(2)) def display(self) -> None: y, x = self.map.currenty, self.map.currentx diff --git a/dungeonbattle/display/texturepack.py b/dungeonbattle/display/texturepack.py index adb1d49..7d9113f 100644 --- a/dungeonbattle/display/texturepack.py +++ b/dungeonbattle/display/texturepack.py @@ -1,3 +1,4 @@ +import curses from typing import Any @@ -6,6 +7,10 @@ class TexturePack: 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 @@ -19,7 +24,7 @@ class TexturePack: self.__dict__.update(**kwargs) TexturePack._packs[name] = self - def __getitem__(self, item) -> Any: + def __getitem__(self, item: str) -> Any: return self.__dict__[item] @classmethod @@ -30,6 +35,10 @@ class TexturePack: 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='.', @@ -40,9 +49,13 @@ TexturePack.ASCII_PACK = TexturePack( 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='🐿️', + WALL='🟫', + FLOOR='██', + PLAYER='🐿 ️', HEDGEHOG='🦔', ) diff --git a/dungeonbattle/term_manager.py b/dungeonbattle/term_manager.py index ab7f4dd..a425272 100644 --- a/dungeonbattle/term_manager.py +++ b/dungeonbattle/term_manager.py @@ -13,6 +13,8 @@ class TermManager: # pragma: no cover curses.cbreak() # make cursor invisible curses.curs_set(False) + # Enable colors + curses.start_color() def __enter__(self): return self diff --git a/dungeonbattle/tests/screen.py b/dungeonbattle/tests/screen.py index b9ec851..6eb2cd0 100644 --- a/dungeonbattle/tests/screen.py +++ b/dungeonbattle/tests/screen.py @@ -3,7 +3,7 @@ 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) -> None: + def addstr(self, y: int, x: int, message: str, color: int = 0) -> None: pass def refresh(self, pminrow: int, pmincol: int, sminrow: int, From d26b66f337d0112c277697592bc5d5589f7ddb99 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 10 Nov 2020 22:34:12 +0100 Subject: [PATCH 09/58] Add possibility to define the background color of entities (black in ASCII, white in Unicode) --- dungeonbattle/entities/monsters.py | 12 +++++++++++- dungeonbattle/game.py | 1 + dungeonbattle/interfaces.py | 7 +++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/dungeonbattle/entities/monsters.py b/dungeonbattle/entities/monsters.py index 2246e50..7c19c62 100644 --- a/dungeonbattle/entities/monsters.py +++ b/dungeonbattle/entities/monsters.py @@ -1,9 +1,19 @@ +from random import choice + from ..interfaces import FightingEntity, Map class Monster(FightingEntity): def act(self, m: Map) -> None: - pass + """ + 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. + """ + # TODO If a player is close, move to the player + while True: + if choice([self.move_up, self.move_down, + self.move_left, self.move_right])(): + break class Hedgehog(Monster): diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index 23cc598..e8f412c 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -71,6 +71,7 @@ class Game: self.display_refresh() key = screen.getkey() self.handle_key_pressed(self.translate_key(key)) + self.map.tick() def translate_key(self, key: str) -> KeyValues: """ diff --git a/dungeonbattle/interfaces.py b/dungeonbattle/interfaces.py index 4c281e4..65e381d 100644 --- a/dungeonbattle/interfaces.py +++ b/dungeonbattle/interfaces.py @@ -88,6 +88,13 @@ class Map: hedgehog.move(y, x) self.add_entity(hedgehog) + def tick(self) -> None: + """ + Trigger all entity events. + """ + for entity in self.entities: + entity.act(self) + class Tile(Enum): EMPTY = auto() From 12ee436f4dbc8d5cb3c28793a18eb4f2e9addf28 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 10 Nov 2020 22:44:53 +0100 Subject: [PATCH 10/58] Fix broken game test --- dungeonbattle/entities/monsters.py | 2 +- dungeonbattle/interfaces.py | 10 +++++++++- dungeonbattle/tests/entities_test.py | 8 +++++++- dungeonbattle/tests/game_test.py | 6 ++++++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/dungeonbattle/entities/monsters.py b/dungeonbattle/entities/monsters.py index 7c19c62..f7dd413 100644 --- a/dungeonbattle/entities/monsters.py +++ b/dungeonbattle/entities/monsters.py @@ -10,7 +10,7 @@ class Monster(FightingEntity): And if a player is close to the monster, the monster run on the player. """ # TODO If a player is close, move to the player - while True: + for _ in range(100): if choice([self.move_up, self.move_down, self.move_left, self.move_right])(): break diff --git a/dungeonbattle/interfaces.py b/dungeonbattle/interfaces.py index 65e381d..04ed8d1 100644 --- a/dungeonbattle/interfaces.py +++ b/dungeonbattle/interfaces.py @@ -33,6 +33,12 @@ class 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 is_free(self, y: int, x: int) -> bool: """ Indicates that the case at the coordinates (y, x) is empty. @@ -138,9 +144,10 @@ class Entity: self.move(y, x) return free - def move(self, y: int, x: int) -> None: + def move(self, y: int, x: int) -> bool: self.y = y self.x = x + return True def move_up(self, force: bool = False) -> bool: return self.move(self.y - 1, self.x) if force else \ @@ -192,3 +199,4 @@ class FightingEntity(Entity): def die(self) -> None: self.dead = True + self.map.remove_entity(self) diff --git a/dungeonbattle/tests/entities_test.py b/dungeonbattle/tests/entities_test.py index 1d3a04b..adca2ec 100644 --- a/dungeonbattle/tests/entities_test.py +++ b/dungeonbattle/tests/entities_test.py @@ -28,7 +28,7 @@ class TestEntities(unittest.TestCase): Test some random stuff with fighting entities. """ entity = Hedgehog() - self.assertIsNone(entity.act(self.map)) + self.map.add_entity(entity) self.assertEqual(entity.maxhealth, 10) self.assertEqual(entity.maxhealth, entity.health) self.assertEqual(entity.strength, 3) @@ -41,6 +41,12 @@ class TestEntities(unittest.TestCase): self.assertIsNone(entity.hit(entity)) self.assertTrue(entity.dead) + entity = Hedgehog() + self.map.add_entity(entity) + entity.move(2, 6) + self.map.tick() + self.assertFalse(entity.y == 2 and entity.x == 6) + def test_items(self) -> None: """ Test some random stuff with items. diff --git a/dungeonbattle/tests/game_test.py b/dungeonbattle/tests/game_test.py index 2498b48..c83e851 100644 --- a/dungeonbattle/tests/game_test.py +++ b/dungeonbattle/tests/game_test.py @@ -4,6 +4,7 @@ import unittest from dungeonbattle.bootstrap import Bootstrap from dungeonbattle.display.display import Display from dungeonbattle.display.display_manager import DisplayManager +from dungeonbattle.entities.player import Player from dungeonbattle.game import Game, KeyValues, GameMode from dungeonbattle.menus import MainMenuValues @@ -90,6 +91,11 @@ class TestGame(unittest.TestCase): 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 From f9f02b6621b679c0352c703fbcb3665ed9b06481 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 10 Nov 2020 22:59:02 +0100 Subject: [PATCH 11/58] Move to closest player if it is close --- dungeonbattle/entities/monsters.py | 29 ++++++++++++++++++++++++---- dungeonbattle/interfaces.py | 19 +++++++++++++++++- dungeonbattle/tests/entities_test.py | 5 +++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/dungeonbattle/entities/monsters.py b/dungeonbattle/entities/monsters.py index f7dd413..67063da 100644 --- a/dungeonbattle/entities/monsters.py +++ b/dungeonbattle/entities/monsters.py @@ -1,5 +1,6 @@ from random import choice +from .player import Player from ..interfaces import FightingEntity, Map @@ -9,12 +10,32 @@ class Monster(FightingEntity): 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. """ - # TODO If a player is close, move to the player - for _ in range(100): - if choice([self.move_up, self.move_down, - self.move_left, self.move_right])(): + target = None + for entity in m.entities: + if self.distance_squared(entity) <= 25 and \ + isinstance(entity, Player): + target = entity break + if target: + # Move to target player + y, x = self.vector(target) + if abs(y) > abs(x): # Move vertically + if y > 0: + self.move_down() + else: + self.move_up() + else: # Move horizontally + if x > 0: + self.move_right() + else: + self.move_left() + else: + for _ in range(100): + if choice([self.move_up, self.move_down, + self.move_left, self.move_right])(): + break + class Hedgehog(Monster): name = "hedgehog" diff --git a/dungeonbattle/interfaces.py b/dungeonbattle/interfaces.py index 04ed8d1..5611795 100644 --- a/dungeonbattle/interfaces.py +++ b/dungeonbattle/interfaces.py @@ -1,7 +1,8 @@ #!/usr/bin/env python from enum import Enum, auto +from math import sqrt from random import randint -from typing import List +from typing import List, Tuple from dungeonbattle.display.texturepack import TexturePack @@ -172,6 +173,22 @@ class Entity: """ 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 vector(self, other: "Entity") -> Tuple[int, int]: + return other.y - self.y, other.x - self.x + class FightingEntity(Entity): maxhealth: int diff --git a/dungeonbattle/tests/entities_test.py b/dungeonbattle/tests/entities_test.py index adca2ec..3d61812 100644 --- a/dungeonbattle/tests/entities_test.py +++ b/dungeonbattle/tests/entities_test.py @@ -23,6 +23,11 @@ class TestEntities(unittest.TestCase): 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. From 279d9d9f5819469519e2cb6b3c9b45e3d4857d97 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 10 Nov 2020 23:00:09 +0100 Subject: [PATCH 12/58] Monsters can hit the player. No respect for unarmed people... --- dungeonbattle/entities/monsters.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dungeonbattle/entities/monsters.py b/dungeonbattle/entities/monsters.py index 67063da..61a6ebf 100644 --- a/dungeonbattle/entities/monsters.py +++ b/dungeonbattle/entities/monsters.py @@ -30,6 +30,8 @@ class Monster(FightingEntity): self.move_right() else: self.move_left() + if self.distance_squared(target) <= 1: + self.hit(target) else: for _ in range(100): if choice([self.move_up, self.move_down, From 5addd42535419d06fe1b0db93700a7608d974ef1 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 00:38:02 +0100 Subject: [PATCH 13/58] Only refresh entities if the player moved, ignore most events --- dungeonbattle/game.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index e8f412c..8be446d 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -71,7 +71,6 @@ class Game: self.display_refresh() key = screen.getkey() self.handle_key_pressed(self.translate_key(key)) - self.map.tick() def translate_key(self, key: str) -> KeyValues: """ @@ -112,13 +111,17 @@ class Game: In play mode, arrows or zqsd should move the main character. """ if key == KeyValues.UP: - self.player.move_up() + if self.player.move_up(): + self.map.tick() elif key == KeyValues.DOWN: - self.player.move_down() + if self.player.move_down(): + self.map.tick() elif key == KeyValues.LEFT: - self.player.move_left() + if self.player.move_left(): + self.map.tick() elif key == KeyValues.RIGHT: - self.player.move_right() + if self.player.move_right(): + self.map.tick() elif key == KeyValues.SPACE: self.state = GameMode.MAINMENU From 9909b125010b3bb7731e352d51fa560a1f1dc3b5 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 00:50:47 +0100 Subject: [PATCH 14/58] Fight other entities --- dungeonbattle/entities/player.py | 17 +++++++++++++++++ dungeonbattle/game.py | 6 ++++++ dungeonbattle/settings.py | 1 + 3 files changed, 24 insertions(+) diff --git a/dungeonbattle/entities/player.py b/dungeonbattle/entities/player.py index c9a6334..99763e1 100644 --- a/dungeonbattle/entities/player.py +++ b/dungeonbattle/entities/player.py @@ -1,3 +1,5 @@ +from random import randint + from ..interfaces import FightingEntity @@ -26,7 +28,22 @@ class Player(FightingEntity): self.level += 1 self.current_xp -= self.max_xp self.max_xp = self.level * 10 + self.health = self.maxhealth def add_xp(self, xp: int) -> None: self.current_xp += xp self.level_up() + + def fight(self) -> bool: + """ + Fight all f + """ + one_fight = False + for entity in self.map.entities: + if entity != self and isinstance(entity, FightingEntity) and\ + self.distance_squared(entity) <= 1: + self.hit(entity) + one_fight = True + if entity.dead: + self.add_xp(randint(3, 7)) + return one_fight diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index 8be446d..e4e7300 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -24,6 +24,7 @@ class KeyValues(Enum): RIGHT = auto() ENTER = auto() SPACE = auto() + FIGHT = auto() class Game: @@ -88,6 +89,8 @@ class Game: elif key in (self.settings.KEY_UP_PRIMARY, self.settings.KEY_UP_SECONDARY): return KeyValues.UP + elif key == self.settings.KEY_FIGHT: + return KeyValues.FIGHT elif key == self.settings.KEY_ENTER: return KeyValues.ENTER elif key == ' ': @@ -122,6 +125,9 @@ class Game: elif key == KeyValues.RIGHT: if self.player.move_right(): self.map.tick() + elif key == KeyValues.FIGHT: + if self.player.fight(): + self.map.tick() elif key == KeyValues.SPACE: self.state = GameMode.MAINMENU diff --git a/dungeonbattle/settings.py b/dungeonbattle/settings.py index 258d88f..0558614 100644 --- a/dungeonbattle/settings.py +++ b/dungeonbattle/settings.py @@ -27,6 +27,7 @@ class Settings: ['d', 'Touche principale pour aller vers la droite'] self.KEY_RIGHT_SECONDARY = \ ['KEY_RIGHT', 'Touche secondaire pour aller vers la droite'] + self.KEY_FIGHT = ['f', 'Touche pour frapper un ennemi'] self.KEY_ENTER = \ ['\n', 'Touche pour valider un menu'] self.TEXTURE_PACK = ['ascii', 'Pack de textures utilisé'] From c5e6459d37150f248b5e119667dace181623f1f1 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 01:04:30 +0100 Subject: [PATCH 15/58] Fight other entities while bumping them, without any weapon --- dungeonbattle/entities/player.py | 16 +++++++++------- dungeonbattle/game.py | 6 ------ dungeonbattle/settings.py | 1 - 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/dungeonbattle/entities/player.py b/dungeonbattle/entities/player.py index 99763e1..1f64d3d 100644 --- a/dungeonbattle/entities/player.py +++ b/dungeonbattle/entities/player.py @@ -34,16 +34,18 @@ class Player(FightingEntity): self.current_xp += xp self.level_up() - def fight(self) -> bool: + def check_move(self, y: int, x: int, move_if_possible: bool = False) \ + -> bool: """ - Fight all f + 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. """ - one_fight = False for entity in self.map.entities: - if entity != self and isinstance(entity, FightingEntity) and\ - self.distance_squared(entity) <= 1: + if entity.y == y and entity.x == x and \ + isinstance(entity, FightingEntity): self.hit(entity) - one_fight = True if entity.dead: self.add_xp(randint(3, 7)) - return one_fight + return True + return super().check_move(y, x, move_if_possible) diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index e4e7300..8be446d 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -24,7 +24,6 @@ class KeyValues(Enum): RIGHT = auto() ENTER = auto() SPACE = auto() - FIGHT = auto() class Game: @@ -89,8 +88,6 @@ class Game: elif key in (self.settings.KEY_UP_PRIMARY, self.settings.KEY_UP_SECONDARY): return KeyValues.UP - elif key == self.settings.KEY_FIGHT: - return KeyValues.FIGHT elif key == self.settings.KEY_ENTER: return KeyValues.ENTER elif key == ' ': @@ -125,9 +122,6 @@ class Game: elif key == KeyValues.RIGHT: if self.player.move_right(): self.map.tick() - elif key == KeyValues.FIGHT: - if self.player.fight(): - self.map.tick() elif key == KeyValues.SPACE: self.state = GameMode.MAINMENU diff --git a/dungeonbattle/settings.py b/dungeonbattle/settings.py index 0558614..258d88f 100644 --- a/dungeonbattle/settings.py +++ b/dungeonbattle/settings.py @@ -27,7 +27,6 @@ class Settings: ['d', 'Touche principale pour aller vers la droite'] self.KEY_RIGHT_SECONDARY = \ ['KEY_RIGHT', 'Touche secondaire pour aller vers la droite'] - self.KEY_FIGHT = ['f', 'Touche pour frapper un ennemi'] self.KEY_ENTER = \ ['\n', 'Touche pour valider un menu'] self.TEXTURE_PACK = ['ascii', 'Pack de textures utilisé'] From 6e8cfdcb1ae74b712b08741deb60c31acfdd7054 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 01:07:19 +0100 Subject: [PATCH 16/58] Spawn new entities on each level (will be removed, only for tests) --- dungeonbattle/entities/player.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dungeonbattle/entities/player.py b/dungeonbattle/entities/player.py index 1f64d3d..7fdf041 100644 --- a/dungeonbattle/entities/player.py +++ b/dungeonbattle/entities/player.py @@ -24,13 +24,22 @@ class Player(FightingEntity): self.map.currentx = x 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(self.level, self.level * 5)) 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() From 56ba9d186e394a2450ed5fec50c58f94fa6906c9 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 01:17:00 +0100 Subject: [PATCH 17/58] Display message if we are dead --- dungeonbattle/display/statsdisplay.py | 10 ++++++++-- dungeonbattle/entities/player.py | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/dungeonbattle/display/statsdisplay.py b/dungeonbattle/display/statsdisplay.py index bf204dd..2eb76aa 100644 --- a/dungeonbattle/display/statsdisplay.py +++ b/dungeonbattle/display/statsdisplay.py @@ -1,3 +1,5 @@ +import curses + from .display import Display from dungeonbattle.entities.player import Player @@ -9,6 +11,7 @@ class StatsDisplay(Display): 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 @@ -32,10 +35,13 @@ class StatsDisplay(Display): for _ in range(self.width - len(string3) - 1): string3 = string3 + " " self.pad.addstr(2, 0, string3) + if self.player.dead: + self.pad.addstr(3, 0, "YOU ARE DEAD", + 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, - 2 + self.y, - self.width + self.x) + 3 + self.y, self.width + self.x) diff --git a/dungeonbattle/entities/player.py b/dungeonbattle/entities/player.py index 7fdf041..4e3affc 100644 --- a/dungeonbattle/entities/player.py +++ b/dungeonbattle/entities/player.py @@ -50,6 +50,9 @@ class Player(FightingEntity): 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 and \ isinstance(entity, FightingEntity): From d9b7db742a0824363e31da5d7447adc7714423d3 Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Wed, 11 Nov 2020 14:46:25 +0100 Subject: [PATCH 18/58] Added settings diplay and ability to change the keys (there is a refreshing problem though) --- dungeonbattle/display/display_manager.py | 8 ++++++-- dungeonbattle/display/menudisplay.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/dungeonbattle/display/display_manager.py b/dungeonbattle/display/display_manager.py index 5c249c8..ea5c8ca 100644 --- a/dungeonbattle/display/display_manager.py +++ b/dungeonbattle/display/display_manager.py @@ -1,7 +1,7 @@ import curses from dungeonbattle.display.mapdisplay import MapDisplay from dungeonbattle.display.statsdisplay import StatsDisplay -from dungeonbattle.display.menudisplay import MainMenuDisplay +from dungeonbattle.display.menudisplay import MenuDisplay, MainMenuDisplay from dungeonbattle.display.texturepack import TexturePack from typing import Any from dungeonbattle.game import Game, GameMode @@ -17,9 +17,11 @@ class DisplayManager: self.statsdisplay = StatsDisplay(screen, pack) self.mainmenudisplay = MainMenuDisplay(self.game.main_menu, screen, pack) + self.settingsmenudisplay = MenuDisplay(screen, pack) self.displays = [self.statsdisplay, self.mapdisplay, - self.mainmenudisplay] + self.mainmenudisplay, self.settingsmenudisplay] self.update_game_components() + self.settingsmenudisplay.update_menu(self.game.settings_menu) def update_game_components(self) -> None: for d in self.displays: @@ -34,6 +36,8 @@ class DisplayManager: 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: diff --git a/dungeonbattle/display/menudisplay.py b/dungeonbattle/display/menudisplay.py index aeed447..9dd416d 100644 --- a/dungeonbattle/display/menudisplay.py +++ b/dungeonbattle/display/menudisplay.py @@ -22,7 +22,7 @@ class MenuDisplay(Display): def update_pad(self) -> None: for i in range(self.trueheight): - self.pad.addstr(i, 0, " ") + self.pad.addstr(i, 0, " " + self.values[i]) # set a marker on the selected line self.pad.addstr(self.menu.position, 0, ">") From e3d28409f52442e4e8d1690a374557047ad9273b Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Wed, 11 Nov 2020 14:56:00 +0100 Subject: [PATCH 19/58] Repaired tthe display problem for settings menu --- dungeonbattle/display/menudisplay.py | 5 ++++- dungeonbattle/game.py | 21 +++++++++++++++++---- dungeonbattle/menus.py | 7 +++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/dungeonbattle/display/menudisplay.py b/dungeonbattle/display/menudisplay.py index 9dd416d..d25fe61 100644 --- a/dungeonbattle/display/menudisplay.py +++ b/dungeonbattle/display/menudisplay.py @@ -11,7 +11,6 @@ class MenuDisplay(Display): def update_menu(self, menu: Menu) -> None: self.menu = menu - self.values = [str(a) for a in menu.values] self.trueheight = len(self.values) self.truewidth = max([len(a) for a in self.values]) @@ -54,6 +53,10 @@ class MenuDisplay(Display): def preferred_height(self) -> int: return self.trueheight + 2 + @property + def values(self): + return [str(a) for a in self.menu.values] + class MainMenuDisplay(Display): def __init__(self, menu: MainMenu, *args): diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index 8be446d..98e56c2 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -37,9 +37,11 @@ class 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: """ @@ -70,7 +72,7 @@ class Game: screen.refresh() self.display_refresh() key = screen.getkey() - self.handle_key_pressed(self.translate_key(key)) + self.handle_key_pressed(self.translate_key(key), screen) def translate_key(self, key: str) -> KeyValues: """ @@ -93,7 +95,7 @@ class Game: elif key == ' ': return KeyValues.SPACE - def handle_key_pressed(self, key: KeyValues) -> None: + def handle_key_pressed(self, key: KeyValues, screen) -> None: """ Indicates what should be done when the given key is pressed, according to the current game state. @@ -103,7 +105,7 @@ class Game: elif self.state == GameMode.MAINMENU: self.handle_key_pressed_main_menu(key) elif self.state == GameMode.SETTINGS: - self.handle_key_pressed_settings(key) + self.handle_key_pressed_settings(key,screen) self.display_refresh() def handle_key_pressed_play(self, key: KeyValues) -> None: @@ -142,9 +144,20 @@ class Game: elif option == menus.MainMenuValues.EXIT: sys.exit(0) - def handle_key_pressed_settings(self, key: KeyValues) -> None: + def handle_key_pressed_settings(self, key: KeyValues, screen : Any) -> None: """ For now, in the settings mode, we can only go backwards. """ if key == KeyValues.SPACE: self.state = GameMode.MAINMENU + if key == KeyValues.DOWN: + self.settings_menu.go_down() + if key == KeyValues.UP: + self.settings_menu.go_up() + if key == KeyValues.ENTER: + option = self.settings_menu.validate().split(": ")[0] + if option != "TEXTURE_PACK": + newkey = screen.getkey() + self.settings.__setattr__(option, newkey) + self.settings.write_settings() + self.settings_menu.update_values(self.settings) diff --git a/dungeonbattle/menus.py b/dungeonbattle/menus.py index 68eeab7..06ef556 100644 --- a/dungeonbattle/menus.py +++ b/dungeonbattle/menus.py @@ -1,5 +1,6 @@ from enum import Enum from typing import Any +from .settings import Settings class Menu: @@ -30,6 +31,12 @@ class MainMenuValues(Enum): class MainMenu(Menu): values = [e for e in MainMenuValues] +class SettingsMenu(Menu) : + def __init__(self): + super().__init__() + def update_values(self, settings : Settings): + s = settings.dumps_to_string() + self.values = s[6:-2].replace("\"","").split(",\n ") class ArbitraryMenu(Menu): def __init__(self, values: list): From 1e66e263bc36421f758c52caa7893f35a0e013cf Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 15:09:28 +0100 Subject: [PATCH 20/58] Use bricks for walls --- dungeonbattle/display/texturepack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dungeonbattle/display/texturepack.py b/dungeonbattle/display/texturepack.py index 7d9113f..57d8bf6 100644 --- a/dungeonbattle/display/texturepack.py +++ b/dungeonbattle/display/texturepack.py @@ -54,7 +54,7 @@ TexturePack.SQUIRREL_PACK = TexturePack( entity_fg_color=curses.COLOR_WHITE, entity_bg_color=curses.COLOR_WHITE, EMPTY=' ', - WALL='🟫', + WALL='🧱', FLOOR='██', PLAYER='🐿 ️', HEDGEHOG='🦔', From 2f3a03dbf76fbbdce4665b1c1f824582bb315c36 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 15:10:46 +0100 Subject: [PATCH 21/58] The camera position should consider the width of a tile --- dungeonbattle/display/mapdisplay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dungeonbattle/display/mapdisplay.py b/dungeonbattle/display/mapdisplay.py index 8ab7be7..aa40039 100644 --- a/dungeonbattle/display/mapdisplay.py +++ b/dungeonbattle/display/mapdisplay.py @@ -24,7 +24,7 @@ class MapDisplay(Display): self.pack[e.name.upper()], self.color_pair(2)) def display(self) -> None: - y, x = self.map.currenty, self.map.currentx + 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) From d08ff7061fe04c66bf42efb377bdea8ead68bb6d Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 15:25:50 +0100 Subject: [PATCH 22/58] Use Dijkstra algorithm to describe monster paths --- dungeonbattle/entities/monsters.py | 21 ++++++++------------- dungeonbattle/entities/player.py | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/dungeonbattle/entities/monsters.py b/dungeonbattle/entities/monsters.py index 61a6ebf..161a3b1 100644 --- a/dungeonbattle/entities/monsters.py +++ b/dungeonbattle/entities/monsters.py @@ -17,20 +17,15 @@ class Monster(FightingEntity): target = entity break - if target: + # 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 - y, x = self.vector(target) - if abs(y) > abs(x): # Move vertically - if y > 0: - self.move_down() - else: - self.move_up() - else: # Move horizontally - if x > 0: - self.move_right() - else: - self.move_left() - if self.distance_squared(target) <= 1: + 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): diff --git a/dungeonbattle/entities/player.py b/dungeonbattle/entities/player.py index 4e3affc..e9ba45f 100644 --- a/dungeonbattle/entities/player.py +++ b/dungeonbattle/entities/player.py @@ -1,4 +1,5 @@ from random import randint +from typing import Dict, Tuple from ..interfaces import FightingEntity @@ -14,6 +15,7 @@ class Player(FightingEntity): level: int = 1 current_xp: int = 0 max_xp: int = 10 + paths: Dict[Tuple[int, int], Tuple[int, int]] def move(self, y: int, x: int) -> None: """ @@ -22,6 +24,7 @@ class Player(FightingEntity): super().move(y, x) self.map.currenty = y self.map.currentx = x + self.recalculate_paths() def level_up(self) -> None: """ @@ -61,3 +64,26 @@ class Player(FightingEntity): self.add_xp(randint(3, 7)) return True return super().check_move(y, x, move_if_possible) + + def recalculate_paths(self) -> None: + """ + Use Dijkstra algorithm to calculate best paths + for monsters to go to the player. + """ + queue = [(self.y, self.x)] + visited = [] + predecessors = {} + while queue: + y, x = queue.pop(0) + visited.append((y, x)) + 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) + queue.append((new_y, new_x)) + self.paths = predecessors From 0f53407b3d552593423401e55e54dfc935c8dfca Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 16:00:40 +0100 Subject: [PATCH 23/58] Use a larger example map --- dungeonbattle/game.py | 4 ++-- dungeonbattle/interfaces.py | 5 +---- resources/example_map_2.txt | 40 +++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 resources/example_map_2.txt diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index 8be446d..a2ea93a 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -46,12 +46,12 @@ class Game: Create a new game on the screen. """ # TODO generate a new map procedurally - self.map = Map.load("resources/example_map.txt") + self.map = Map.load("resources/example_map_2.txt") self.map.currenty = 1 self.map.currentx = 6 self.player = Player() self.map.add_entity(self.player) - self.player.move(1, 6) + self.player.move(1, 14) self.map.spawn_random_entities(randint(1, 5)) @staticmethod diff --git a/dungeonbattle/interfaces.py b/dungeonbattle/interfaces.py index 5611795..7b89ec6 100644 --- a/dungeonbattle/interfaces.py +++ b/dungeonbattle/interfaces.py @@ -2,7 +2,7 @@ from enum import Enum, auto from math import sqrt from random import randint -from typing import List, Tuple +from typing import List from dungeonbattle.display.texturepack import TexturePack @@ -186,9 +186,6 @@ class Entity: """ return sqrt(self.distance_squared(other)) - def vector(self, other: "Entity") -> Tuple[int, int]: - return other.y - self.y, other.x - self.x - class FightingEntity(Entity): maxhealth: int diff --git a/resources/example_map_2.txt b/resources/example_map_2.txt new file mode 100644 index 0000000..bcb4b60 --- /dev/null +++ b/resources/example_map_2.txt @@ -0,0 +1,40 @@ + ########### ######### + #.........# #.......# + #.........# ############.......# + #.........###############..........#.......############## + #.........#........................#....................# + #.........#.............#..........#.......#............# + ########.########.............#..................#............# + #.........# #.............####.#######.......#............# + #.........# #.............##.........###################### + #.........# #####.##########.........# ########### + #.........# #......# #.........# #.........# + ########.##########......# #.........# #.........# + #...........##......# #.........# #.........# + #...........##......# #.........# #.........# + #...........##......# #.........# ################.###### + #...........##......# #.........# #.................############ + #...........##......# ########.########.......#.........#..........# + #...........##......# #...............#.......#.........#..........# + #...........######### #...............#.......#.........#..........# + #...........# #...............#.......#....................# + #####.####### #.......................#.........#..........# + #.........# #...............###################..........# + #.........############ #...............# #..........# + #.........#..........# #...............# ############ + #....................#####.###########.############# + ########.#########...................# #.............# + #........# #..........#........# #.............######### + #........# ######.##########........# #.............#.......# + #........# #..........# #........# #.....................# + #........# #..........# #........# #.............#.......# + #........# #..........# #........# #.............#.......# + #........# #..........# #........# #.............#.......# + #........# #..........#########.##### #.............#.......# + #........# #..........#.........# ##########.############.####### + #........# #..........#.........# #..............# #..........# + ########## #..........#.........# #..............# #..........# + ############.........# #..............# #..........# + #.........# #..............# #..........# + ########### #..............# #..........# + ################ ############ From 279ef2439de0c6152967a489520a14f3b88f9777 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 16:02:32 +0100 Subject: [PATCH 24/58] Limit the complexity of the dijkstra to eight --- dungeonbattle/entities/player.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dungeonbattle/entities/player.py b/dungeonbattle/entities/player.py index e9ba45f..c656130 100644 --- a/dungeonbattle/entities/player.py +++ b/dungeonbattle/entities/player.py @@ -65,17 +65,20 @@ class Player(FightingEntity): return True return super().check_move(y, x, move_if_possible) - def recalculate_paths(self) -> None: + 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 \ @@ -85,5 +88,6 @@ class Player(FightingEntity): (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 From d75f4290ffebd0f67f7dcea425f794f2d3524eb4 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 16:09:03 +0100 Subject: [PATCH 25/58] Store the start position in a map --- dungeonbattle/interfaces.py | 13 ++++-- dungeonbattle/tests/interfaces_test.py | 2 +- resources/example_map.txt | 3 +- resources/example_map_2.txt | 63 +++++++++++++------------- 4 files changed, 45 insertions(+), 36 deletions(-) diff --git a/dungeonbattle/interfaces.py b/dungeonbattle/interfaces.py index 7b89ec6..a1ab13c 100644 --- a/dungeonbattle/interfaces.py +++ b/dungeonbattle/interfaces.py @@ -14,6 +14,8 @@ class Map: """ width: int height: int + start_y: int + start_x: int tiles: List[List["Tile"]] entities: List["Entity"] # coordinates of the point that should be @@ -21,9 +23,12 @@ class Map: currentx: int currenty: int - def __init__(self, width: int, height: int, tiles: list): + 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 = [] @@ -63,13 +68,15 @@ class Map: Load a map represented by its characters and build a Map object. """ lines = content.split("\n") - lines = [line for line in lines if line] + 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) + return Map(width, height, tiles, start_y, start_x) def draw_string(self, pack: TexturePack) -> str: """ diff --git a/dungeonbattle/tests/interfaces_test.py b/dungeonbattle/tests/interfaces_test.py index c82083c..b487eac 100644 --- a/dungeonbattle/tests/interfaces_test.py +++ b/dungeonbattle/tests/interfaces_test.py @@ -9,7 +9,7 @@ class TestInterfaces(unittest.TestCase): """ Create a map and check that it is well parsed. """ - m = Map.load_from_string(".#\n#.\n") + 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#.") diff --git a/resources/example_map.txt b/resources/example_map.txt index 4111fae..4b82063 100644 --- a/resources/example_map.txt +++ b/resources/example_map.txt @@ -1,4 +1,5 @@ - ####### ############# +1 6 + ####### ############# #.....# #...........# #.....# #####...........# #.....# #...............# diff --git a/resources/example_map_2.txt b/resources/example_map_2.txt index bcb4b60..8864f04 100644 --- a/resources/example_map_2.txt +++ b/resources/example_map_2.txt @@ -1,18 +1,19 @@ - ########### ######### - #.........# #.......# - #.........# ############.......# - #.........###############..........#.......############## - #.........#........................#....................# - #.........#.............#..........#.......#............# - ########.########.............#..................#............# - #.........# #.............####.#######.......#............# - #.........# #.............##.........###################### - #.........# #####.##########.........# ########### - #.........# #......# #.........# #.........# - ########.##########......# #.........# #.........# - #...........##......# #.........# #.........# - #...........##......# #.........# #.........# - #...........##......# #.........# ################.###### +1 17 + ########### ######### + #.........# #.......# + #.........# ############.......# + #.........###############..........#.......############## + #.........#........................#....................# + #.........#.............#..........#.......#............# + ########.########.............#..................#............# + #.........# #.............####.#######.......#............# + #.........# #.............##.........###################### + #.........# #####.##########.........# ########### + #.........# #......# #.........# #.........# + ########.##########......# #.........# #.........# + #...........##......# #.........# #.........# + #...........##......# #.........# #.........# + #...........##......# #.........# ################.###### #...........##......# #.........# #.................############ #...........##......# ########.########.......#.........#..........# #...........##......# #...............#.......#.........#..........# @@ -22,19 +23,19 @@ #.........# #...............###################..........# #.........############ #...............# #..........# #.........#..........# #...............# ############ - #....................#####.###########.############# - ########.#########...................# #.............# - #........# #..........#........# #.............######### - #........# ######.##########........# #.............#.......# - #........# #..........# #........# #.....................# - #........# #..........# #........# #.............#.......# - #........# #..........# #........# #.............#.......# - #........# #..........# #........# #.............#.......# - #........# #..........#########.##### #.............#.......# - #........# #..........#.........# ##########.############.####### - #........# #..........#.........# #..............# #..........# - ########## #..........#.........# #..............# #..........# - ############.........# #..............# #..........# - #.........# #..............# #..........# - ########### #..............# #..........# - ################ ############ + #....................#####.###########.############# + ########.#########...................# #.............# + #........# #..........#........# #.............######### + #........# ######.##########........# #.............#.......# + #........# #..........# #........# #.....................# + #........# #..........# #........# #.............#.......# + #........# #..........# #........# #.............#.......# + #........# #..........# #........# #.............#.......# + #........# #..........#########.##### #.............#.......# + #........# #..........#.........# ##########.############.####### + #........# #..........#.........# #..............# #..........# + ########## #..........#.........# #..............# #..........# + ############.........# #..............# #..........# + #.........# #..............# #..........# + ########### #..............# #..........# + ################ ############ From 2b5d82db571e72d3f5498ebba6c345e44181eb69 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 16:23:27 +0100 Subject: [PATCH 26/58] Spawn also items --- dungeonbattle/display/texturepack.py | 4 ++++ dungeonbattle/entities/items.py | 4 ++++ dungeonbattle/entities/player.py | 3 ++- dungeonbattle/game.py | 2 +- dungeonbattle/interfaces.py | 15 ++++++++++----- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/dungeonbattle/display/texturepack.py b/dungeonbattle/display/texturepack.py index 57d8bf6..536bfa1 100644 --- a/dungeonbattle/display/texturepack.py +++ b/dungeonbattle/display/texturepack.py @@ -44,6 +44,8 @@ TexturePack.ASCII_PACK = TexturePack( FLOOR='.', PLAYER='@', HEDGEHOG='*', + HEART='❤', + BOMB='o', ) TexturePack.SQUIRREL_PACK = TexturePack( @@ -58,4 +60,6 @@ TexturePack.SQUIRREL_PACK = TexturePack( FLOOR='██', PLAYER='🐿 ️', HEDGEHOG='🦔', + HEART='💜', + BOMB='💣', ) diff --git a/dungeonbattle/entities/items.py b/dungeonbattle/entities/items.py index 88c6127..676f517 100644 --- a/dungeonbattle/entities/items.py +++ b/dungeonbattle/entities/items.py @@ -16,6 +16,10 @@ class Item(Entity): self.held = True +class Heart(Item): + name = "heart" + + class Bomb(Item): name = "bomb" damage: int = 5 diff --git a/dungeonbattle/entities/player.py b/dungeonbattle/entities/player.py index c656130..2ab3a43 100644 --- a/dungeonbattle/entities/player.py +++ b/dungeonbattle/entities/player.py @@ -36,7 +36,8 @@ class Player(FightingEntity): self.max_xp = self.level * 10 self.health = self.maxhealth # TODO Remove it, that's only fun - self.map.spawn_random_entities(randint(self.level, self.level * 5)) + self.map.spawn_random_entities(randint(3 * self.level, + 10 * self.level)) def add_xp(self, xp: int) -> None: """ diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index a2ea93a..3607c3a 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -52,7 +52,7 @@ class Game: self.player = Player() self.map.add_entity(self.player) self.player.move(1, 14) - self.map.spawn_random_entities(randint(1, 5)) + self.map.spawn_random_entities(randint(3, 10)) @staticmethod def load_game(filename: str) -> None: diff --git a/dungeonbattle/interfaces.py b/dungeonbattle/interfaces.py index a1ab13c..20f8f3d 100644 --- a/dungeonbattle/interfaces.py +++ b/dungeonbattle/interfaces.py @@ -1,7 +1,7 @@ #!/usr/bin/env python from enum import Enum, auto from math import sqrt -from random import randint +from random import choice, randint from typing import List from dungeonbattle.display.texturepack import TexturePack @@ -97,10 +97,9 @@ class Map: tile = self.tiles[y][x] if tile.can_walk(): break - from dungeonbattle.entities.monsters import Hedgehog - hedgehog = Hedgehog() - hedgehog.move(y, x) - self.add_entity(hedgehog) + entity = choice(Entity.get_all_entity_classes())() + entity.move(y, x) + self.add_entity(entity) def tick(self) -> None: """ @@ -193,6 +192,12 @@ class Entity: """ return sqrt(self.distance_squared(other)) + @staticmethod + def get_all_entity_classes(): + from dungeonbattle.entities.items import Heart, Bomb + from dungeonbattle.entities.monsters import Hedgehog + return [Hedgehog, Heart, Bomb] + class FightingEntity(Entity): maxhealth: int From f11fb31c28826eaa9366b348ea435a3b3f6796de Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 16:47:19 +0100 Subject: [PATCH 27/58] Interact with items --- dungeonbattle/entities/items.py | 22 +++++++++++++++++++--- dungeonbattle/entities/player.py | 15 +++++++++------ dungeonbattle/interfaces.py | 7 +++++++ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/dungeonbattle/entities/items.py b/dungeonbattle/entities/items.py index 676f517..4284f42 100644 --- a/dungeonbattle/entities/items.py +++ b/dungeonbattle/entities/items.py @@ -1,8 +1,12 @@ +from typing import Optional + +from .player import Player from ..interfaces import Entity, FightingEntity, Map class Item(Entity): held: bool + hold_by: Optional["Player"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -10,18 +14,30 @@ class Item(Entity): def drop(self, y: int, x: int) -> None: self.held = False + self.hold_by = None + self.map.add_entity(self) self.move(y, x) - def hold(self) -> None: + def hold(self, player: "Player") -> None: self.held = True + self.hold_by = player + self.map.remove_entity(self) class Heart(Item): - name = "heart" + name: str = "heart" + healing: int = 5 + + 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) + return super().hold(player) class Bomb(Item): - name = "bomb" + name: str = "bomb" damage: int = 5 exploding: bool diff --git a/dungeonbattle/entities/player.py b/dungeonbattle/entities/player.py index 2ab3a43..7dcaf2f 100644 --- a/dungeonbattle/entities/player.py +++ b/dungeonbattle/entities/player.py @@ -47,6 +47,7 @@ class Player(FightingEntity): self.current_xp += xp self.level_up() + # noinspection PyTypeChecker,PyUnresolvedReferences def check_move(self, y: int, x: int, move_if_possible: bool = False) \ -> bool: """ @@ -58,12 +59,14 @@ class Player(FightingEntity): if self.dead: return False for entity in self.map.entities: - if entity.y == y and entity.x == x and \ - isinstance(entity, FightingEntity): - self.hit(entity) - if entity.dead: - self.add_xp(randint(3, 7)) - return True + 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: diff --git a/dungeonbattle/interfaces.py b/dungeonbattle/interfaces.py index 20f8f3d..90e1bb7 100644 --- a/dungeonbattle/interfaces.py +++ b/dungeonbattle/interfaces.py @@ -192,6 +192,13 @@ class Entity: """ return sqrt(self.distance_squared(other)) + def is_fighting_entity(self) -> bool: + return isinstance(self, FightingEntity) + + def is_item(self) -> bool: + from dungeonbattle.entities.items import Item + return isinstance(self, Item) + @staticmethod def get_all_entity_classes(): from dungeonbattle.entities.items import Heart, Bomb From d35331fdb04ecfe56dbeaf76b3ec9a69fca2b2e5 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 16:49:04 +0100 Subject: [PATCH 28/58] Add items in inventory --- dungeonbattle/entities/items.py | 4 ++++ dungeonbattle/entities/player.py | 1 + 2 files changed, 5 insertions(+) diff --git a/dungeonbattle/entities/items.py b/dungeonbattle/entities/items.py index 4284f42..1443e2d 100644 --- a/dungeonbattle/entities/items.py +++ b/dungeonbattle/entities/items.py @@ -7,6 +7,7 @@ from ..interfaces import Entity, FightingEntity, Map class Item(Entity): held: bool hold_by: Optional["Player"] + can_be_in_inventory: bool = False def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -22,10 +23,13 @@ class Item(Entity): self.held = True self.hold_by = player self.map.remove_entity(self) + if self.can_be_in_inventory: + player.inventory.append(self) class Heart(Item): name: str = "heart" + can_be_in_inventory: bool = False healing: int = 5 def hold(self, player: "Player") -> None: diff --git a/dungeonbattle/entities/player.py b/dungeonbattle/entities/player.py index 7dcaf2f..663a774 100644 --- a/dungeonbattle/entities/player.py +++ b/dungeonbattle/entities/player.py @@ -15,6 +15,7 @@ class Player(FightingEntity): level: int = 1 current_xp: int = 0 max_xp: int = 10 + inventory: list = list() paths: Dict[Tuple[int, int], Tuple[int, int]] def move(self, y: int, x: int) -> None: From ac22aef8606bbc957f0d3e97b4d72774f0ccd88f Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 16:57:09 +0100 Subject: [PATCH 29/58] Items can be put in inventory by default --- dungeonbattle/entities/items.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/dungeonbattle/entities/items.py b/dungeonbattle/entities/items.py index 1443e2d..989b52f 100644 --- a/dungeonbattle/entities/items.py +++ b/dungeonbattle/entities/items.py @@ -6,8 +6,10 @@ from ..interfaces import Entity, FightingEntity, Map class Item(Entity): held: bool - hold_by: Optional["Player"] - can_be_in_inventory: bool = False + held_by: Optional["Player"] + # When it is False, items disappear when they are hold. + # Action is done when the item is picked up. + can_be_in_inventory: bool = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -15,13 +17,13 @@ class Item(Entity): def drop(self, y: int, x: int) -> None: self.held = False - self.hold_by = None + self.held_by = None self.map.add_entity(self) self.move(y, x) def hold(self, player: "Player") -> None: self.held = True - self.hold_by = player + self.held_by = player self.map.remove_entity(self) if self.can_be_in_inventory: player.inventory.append(self) From 8db00bcaa69750bc8a3dffdf72fe1606ef4cac0f Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 16:58:20 +0100 Subject: [PATCH 30/58] Fix broken tests --- dungeonbattle/game.py | 4 +--- dungeonbattle/tests/entities_test.py | 23 +++++++++++++++++------ dungeonbattle/tests/interfaces_test.py | 2 +- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index 3607c3a..b28032f 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -47,11 +47,9 @@ class Game: """ # TODO generate a new map procedurally self.map = Map.load("resources/example_map_2.txt") - self.map.currenty = 1 - self.map.currentx = 6 self.player = Player() self.map.add_entity(self.player) - self.player.move(1, 14) + self.player.move(self.map.start_y, self.map.start_x) self.map.spawn_random_entities(randint(3, 10)) @staticmethod diff --git a/dungeonbattle/tests/entities_test.py b/dungeonbattle/tests/entities_test.py index 3d61812..7a81d63 100644 --- a/dungeonbattle/tests/entities_test.py +++ b/dungeonbattle/tests/entities_test.py @@ -12,6 +12,9 @@ class TestEntities(unittest.TestCase): Load example map that can be used in tests. """ self.map = Map.load("resources/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: """ @@ -48,21 +51,29 @@ class TestEntities(unittest.TestCase): entity = Hedgehog() self.map.add_entity(entity) - entity.move(2, 6) + entity.move(3, 6) self.map.tick() - self.assertFalse(entity.y == 2 and entity.x == 6) + self.assertTrue(entity.y == 2 and entity.x == 6) def test_items(self) -> None: """ Test some random stuff with items. """ item = Item() + self.map.add_entity(item) self.assertFalse(item.held) - item.hold() + item.hold(self.player) self.assertTrue(item.held) - item.drop(42, 42) - self.assertEqual(item.y, 42) - self.assertEqual(item.x, 42) + 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: """ diff --git a/dungeonbattle/tests/interfaces_test.py b/dungeonbattle/tests/interfaces_test.py index b487eac..66c06c7 100644 --- a/dungeonbattle/tests/interfaces_test.py +++ b/dungeonbattle/tests/interfaces_test.py @@ -19,7 +19,7 @@ class TestInterfaces(unittest.TestCase): Try to load a map from a file. """ m = Map.load("resources/example_map.txt") - self.assertEqual(m.width, 52) + self.assertEqual(m.width, 44) self.assertEqual(m.height, 17) def test_tiles(self) -> None: From 61714a51293b741f2c81ed9b34844107c257d70a Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 17:15:28 +0100 Subject: [PATCH 31/58] Test heart healing --- dungeonbattle/entities/items.py | 9 ++------- dungeonbattle/tests/entities_test.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/dungeonbattle/entities/items.py b/dungeonbattle/entities/items.py index 989b52f..e4eb74d 100644 --- a/dungeonbattle/entities/items.py +++ b/dungeonbattle/entities/items.py @@ -7,9 +7,6 @@ from ..interfaces import Entity, FightingEntity, Map class Item(Entity): held: bool held_by: Optional["Player"] - # When it is False, items disappear when they are hold. - # Action is done when the item is picked up. - can_be_in_inventory: bool = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -25,13 +22,11 @@ class Item(Entity): self.held = True self.held_by = player self.map.remove_entity(self) - if self.can_be_in_inventory: - player.inventory.append(self) + player.inventory.append(self) class Heart(Item): name: str = "heart" - can_be_in_inventory: bool = False healing: int = 5 def hold(self, player: "Player") -> None: @@ -39,7 +34,7 @@ class Heart(Item): When holding a heart, heal the player and don't put item in inventory. """ player.health = min(player.maxhealth, player.health + self.healing) - return super().hold(player) + self.map.remove_entity(self) class Bomb(Item): diff --git a/dungeonbattle/tests/entities_test.py b/dungeonbattle/tests/entities_test.py index 7a81d63..015d6be 100644 --- a/dungeonbattle/tests/entities_test.py +++ b/dungeonbattle/tests/entities_test.py @@ -1,6 +1,6 @@ import unittest -from dungeonbattle.entities.items import Bomb, Item +from dungeonbattle.entities.items import Bomb, Heart, Item from dungeonbattle.entities.monsters import Hedgehog from dungeonbattle.entities.player import Player from dungeonbattle.interfaces import Entity, Map @@ -93,6 +93,19 @@ class TestEntities(unittest.TestCase): item.act(self.map) self.assertTrue(hedgehog.dead) + 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) + def test_players(self) -> None: """ Test some random stuff with players. From e88b4ee775fc002e7771d0c6fbc3f4ca8e314441 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 17:17:28 +0100 Subject: [PATCH 32/58] Render game when we are dead --- dungeonbattle/tests/game_test.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/dungeonbattle/tests/game_test.py b/dungeonbattle/tests/game_test.py index c83e851..84d7cf9 100644 --- a/dungeonbattle/tests/game_test.py +++ b/dungeonbattle/tests/game_test.py @@ -122,3 +122,20 @@ class TestGame(unittest.TestCase): self.game.handle_key_pressed(KeyValues.SPACE) self.assertEqual(self.game.state, GameMode.MAINMENU) + + def test_dead_screen(self): + """ + 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) + From af39a305b14be2cfec0060e42fd9c60bdea630a2 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 17:23:31 +0100 Subject: [PATCH 33/58] Test entity movements and fights with players --- dungeonbattle/tests/entities_test.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/dungeonbattle/tests/entities_test.py b/dungeonbattle/tests/entities_test.py index 015d6be..d2c8171 100644 --- a/dungeonbattle/tests/entities_test.py +++ b/dungeonbattle/tests/entities_test.py @@ -51,10 +51,33 @@ class TestEntities(unittest.TestCase): entity = Hedgehog() 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) + # Hedgehog 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 hedgehog + 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. From a4eaab0db6df14889571240e62da04425fa48a9a Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 17:24:57 +0100 Subject: [PATCH 34/58] Linting --- dungeonbattle/tests/game_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dungeonbattle/tests/game_test.py b/dungeonbattle/tests/game_test.py index 84d7cf9..5bf97e9 100644 --- a/dungeonbattle/tests/game_test.py +++ b/dungeonbattle/tests/game_test.py @@ -123,7 +123,7 @@ class TestGame(unittest.TestCase): self.game.handle_key_pressed(KeyValues.SPACE) self.assertEqual(self.game.state, GameMode.MAINMENU) - def test_dead_screen(self): + def test_dead_screen(self) -> None: """ Kill player and render dead screen. """ @@ -138,4 +138,3 @@ class TestGame(unittest.TestCase): new_y, new_x = self.game.player.y, self.game.player.x self.assertEqual(new_y, y) self.assertEqual(new_x, x) - From c329ec927f81d50a34381531e138d78ce784c32f Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 17:39:48 +0100 Subject: [PATCH 35/58] Add rabbits, beavers and teddy bears --- dungeonbattle/display/texturepack.py | 6 ++++++ dungeonbattle/entities/monsters.py | 18 ++++++++++++++++++ dungeonbattle/interfaces.py | 5 +++-- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/dungeonbattle/display/texturepack.py b/dungeonbattle/display/texturepack.py index 536bfa1..fc17ead 100644 --- a/dungeonbattle/display/texturepack.py +++ b/dungeonbattle/display/texturepack.py @@ -46,6 +46,9 @@ TexturePack.ASCII_PACK = TexturePack( HEDGEHOG='*', HEART='❤', BOMB='o', + RABBIT='Y', + BEAVER='_', + TEDDY_BEAR='8', ) TexturePack.SQUIRREL_PACK = TexturePack( @@ -62,4 +65,7 @@ TexturePack.SQUIRREL_PACK = TexturePack( HEDGEHOG='🦔', HEART='💜', BOMB='💣', + RABBIT='🐇', + BEAVER='🦫', + TEDDY_BEAR='🧸', ) diff --git a/dungeonbattle/entities/monsters.py b/dungeonbattle/entities/monsters.py index 161a3b1..9c23e2f 100644 --- a/dungeonbattle/entities/monsters.py +++ b/dungeonbattle/entities/monsters.py @@ -34,7 +34,25 @@ class Monster(FightingEntity): break +class Beaver(Monster): + name = "beaver" + maxhealth = 30 + strength = 2 + + class Hedgehog(Monster): name = "hedgehog" maxhealth = 10 strength = 3 + + +class Rabbit(Monster): + name = "rabbit" + maxhealth = 15 + strength = 1 + + +class TeddyBear(Monster): + name = "teddy_bear" + maxhealth = 500 + strength = 0 diff --git a/dungeonbattle/interfaces.py b/dungeonbattle/interfaces.py index 90e1bb7..b057400 100644 --- a/dungeonbattle/interfaces.py +++ b/dungeonbattle/interfaces.py @@ -202,8 +202,9 @@ class Entity: @staticmethod def get_all_entity_classes(): from dungeonbattle.entities.items import Heart, Bomb - from dungeonbattle.entities.monsters import Hedgehog - return [Hedgehog, Heart, Bomb] + from dungeonbattle.entities.monsters import Beaver, Hedgehog, \ + Rabbit, TeddyBear + return [Beaver, Bomb, Heart, Hedgehog, Rabbit, TeddyBear] class FightingEntity(Entity): From 53077aacb01c274e4550ed88a32bf397f8713f91 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 20:36:43 +0100 Subject: [PATCH 36/58] Linting --- dungeonbattle/display/display_manager.py | 2 +- dungeonbattle/display/menudisplay.py | 4 +++- dungeonbattle/game.py | 6 +++--- dungeonbattle/menus.py | 9 ++++++--- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/dungeonbattle/display/display_manager.py b/dungeonbattle/display/display_manager.py index ea5c8ca..18b61c3 100644 --- a/dungeonbattle/display/display_manager.py +++ b/dungeonbattle/display/display_manager.py @@ -37,7 +37,7 @@ class DisplayManager: 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.settingsmenudisplay.refresh(0, 0, self.rows, self.cols - 1) self.resize_window() def resize_window(self) -> bool: diff --git a/dungeonbattle/display/menudisplay.py b/dungeonbattle/display/menudisplay.py index d25fe61..973dd31 100644 --- a/dungeonbattle/display/menudisplay.py +++ b/dungeonbattle/display/menudisplay.py @@ -1,3 +1,5 @@ +from typing import List + from dungeonbattle.menus import Menu, MainMenu from .display import Display @@ -54,7 +56,7 @@ class MenuDisplay(Display): return self.trueheight + 2 @property - def values(self): + def values(self) -> List[str]: return [str(a) for a in self.menu.values] diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index 4bbe9b8..ff1c91e 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -93,7 +93,7 @@ class Game: elif key == ' ': return KeyValues.SPACE - def handle_key_pressed(self, key: KeyValues, screen) -> None: + def handle_key_pressed(self, key: KeyValues, screen: Any) -> None: """ Indicates what should be done when the given key is pressed, according to the current game state. @@ -103,7 +103,7 @@ class Game: elif self.state == GameMode.MAINMENU: self.handle_key_pressed_main_menu(key) elif self.state == GameMode.SETTINGS: - self.handle_key_pressed_settings(key,screen) + self.handle_key_pressed_settings(key, screen) self.display_refresh() def handle_key_pressed_play(self, key: KeyValues) -> None: @@ -142,7 +142,7 @@ class Game: elif option == menus.MainMenuValues.EXIT: sys.exit(0) - def handle_key_pressed_settings(self, key: KeyValues, screen : Any) -> None: + def handle_key_pressed_settings(self, key: KeyValues, screen: Any) -> None: """ For now, in the settings mode, we can only go backwards. """ diff --git a/dungeonbattle/menus.py b/dungeonbattle/menus.py index 06ef556..20a103e 100644 --- a/dungeonbattle/menus.py +++ b/dungeonbattle/menus.py @@ -31,12 +31,15 @@ class MainMenuValues(Enum): class MainMenu(Menu): values = [e for e in MainMenuValues] -class SettingsMenu(Menu) : + +class SettingsMenu(Menu): def __init__(self): super().__init__() - def update_values(self, settings : Settings): + + def update_values(self, settings: Settings) -> None: s = settings.dumps_to_string() - self.values = s[6:-2].replace("\"","").split(",\n ") + self.values = s[6:-2].replace("\"", "").split(",\n ") + class ArbitraryMenu(Menu): def __init__(self, values: list): From 42d8caefdefcd63522a9a570c830725abbc9268a Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 20:51:50 +0100 Subject: [PATCH 37/58] Pycharm, please don't remove trailing white spaces on maps --- resources/example_map.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/example_map.txt b/resources/example_map.txt index 4b82063..5aaade9 100644 --- a/resources/example_map.txt +++ b/resources/example_map.txt @@ -1,5 +1,5 @@ 1 6 - ####### ############# + ####### ############# #.....# #...........# #.....# #####...........# #.....# #...............# From 4b8acc0597be1ba6498d8ad5a706afc87987cdd4 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 21:55:57 +0100 Subject: [PATCH 38/58] Don't resize pads when resizing window. For an unknown reason, pads don't want to be displayed on small screens. --- dungeonbattle/display/display.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dungeonbattle/display/display.py b/dungeonbattle/display/display.py index 1a2e0ca..c803aca 100644 --- a/dungeonbattle/display/display.py +++ b/dungeonbattle/display/display.py @@ -31,8 +31,6 @@ class Display: self.y = y self.width = width self.height = height - if self.pad: - self.pad.resize(height - 1, width - 1) def refresh(self, *args) -> None: if len(args) == 4: From 17edb6a6459285efa6223106b4a3fe6bb00389ea Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 22:12:05 +0100 Subject: [PATCH 39/58] Don't resize map pad: it has already the good size (the map dimensions) --- dungeonbattle/display/display.py | 9 ++++++--- dungeonbattle/display/display_manager.py | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/dungeonbattle/display/display.py b/dungeonbattle/display/display.py index c803aca..6aba26a 100644 --- a/dungeonbattle/display/display.py +++ b/dungeonbattle/display/display.py @@ -26,15 +26,18 @@ class Display: 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) -> None: + 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) -> None: + def refresh(self, *args, resize_pad: bool = True) -> None: if len(args) == 4: - self.resize(*args) + self.resize(*args, resize_pad) self.display() def display(self) -> None: diff --git a/dungeonbattle/display/display_manager.py b/dungeonbattle/display/display_manager.py index 18b61c3..c5456bc 100644 --- a/dungeonbattle/display/display_manager.py +++ b/dungeonbattle/display/display_manager.py @@ -31,7 +31,9 @@ class DisplayManager: def refresh(self) -> None: if self.game.state == GameMode.PLAY: - self.mapdisplay.refresh(0, 0, self.rows * 4 // 5, self.cols) + # 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: From 748561e87df2afc1912cf810617443d2c5a6ed6c Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 22:22:33 +0100 Subject: [PATCH 40/58] More separation on menu code --- dungeonbattle/enums.py | 41 +++++++++++++++++++++ dungeonbattle/game.py | 83 +++--------------------------------------- dungeonbattle/menus.py | 38 +++++++++++++++++++ 3 files changed, 84 insertions(+), 78 deletions(-) create mode 100644 dungeonbattle/enums.py diff --git a/dungeonbattle/enums.py b/dungeonbattle/enums.py new file mode 100644 index 0000000..2304e7e --- /dev/null +++ b/dungeonbattle/enums.py @@ -0,0 +1,41 @@ +from enum import Enum, auto + +from dungeonbattle.settings import Settings + + +class GameMode(Enum): + MAINMENU = auto() + PLAY = auto() + SETTINGS = auto() + INVENTORY = auto() + + +class KeyValues(Enum): + UP = auto() + DOWN = auto() + LEFT = auto() + RIGHT = auto() + ENTER = auto() + SPACE = auto() + + @staticmethod + def translate_key(key: str, settings: Settings) -> "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 diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index ff1c91e..3869b0b 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -1,31 +1,14 @@ -import sys from random import randint from typing import Any from .entities.player import Player +from .enums import GameMode, KeyValues from .interfaces import Map from .settings import Settings -from enum import Enum, auto from . import menus from typing import Callable -class GameMode(Enum): - MAINMENU = auto() - PLAY = auto() - SETTINGS = auto() - INVENTORY = auto() - - -class KeyValues(Enum): - UP = auto() - DOWN = auto() - LEFT = auto() - RIGHT = auto() - ENTER = auto() - SPACE = auto() - - class Game: map: Map player: Player @@ -70,30 +53,9 @@ class Game: screen.refresh() self.display_refresh() key = screen.getkey() - self.handle_key_pressed(self.translate_key(key), screen) + self.handle_key_pressed(KeyValues.translate_key(key, self.settings)) - def translate_key(self, key: str) -> KeyValues: - """ - Translate the raw string key into an enum value that we can use. - """ - if key in (self.settings.KEY_DOWN_SECONDARY, - self.settings.KEY_DOWN_PRIMARY): - return KeyValues.DOWN - elif key in (self.settings.KEY_LEFT_PRIMARY, - self.settings.KEY_LEFT_SECONDARY): - return KeyValues.LEFT - elif key in (self.settings.KEY_RIGHT_PRIMARY, - self.settings.KEY_RIGHT_SECONDARY): - return KeyValues.RIGHT - elif key in (self.settings.KEY_UP_PRIMARY, - self.settings.KEY_UP_SECONDARY): - return KeyValues.UP - elif key == self.settings.KEY_ENTER: - return KeyValues.ENTER - elif key == ' ': - return KeyValues.SPACE - - def handle_key_pressed(self, key: KeyValues, screen: Any) -> None: + def handle_key_pressed(self, key: KeyValues) -> None: """ Indicates what should be done when the given key is pressed, according to the current game state. @@ -101,9 +63,9 @@ class Game: if self.state == GameMode.PLAY: self.handle_key_pressed_play(key) elif self.state == GameMode.MAINMENU: - self.handle_key_pressed_main_menu(key) + self.main_menu.handle_key_pressed(key, self) elif self.state == GameMode.SETTINGS: - self.handle_key_pressed_settings(key, screen) + self.settings_menu.handle_key_pressed(key, self.settings) self.display_refresh() def handle_key_pressed_play(self, key: KeyValues) -> None: @@ -124,38 +86,3 @@ class Game: 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.state = GameMode.PLAY - elif option == menus.MainMenuValues.SETTINGS: - self.state = GameMode.SETTINGS - elif option == menus.MainMenuValues.EXIT: - sys.exit(0) - - def handle_key_pressed_settings(self, key: KeyValues, screen: Any) -> None: - """ - For now, in the settings mode, we can only go backwards. - """ - if key == KeyValues.SPACE: - self.state = GameMode.MAINMENU - if key == KeyValues.DOWN: - self.settings_menu.go_down() - if key == KeyValues.UP: - self.settings_menu.go_up() - if key == KeyValues.ENTER: - option = self.settings_menu.validate().split(": ")[0] - if option != "TEXTURE_PACK": - newkey = screen.getkey() - self.settings.__setattr__(option, newkey) - self.settings.write_settings() - self.settings_menu.update_values(self.settings) diff --git a/dungeonbattle/menus.py b/dungeonbattle/menus.py index 20a103e..00e8cf4 100644 --- a/dungeonbattle/menus.py +++ b/dungeonbattle/menus.py @@ -1,5 +1,8 @@ +import sys from enum import Enum from typing import Any + +from .enums import GameMode, KeyValues from .settings import Settings @@ -31,6 +34,23 @@ class MainMenuValues(Enum): class MainMenu(Menu): values = [e for e in MainMenuValues] + def handle_key_pressed(self, key: KeyValues, game: Any) -> None: + """ + In the main menu, we can navigate through options. + """ + if key == KeyValues.DOWN: + self.go_down() + if key == KeyValues.UP: + self.go_up() + if key == KeyValues.ENTER: + option = self.validate() + if option == MainMenuValues.START: + game.state = GameMode.PLAY + elif option == MainMenuValues.SETTINGS: + game.state = GameMode.SETTINGS + elif option == MainMenuValues.EXIT: + sys.exit(0) + class SettingsMenu(Menu): def __init__(self): @@ -40,6 +60,24 @@ class SettingsMenu(Menu): s = settings.dumps_to_string() self.values = s[6:-2].replace("\"", "").split(",\n ") + def handle_key_pressed(self, key: KeyValues, settings: Settings) -> None: + """ + For now, in the settings mode, we can only go backwards. + """ + if key == KeyValues.SPACE: + self.state = GameMode.MAINMENU + if key == KeyValues.DOWN: + self.go_down() + if key == KeyValues.UP: + self.go_up() + if key == KeyValues.ENTER: + option = self.validate().split(": ")[0] + if option != "TEXTURE_PACK": + newkey = screen.getkey() + self.settings.__setattr__(option, newkey) + self.settings.write_settings() + self.settings_menu.update_values(self.settings) + class ArbitraryMenu(Menu): def __init__(self, values: list): From 0c17e74d6aea974fdf1e60c75c913dab96c85c80 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 22:36:42 +0100 Subject: [PATCH 41/58] Key handler does not depend on curses's screen anymore --- dungeonbattle/enums.py | 17 +++++---- dungeonbattle/game.py | 7 ++-- dungeonbattle/menus.py | 34 +++++++++-------- dungeonbattle/tests/game_test.py | 53 +++++++++++++++++--------- dungeonbattle/tests/interfaces_test.py | 2 +- 5 files changed, 68 insertions(+), 45 deletions(-) diff --git a/dungeonbattle/enums.py b/dungeonbattle/enums.py index 2304e7e..aa215d3 100644 --- a/dungeonbattle/enums.py +++ b/dungeonbattle/enums.py @@ -1,4 +1,5 @@ from enum import Enum, auto +from typing import Optional, Tuple from dungeonbattle.settings import Settings @@ -19,23 +20,25 @@ class KeyValues(Enum): SPACE = auto() @staticmethod - def translate_key(key: str, settings: Settings) -> "KeyValues": + def translate_key(key: str, settings: Settings) \ + -> Tuple[Optional["KeyValues"], str]: """ 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 + return KeyValues.DOWN, key elif key in (settings.KEY_LEFT_PRIMARY, settings.KEY_LEFT_SECONDARY): - return KeyValues.LEFT + return KeyValues.LEFT, key elif key in (settings.KEY_RIGHT_PRIMARY, settings.KEY_RIGHT_SECONDARY): - return KeyValues.RIGHT + return KeyValues.RIGHT, key elif key in (settings.KEY_UP_PRIMARY, settings.KEY_UP_SECONDARY): - return KeyValues.UP + return KeyValues.UP, key elif key == settings.KEY_ENTER: - return KeyValues.ENTER + return KeyValues.ENTER, key elif key == ' ': - return KeyValues.SPACE + return KeyValues.SPACE, key + return None, key diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index 3869b0b..138c161 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -53,9 +53,10 @@ class Game: screen.refresh() self.display_refresh() key = screen.getkey() - self.handle_key_pressed(KeyValues.translate_key(key, self.settings)) + self.handle_key_pressed( + *KeyValues.translate_key(key, self.settings)) - def handle_key_pressed(self, key: KeyValues) -> None: + def handle_key_pressed(self, key: KeyValues, raw_key: str = '') -> None: """ Indicates what should be done when the given key is pressed, according to the current game state. @@ -65,7 +66,7 @@ class Game: elif self.state == GameMode.MAINMENU: self.main_menu.handle_key_pressed(key, self) elif self.state == GameMode.SETTINGS: - self.settings_menu.handle_key_pressed(key, self.settings) + self.settings_menu.handle_key_pressed(key, raw_key, self) self.display_refresh() def handle_key_pressed_play(self, key: KeyValues) -> None: diff --git a/dungeonbattle/menus.py b/dungeonbattle/menus.py index 00e8cf4..fb22aef 100644 --- a/dungeonbattle/menus.py +++ b/dungeonbattle/menus.py @@ -53,30 +53,34 @@ class MainMenu(Menu): class SettingsMenu(Menu): - def __init__(self): - super().__init__() + waiting_for_key: bool = False def update_values(self, settings: Settings) -> None: s = settings.dumps_to_string() self.values = s[6:-2].replace("\"", "").split(",\n ") - def handle_key_pressed(self, key: KeyValues, settings: Settings) -> None: + def handle_key_pressed(self, key: KeyValues, raw_key: str, game: Any) \ + -> None: """ For now, in the settings mode, we can only go backwards. """ - if key == KeyValues.SPACE: - self.state = GameMode.MAINMENU - if key == KeyValues.DOWN: - self.go_down() - if key == KeyValues.UP: - self.go_up() - if key == KeyValues.ENTER: + if not self.waiting_for_key: + if key == KeyValues.SPACE: + game.state = GameMode.MAINMENU + if key == KeyValues.DOWN: + self.go_down() + if key == KeyValues.UP: + self.go_up() + if key == KeyValues.ENTER: + option = self.validate().split(": ")[0] + if option != "TEXTURE_PACK": + self.waiting_for_key = True + else: option = self.validate().split(": ")[0] - if option != "TEXTURE_PACK": - newkey = screen.getkey() - self.settings.__setattr__(option, newkey) - self.settings.write_settings() - self.settings_menu.update_values(self.settings) + setattr(game.settings, option, raw_key) + game.settings.write_settings() + self.update_values(game.settings) + self.waiting_for_key = False class ArbitraryMenu(Menu): diff --git a/dungeonbattle/tests/game_test.py b/dungeonbattle/tests/game_test.py index 5bf97e9..ec6ffd6 100644 --- a/dungeonbattle/tests/game_test.py +++ b/dungeonbattle/tests/game_test.py @@ -7,6 +7,7 @@ from dungeonbattle.display.display_manager import DisplayManager from dungeonbattle.entities.player import Player from dungeonbattle.game import Game, KeyValues, GameMode from dungeonbattle.menus import MainMenuValues +from dungeonbattle.settings import Settings class TestGame(unittest.TestCase): @@ -36,25 +37,39 @@ class TestGame(unittest.TestCase): """ Test key bindings. """ - self.assertEqual(self.game.translate_key( - self.game.settings.KEY_UP_PRIMARY), KeyValues.UP) - self.assertEqual(self.game.translate_key( - self.game.settings.KEY_UP_SECONDARY), KeyValues.UP) - self.assertEqual(self.game.translate_key( - self.game.settings.KEY_DOWN_PRIMARY), KeyValues.DOWN) - self.assertEqual(self.game.translate_key( - self.game.settings.KEY_DOWN_SECONDARY), KeyValues.DOWN) - self.assertEqual(self.game.translate_key( - self.game.settings.KEY_LEFT_PRIMARY), KeyValues.LEFT) - self.assertEqual(self.game.translate_key( - self.game.settings.KEY_LEFT_SECONDARY), KeyValues.LEFT) - self.assertEqual(self.game.translate_key( - self.game.settings.KEY_RIGHT_PRIMARY), KeyValues.RIGHT) - self.assertEqual(self.game.translate_key( - self.game.settings.KEY_RIGHT_SECONDARY), KeyValues.RIGHT) - self.assertEqual(self.game.translate_key( - self.game.settings.KEY_ENTER), KeyValues.ENTER) - self.assertEqual(self.game.translate_key(' '), KeyValues.SPACE) + self.game.settings = Settings() + + self.assertEqual(KeyValues.translate_key( + self.game.settings.KEY_UP_PRIMARY, self.game.settings)[0], + KeyValues.UP) + self.assertEqual(KeyValues.translate_key( + self.game.settings.KEY_UP_SECONDARY, self.game.settings)[0], + KeyValues.UP) + self.assertEqual(KeyValues.translate_key( + self.game.settings.KEY_DOWN_PRIMARY, self.game.settings)[0], + KeyValues.DOWN) + self.assertEqual(KeyValues.translate_key( + self.game.settings.KEY_DOWN_SECONDARY, self.game.settings)[0], + KeyValues.DOWN) + self.assertEqual(KeyValues.translate_key( + self.game.settings.KEY_LEFT_PRIMARY, self.game.settings)[0], + KeyValues.LEFT) + self.assertEqual(KeyValues.translate_key( + self.game.settings.KEY_LEFT_SECONDARY, self.game.settings)[0], + KeyValues.LEFT) + self.assertEqual(KeyValues.translate_key( + self.game.settings.KEY_RIGHT_PRIMARY, self.game.settings)[0], + KeyValues.RIGHT) + self.assertEqual(KeyValues.translate_key( + self.game.settings.KEY_RIGHT_SECONDARY, self.game.settings)[0], + KeyValues.RIGHT) + self.assertEqual(KeyValues.translate_key( + self.game.settings.KEY_ENTER, self.game.settings)[0], + KeyValues.ENTER) + self.assertEqual(KeyValues.translate_key(' ', self.game.settings)[0], + KeyValues.SPACE) + self.assertEqual(KeyValues.translate_key('plop', self.game.settings)[0], + None) def test_key_press(self) -> None: """ diff --git a/dungeonbattle/tests/interfaces_test.py b/dungeonbattle/tests/interfaces_test.py index 66c06c7..b487eac 100644 --- a/dungeonbattle/tests/interfaces_test.py +++ b/dungeonbattle/tests/interfaces_test.py @@ -19,7 +19,7 @@ class TestInterfaces(unittest.TestCase): Try to load a map from a file. """ m = Map.load("resources/example_map.txt") - self.assertEqual(m.width, 44) + self.assertEqual(m.width, 52) self.assertEqual(m.height, 17) def test_tiles(self) -> None: From caef8dc9b21bfed65b83a8a8f6da1f32712823df Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 22:45:15 +0100 Subject: [PATCH 42/58] Cover settings --- dungeonbattle/game.py | 5 +++-- dungeonbattle/menus.py | 6 +++--- dungeonbattle/tests/game_test.py | 34 ++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index 138c161..14e3206 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -1,5 +1,5 @@ from random import randint -from typing import Any +from typing import Any, Optional from .entities.player import Player from .enums import GameMode, KeyValues @@ -56,7 +56,8 @@ class Game: self.handle_key_pressed( *KeyValues.translate_key(key, self.settings)) - def handle_key_pressed(self, key: KeyValues, raw_key: str = '') -> None: + 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. diff --git a/dungeonbattle/menus.py b/dungeonbattle/menus.py index fb22aef..3713900 100644 --- a/dungeonbattle/menus.py +++ b/dungeonbattle/menus.py @@ -1,6 +1,6 @@ import sys from enum import Enum -from typing import Any +from typing import Any, Optional from .enums import GameMode, KeyValues from .settings import Settings @@ -59,8 +59,8 @@ class SettingsMenu(Menu): s = settings.dumps_to_string() self.values = s[6:-2].replace("\"", "").split(",\n ") - def handle_key_pressed(self, key: KeyValues, raw_key: str, game: Any) \ - -> None: + def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str, + game: Any) -> None: """ For now, in the settings mode, we can only go backwards. """ diff --git a/dungeonbattle/tests/game_test.py b/dungeonbattle/tests/game_test.py index ec6ffd6..0894060 100644 --- a/dungeonbattle/tests/game_test.py +++ b/dungeonbattle/tests/game_test.py @@ -138,6 +138,40 @@ class TestGame(unittest.TestCase): self.game.handle_key_pressed(KeyValues.SPACE) self.assertEqual(self.game.state, GameMode.MAINMENU) + 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.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) + 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') + def test_dead_screen(self) -> None: """ Kill player and render dead screen. From edecb7eb905d4ad01ac32e6c9ac1b6ec965c659b Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 22:45:57 +0100 Subject: [PATCH 43/58] I felt frustrated to don't have 100% of coverage --- dungeonbattle/game.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index 14e3206..6c7b347 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -48,7 +48,7 @@ class Game: We wait for a player action, then we do what that should be done when the given key got pressed. """ - while True: + while True: # pragma no cover screen.clear() screen.refresh() self.display_refresh() From cde0b19c72930eb32fa30d14eb41b37df29c834a Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 22:47:51 +0100 Subject: [PATCH 44/58] KeyValues.translate_key returns a KeyValues object --- dungeonbattle/enums.py | 17 ++++++++--------- dungeonbattle/game.py | 2 +- dungeonbattle/tests/game_test.py | 22 +++++++++++----------- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/dungeonbattle/enums.py b/dungeonbattle/enums.py index aa215d3..e6fc955 100644 --- a/dungeonbattle/enums.py +++ b/dungeonbattle/enums.py @@ -20,25 +20,24 @@ class KeyValues(Enum): SPACE = auto() @staticmethod - def translate_key(key: str, settings: Settings) \ - -> Tuple[Optional["KeyValues"], str]: + 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, key + return KeyValues.DOWN elif key in (settings.KEY_LEFT_PRIMARY, settings.KEY_LEFT_SECONDARY): - return KeyValues.LEFT, key + return KeyValues.LEFT elif key in (settings.KEY_RIGHT_PRIMARY, settings.KEY_RIGHT_SECONDARY): - return KeyValues.RIGHT, key + return KeyValues.RIGHT elif key in (settings.KEY_UP_PRIMARY, settings.KEY_UP_SECONDARY): - return KeyValues.UP, key + return KeyValues.UP elif key == settings.KEY_ENTER: - return KeyValues.ENTER, key + return KeyValues.ENTER elif key == ' ': - return KeyValues.SPACE, key - return None, key + return KeyValues.SPACE + return None diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index 6c7b347..3f75960 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -54,7 +54,7 @@ class Game: self.display_refresh() key = screen.getkey() self.handle_key_pressed( - *KeyValues.translate_key(key, self.settings)) + KeyValues.translate_key(key, self.settings), key) def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str = '')\ -> None: diff --git a/dungeonbattle/tests/game_test.py b/dungeonbattle/tests/game_test.py index 0894060..2c327dc 100644 --- a/dungeonbattle/tests/game_test.py +++ b/dungeonbattle/tests/game_test.py @@ -40,35 +40,35 @@ class TestGame(unittest.TestCase): self.game.settings = Settings() self.assertEqual(KeyValues.translate_key( - self.game.settings.KEY_UP_PRIMARY, self.game.settings)[0], + 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)[0], + 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)[0], + 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)[0], + 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)[0], + 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)[0], + 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)[0], + 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)[0], + self.game.settings.KEY_RIGHT_SECONDARY, self.game.settings), KeyValues.RIGHT) self.assertEqual(KeyValues.translate_key( - self.game.settings.KEY_ENTER, self.game.settings)[0], + self.game.settings.KEY_ENTER, self.game.settings), KeyValues.ENTER) - self.assertEqual(KeyValues.translate_key(' ', self.game.settings)[0], + self.assertEqual(KeyValues.translate_key(' ', self.game.settings), KeyValues.SPACE) - self.assertEqual(KeyValues.translate_key('plop', self.game.settings)[0], + self.assertEqual(KeyValues.translate_key('plop', self.game.settings), None) def test_key_press(self) -> None: From c243176d8fd19f2a005a9a0793b155b4b97cbf53 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 22:50:14 +0100 Subject: [PATCH 45/58] Add Gitlab stage for Python 3.7 because it is Debian default version --- .gitlab-ci.yml | 11 +++++++++-- tox.ini | 3 +-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d08a30b..5ca51af 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,12 +2,19 @@ stages: - test - quality-assurance +py37: + stage: test + image: python:3.7-alpine + before_script: + - pip install tox + script: tox -e py3 + py38: stage: test image: python:3.8-alpine before_script: - pip install tox - script: tox -e py38 + script: tox -e py3 py39: @@ -15,7 +22,7 @@ py39: image: python:3.9-alpine before_script: - pip install tox - script: tox -e py39 + script: tox -e py3 linters: stage: quality-assurance diff --git a/tox.ini b/tox.ini index 098a080..1e43e33 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,6 @@ [tox] envlist = - py38 - py39 + py3 linters skipsdist = True From 1eee45a0fd470edd1097ab795be9da7ae12d76e7 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 22:51:02 +0100 Subject: [PATCH 46/58] Forgot one unused import --- dungeonbattle/enums.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dungeonbattle/enums.py b/dungeonbattle/enums.py index e6fc955..d74efbc 100644 --- a/dungeonbattle/enums.py +++ b/dungeonbattle/enums.py @@ -1,5 +1,5 @@ from enum import Enum, auto -from typing import Optional, Tuple +from typing import Optional from dungeonbattle.settings import Settings From 13ac2ba13af8f89b11b6b6546e438259c6151882 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 23:00:45 +0100 Subject: [PATCH 47/58] Display comments on settings menu --- dungeonbattle/menus.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/dungeonbattle/menus.py b/dungeonbattle/menus.py index 3713900..1f741d7 100644 --- a/dungeonbattle/menus.py +++ b/dungeonbattle/menus.py @@ -56,8 +56,15 @@ class SettingsMenu(Menu): waiting_for_key: bool = False def update_values(self, settings: Settings) -> None: - s = settings.dumps_to_string() - self.values = s[6:-2].replace("\"", "").split(",\n ") + self.values = [] + for i, key in enumerate(settings.settings_keys): + s = settings.get_comment(key) + s += " : " + if self.waiting_for_key and i == self.position: + s += "? " + else: + s += getattr(settings, key).replace("\n", "\\n") + self.values.append(s) def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str, game: Any) -> None: @@ -72,15 +79,16 @@ class SettingsMenu(Menu): if key == KeyValues.UP: self.go_up() if key == KeyValues.ENTER: - option = self.validate().split(": ")[0] + option = list(game.settings.settings_keys)[self.position] if option != "TEXTURE_PACK": self.waiting_for_key = True + self.update_values(game.settings) else: - option = self.validate().split(": ")[0] + option = list(game.settings.settings_keys)[self.position] setattr(game.settings, option, raw_key) game.settings.write_settings() - self.update_values(game.settings) self.waiting_for_key = False + self.update_values(game.settings) class ArbitraryMenu(Menu): From ddf0b21e372cb981557b9e64bfe7a01e488610d7 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 23:09:15 +0100 Subject: [PATCH 48/58] Change texture pack --- dungeonbattle/display/texturepack.py | 4 ++++ dungeonbattle/menus.py | 17 ++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/dungeonbattle/display/texturepack.py b/dungeonbattle/display/texturepack.py index fc17ead..0ae8f56 100644 --- a/dungeonbattle/display/texturepack.py +++ b/dungeonbattle/display/texturepack.py @@ -31,6 +31,10 @@ class TexturePack: 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", diff --git a/dungeonbattle/menus.py b/dungeonbattle/menus.py index 1f741d7..1f8d51b 100644 --- a/dungeonbattle/menus.py +++ b/dungeonbattle/menus.py @@ -2,6 +2,7 @@ import sys from enum import Enum from typing import Any, Optional +from .display.texturepack import TexturePack from .enums import GameMode, KeyValues from .settings import Settings @@ -61,10 +62,14 @@ class SettingsMenu(Menu): s = settings.get_comment(key) s += " : " if self.waiting_for_key and i == self.position: - s += "? " + s += "?" else: s += getattr(settings, key).replace("\n", "\\n") + s += 8 * " " # Write over old text self.values.append(s) + self.values.append("") + self.values.append("Changer le pack de textures n'aura effet") + self.values.append("qu'après avoir relancé le jeu.") def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str, game: Any) -> None: @@ -78,9 +83,15 @@ class SettingsMenu(Menu): self.go_down() if key == KeyValues.UP: self.go_up() - if key == KeyValues.ENTER: + if key == KeyValues.ENTER and self.position < len(self.values) - 3: option = list(game.settings.settings_keys)[self.position] - if option != "TEXTURE_PACK": + 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: From ffa1ea01b1e2b109943c2fdc2d275732fa00419e Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 23:10:49 +0100 Subject: [PATCH 49/58] Add "back" button in settings menu --- dungeonbattle/menus.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dungeonbattle/menus.py b/dungeonbattle/menus.py index 1f8d51b..74827c6 100644 --- a/dungeonbattle/menus.py +++ b/dungeonbattle/menus.py @@ -70,6 +70,8 @@ class SettingsMenu(Menu): self.values.append("") self.values.append("Changer le pack de textures n'aura effet") self.values.append("qu'après avoir relancé le jeu.") + self.values.append("") + self.values.append("Retour (espace)") def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str, game: Any) -> None: @@ -77,7 +79,9 @@ class SettingsMenu(Menu): For now, in the settings mode, we can only go backwards. """ if not self.waiting_for_key: - if key == KeyValues.SPACE: + if key == KeyValues.SPACE or \ + key == KeyValues.ENTER and \ + self.position == len(self.values) - 1: game.state = GameMode.MAINMENU if key == KeyValues.DOWN: self.go_down() From 12c653fe1555acd052cd91be0e8dc1b09481d9a8 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 23:13:21 +0100 Subject: [PATCH 50/58] Cover change texture pack --- dungeonbattle/tests/game_test.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/dungeonbattle/tests/game_test.py b/dungeonbattle/tests/game_test.py index 2c327dc..c9bd8bb 100644 --- a/dungeonbattle/tests/game_test.py +++ b/dungeonbattle/tests/game_test.py @@ -172,6 +172,30 @@ class TestGame(unittest.TestCase): 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. From 21e85078a49dd2474575dc2c984bcd6622d93208 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Nov 2020 23:41:06 +0100 Subject: [PATCH 51/58] Display inventory content in statdisplay --- dungeonbattle/display/statsdisplay.py | 9 +++++++-- dungeonbattle/entities/items.py | 6 ++++-- dungeonbattle/entities/player.py | 6 +++++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/dungeonbattle/display/statsdisplay.py b/dungeonbattle/display/statsdisplay.py index 2eb76aa..70c6f0c 100644 --- a/dungeonbattle/display/statsdisplay.py +++ b/dungeonbattle/display/statsdisplay.py @@ -35,8 +35,13 @@ class StatsDisplay(Display): 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(3, 0, "YOU ARE DEAD", + self.pad.addstr(4, 0, "VOUS ÊTES MORT", curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT | self.color_pair(3)) @@ -44,4 +49,4 @@ class StatsDisplay(Display): self.pad.clear() self.update_pad() self.pad.refresh(0, 0, self.y, self.x, - 3 + self.y, self.width + self.x) + 4 + self.y, self.width + self.x) diff --git a/dungeonbattle/entities/items.py b/dungeonbattle/entities/items.py index e4eb74d..4cfd26b 100644 --- a/dungeonbattle/entities/items.py +++ b/dungeonbattle/entities/items.py @@ -13,8 +13,10 @@ class Item(Entity): self.held = False def drop(self, y: int, x: int) -> None: - self.held = False - self.held_by = None + 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) diff --git a/dungeonbattle/entities/player.py b/dungeonbattle/entities/player.py index 663a774..c1bde5e 100644 --- a/dungeonbattle/entities/player.py +++ b/dungeonbattle/entities/player.py @@ -15,9 +15,13 @@ class Player(FightingEntity): level: int = 1 current_xp: int = 0 max_xp: int = 10 - inventory: list = list() + inventory: list paths: Dict[Tuple[int, int], Tuple[int, int]] + def __init__(self): + super().__init__() + self.inventory = list() + def move(self, y: int, x: int) -> None: """ When the player moves, move the camera of the map. From b0292c05b24f08f556b45198f65e49de7a83a574 Mon Sep 17 00:00:00 2001 From: nicomarg Date: Wed, 11 Nov 2020 23:48:46 +0100 Subject: [PATCH 52/58] The settings menu refreshes texture pack when exited --- dungeonbattle/enums.py | 3 +++ dungeonbattle/game.py | 8 ++++---- dungeonbattle/menus.py | 3 ++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/dungeonbattle/enums.py b/dungeonbattle/enums.py index d74efbc..9bc9aed 100644 --- a/dungeonbattle/enums.py +++ b/dungeonbattle/enums.py @@ -3,6 +3,9 @@ from typing import Optional from dungeonbattle.settings import Settings +class DisplayActions(Enum): + REFRESH = auto() + UPDATE = auto() class GameMode(Enum): MAINMENU = auto() diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index 3f75960..39e7b48 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -2,7 +2,7 @@ from random import randint from typing import Any, Optional from .entities.player import Player -from .enums import GameMode, KeyValues +from .enums import GameMode, KeyValues, DisplayActions from .interfaces import Map from .settings import Settings from . import menus @@ -12,7 +12,7 @@ from typing import Callable class Game: map: Map player: Player - display_refresh: Callable[[], None] + display_actions: Callable[[DisplayActions], None] def __init__(self) -> None: """ @@ -51,7 +51,7 @@ class Game: while True: # pragma no cover screen.clear() screen.refresh() - self.display_refresh() + self.display_actions(DisplayActions.REFRESH) key = screen.getkey() self.handle_key_pressed( KeyValues.translate_key(key, self.settings), key) @@ -68,7 +68,7 @@ class Game: self.main_menu.handle_key_pressed(key, self) elif self.state == GameMode.SETTINGS: self.settings_menu.handle_key_pressed(key, raw_key, self) - self.display_refresh() + self.display_actions(DisplayActions.REFRESH) def handle_key_pressed_play(self, key: KeyValues) -> None: """ diff --git a/dungeonbattle/menus.py b/dungeonbattle/menus.py index 74827c6..f6d7058 100644 --- a/dungeonbattle/menus.py +++ b/dungeonbattle/menus.py @@ -3,7 +3,7 @@ from enum import Enum from typing import Any, Optional from .display.texturepack import TexturePack -from .enums import GameMode, KeyValues +from .enums import GameMode, KeyValues, DisplayActions from .settings import Settings @@ -82,6 +82,7 @@ class SettingsMenu(Menu): if key == KeyValues.SPACE or \ key == KeyValues.ENTER and \ self.position == len(self.values) - 1: + game.display_actions(DisplayActions.UPDATE) game.state = GameMode.MAINMENU if key == KeyValues.DOWN: self.go_down() From 92ab9ae07508f260c58ac12dc401f1b4a1bed111 Mon Sep 17 00:00:00 2001 From: nicomarg Date: Wed, 11 Nov 2020 23:56:08 +0100 Subject: [PATCH 53/58] Changed the behaviour of DisplayManager to match previous commit and fixed tests and bootstrap accordingly --- dungeonbattle/bootstrap.py | 2 +- dungeonbattle/display/display_manager.py | 7 +++++++ dungeonbattle/tests/game_test.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/dungeonbattle/bootstrap.py b/dungeonbattle/bootstrap.py index a2b5c72..3e16796 100644 --- a/dungeonbattle/bootstrap.py +++ b/dungeonbattle/bootstrap.py @@ -11,5 +11,5 @@ class Bootstrap: game = Game() game.new_game() display = DisplayManager(term_manager.screen, game) - game.display_refresh = display.refresh + game.display_actions = display.handle_display_actions game.run(term_manager.screen) diff --git a/dungeonbattle/display/display_manager.py b/dungeonbattle/display/display_manager.py index c5456bc..beb1ec4 100644 --- a/dungeonbattle/display/display_manager.py +++ b/dungeonbattle/display/display_manager.py @@ -5,6 +5,7 @@ from dungeonbattle.display.menudisplay import MenuDisplay, MainMenuDisplay from dungeonbattle.display.texturepack import TexturePack from typing import Any from dungeonbattle.game import Game, GameMode +from dungeonbattle.enums import DisplayAction class DisplayManager: @@ -23,6 +24,12 @@ class DisplayManager: self.update_game_components() self.settingsmenudisplay.update_menu(self.game.settings_menu) + def handle_display_action(self, action:DisplayAction) -> None: + if action == DisplayAction.REFRESH: + self.refresh() + elif action == DisplayAction.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) diff --git a/dungeonbattle/tests/game_test.py b/dungeonbattle/tests/game_test.py index c9bd8bb..cfded4d 100644 --- a/dungeonbattle/tests/game_test.py +++ b/dungeonbattle/tests/game_test.py @@ -18,7 +18,7 @@ class TestGame(unittest.TestCase): self.game = Game() self.game.new_game() display = DisplayManager(None, self.game) - self.game.display_refresh = display.refresh + self.game.display_actions = display.handle_display_action def test_load_game(self) -> None: self.assertRaises(NotImplementedError, Game.load_game, "game.save") From 61d66ac220f9ef25fd6ce0056719c9f9b5678bb8 Mon Sep 17 00:00:00 2001 From: nicomarg Date: Thu, 12 Nov 2020 00:12:30 +0100 Subject: [PATCH 54/58] Fixed typos --- dungeonbattle/display/display_manager.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dungeonbattle/display/display_manager.py b/dungeonbattle/display/display_manager.py index beb1ec4..a51fc9f 100644 --- a/dungeonbattle/display/display_manager.py +++ b/dungeonbattle/display/display_manager.py @@ -5,7 +5,7 @@ from dungeonbattle.display.menudisplay import MenuDisplay, MainMenuDisplay from dungeonbattle.display.texturepack import TexturePack from typing import Any from dungeonbattle.game import Game, GameMode -from dungeonbattle.enums import DisplayAction +from dungeonbattle.enums import DisplayActions class DisplayManager: @@ -22,12 +22,11 @@ class DisplayManager: self.displays = [self.statsdisplay, self.mapdisplay, self.mainmenudisplay, self.settingsmenudisplay] self.update_game_components() - self.settingsmenudisplay.update_menu(self.game.settings_menu) - def handle_display_action(self, action:DisplayAction) -> None: - if action == DisplayAction.REFRESH: + def handle_display_action(self, action:DisplayActions) -> None: + if action == DisplayActions.REFRESH: self.refresh() - elif action == DisplayAction.UPDATE: + elif action == DisplayActions.UPDATE: self.update_game_components() def update_game_components(self) -> None: @@ -35,6 +34,7 @@ class DisplayManager: 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: From d241bc82348559099b76b6ac91e97e45e0a5333e Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 12 Nov 2020 01:57:56 +0100 Subject: [PATCH 55/58] One more typo --- dungeonbattle/bootstrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dungeonbattle/bootstrap.py b/dungeonbattle/bootstrap.py index 3e16796..0bc97be 100644 --- a/dungeonbattle/bootstrap.py +++ b/dungeonbattle/bootstrap.py @@ -11,5 +11,5 @@ class Bootstrap: game = Game() game.new_game() display = DisplayManager(term_manager.screen, game) - game.display_actions = display.handle_display_actions + game.display_actions = display.handle_display_action game.run(term_manager.screen) From 1366e6a54d7a0365c574094c88319f69580b7321 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 12 Nov 2020 01:58:10 +0100 Subject: [PATCH 56/58] Linting --- dungeonbattle/display/display_manager.py | 2 +- dungeonbattle/enums.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dungeonbattle/display/display_manager.py b/dungeonbattle/display/display_manager.py index a51fc9f..b2cb125 100644 --- a/dungeonbattle/display/display_manager.py +++ b/dungeonbattle/display/display_manager.py @@ -23,7 +23,7 @@ class DisplayManager: self.mainmenudisplay, self.settingsmenudisplay] self.update_game_components() - def handle_display_action(self, action:DisplayActions) -> None: + def handle_display_action(self, action: DisplayActions) -> None: if action == DisplayActions.REFRESH: self.refresh() elif action == DisplayActions.UPDATE: diff --git a/dungeonbattle/enums.py b/dungeonbattle/enums.py index 9bc9aed..2a6b993 100644 --- a/dungeonbattle/enums.py +++ b/dungeonbattle/enums.py @@ -3,10 +3,12 @@ from typing import Optional from dungeonbattle.settings import Settings + class DisplayActions(Enum): REFRESH = auto() UPDATE = auto() + class GameMode(Enum): MAINMENU = auto() PLAY = auto() From 526a1a1e27ece979378470b7d22c73f74aa25bf0 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 12 Nov 2020 02:03:08 +0100 Subject: [PATCH 57/58] Don't use twice the same setting --- dungeonbattle/menus.py | 9 ++++++++- dungeonbattle/tests/game_test.py | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/dungeonbattle/menus.py b/dungeonbattle/menus.py index f6d7058..1990b27 100644 --- a/dungeonbattle/menus.py +++ b/dungeonbattle/menus.py @@ -76,12 +76,14 @@ class SettingsMenu(Menu): def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str, game: Any) -> None: """ - For now, in the settings mode, we can only go backwards. + Update settings """ 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: @@ -89,6 +91,7 @@ class SettingsMenu(Menu): if key == KeyValues.UP: self.go_up() if key == KeyValues.ENTER and self.position < len(self.values) - 3: + # Change a setting option = list(game.settings.settings_keys)[self.position] if option == "TEXTURE_PACK": game.settings.TEXTURE_PACK = \ @@ -101,6 +104,10 @@ class SettingsMenu(Menu): self.update_values(game.settings) else: option = list(game.settings.settings_keys)[self.position] + # 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 diff --git a/dungeonbattle/tests/game_test.py b/dungeonbattle/tests/game_test.py index cfded4d..80720ee 100644 --- a/dungeonbattle/tests/game_test.py +++ b/dungeonbattle/tests/game_test.py @@ -168,6 +168,9 @@ class TestGame(unittest.TestCase): # 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') From 600819e648491de26d089aaf3f32e984fe2237be Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 13 Nov 2020 14:07:27 +0100 Subject: [PATCH 58/58] Teddy bears has 50 HP --- dungeonbattle/entities/monsters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dungeonbattle/entities/monsters.py b/dungeonbattle/entities/monsters.py index 9c23e2f..327521f 100644 --- a/dungeonbattle/entities/monsters.py +++ b/dungeonbattle/entities/monsters.py @@ -54,5 +54,5 @@ class Rabbit(Monster): class TeddyBear(Monster): name = "teddy_bear" - maxhealth = 500 + maxhealth = 50 strength = 0