From e8ee690a80f2046f550b3db0759810a89e404782 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Wed, 10 Jun 2020 04:02:05 -1000 Subject: [PATCH 01/39] resolve conflicts --- player.py | 1 + tiles.py | 1 + world.py | 3 +++ 3 files changed, 5 insertions(+) diff --git a/player.py b/player.py index 0d21e97..a828b54 100644 --- a/player.py +++ b/player.py @@ -59,5 +59,6 @@ def do_action(self, action, **kwargs): def flee(self, tile): """Moves the player randomly to an adjacent tile""" available_moves = tile.adjacent_moves() + print(f"av moves: { available_moves }") random_room = random.randint(0, len(available_moves) - 1) self.do_action(available_moves[random_room]) diff --git a/tiles.py b/tiles.py index e932e66..0dc77b8 100644 --- a/tiles.py +++ b/tiles.py @@ -19,6 +19,7 @@ def modify_player(self, player): def adjacent_moves(self): """Returns all move actions for adjacent tiles.""" moves = [] + print(f"player is at {self.x},{self.y}") if world.tile_exists(self.x + 1, self.y): moves.append(actions.MoveRight()) if world.tile_exists(self.x - 1, self.y): diff --git a/world.py b/world.py index 6c4f338..70f58da 100644 --- a/world.py +++ b/world.py @@ -19,6 +19,9 @@ def load_tiles(): __import__('tiles'), tile_name )(x, y) + for x, y in _world: + print(f"{ x }, { y }: { _world[(x, y)] }") + def tile_exists(x, y): return _world.get((x, y)) From 442cb8f3ccfcb821d70378a1473fcb982c0de925 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Wed, 10 Jun 2020 06:51:43 -1000 Subject: [PATCH 02/39] convert game.py into class --- game.py | 91 ++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 71 insertions(+), 20 deletions(-) diff --git a/game.py b/game.py index a763321..f776e9e 100644 --- a/game.py +++ b/game.py @@ -3,26 +3,77 @@ from player import Player -def play(): - world.load_tiles() - player = Player() - room = world.tile_exists(player.location_x, player.location_y) - print(room.intro_text()) - while player.is_alive() and not player.victory: - room = world.tile_exists(player.location_x, player.location_y) - room.modify_player(player) - # Check again since the room could have changed the player's state - if player.is_alive() and not player.victory: - print('Choose an action:\n') - available_actions = room.available_actions() - for action in available_actions: - print(action) - action_input = input('Action: ') - for action in available_actions: - if action_input == action.hotkey: - player.do_action(action, **action.kwargs) +class Game(): + + def __init__(self): + + self.player = Player(self) # the current Player instance + + self.actions = [] # a list of possible actions + self.status = '' # a string describing the current situation + + self.next_key_press = "" # the next key to read from the user + + self.room = None # the current room the player is in + + self.log = [] # list of log messages + + # load the world + world.load_tiles() + + self.player.location_x = 2 + self.player.location_y = 4 + + # update the current room + self.update_room() + + print(f'loaded a new game') + + def update_room(self): + + print(f'retrieving room @ { self.player.location_x, self.player.location_y }') + + # get the tile at the current position + self.room = world.tile_exists(self.player.location_x, self.player.location_y) + self.room.modify_player(self.player) + + # update the current status + self.status = self.room.intro_text() + + # update the current actions + self.actions = self.room.available_actions() + + + def send_log_message(self, msg): + + self.log.insert(0, msg) + if len(self.log) > 5: + del self.log[5] + + + def send_key_press(self, key): + """Set the next key to input and update the game.""" + + self.next_key_press = key + self.update() + + + def get_room(self, x, y): + + return world.tile_exists(x, y) + + + def update(self): + + if self.player.is_alive() and not self.player.victory: + + # check to do any actions + for action in self.actions: + if self.next_key_press == action.hotkey: + self.player.do_action(action, **action.kwargs) break + self.update_room() -if __name__ == '__main__': - play() +# if __name__ == '__main__': + # play() From 17701355a9fc7fd332c22e310604f8fb4808bf5b Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Wed, 10 Jun 2020 06:52:38 -1000 Subject: [PATCH 03/39] convert prints to logs in player --- player.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/player.py b/player.py index a828b54..02b7d39 100644 --- a/player.py +++ b/player.py @@ -5,23 +5,26 @@ class Player(): - def __init__(self): + def __init__(self, game): + self.inventory = [items.Gold(15), items.Rock()] self.hp = 100 self.location_x, self.location_y = world.starting_position self.victory = False + self.game = game + def is_alive(self): return self.hp > 0 def print_inventory(self): for item in self.inventory: - print(item, '\n') + self.game.send_log_message(item, '\n') def move(self, dx, dy): self.location_x += dx self.location_y += dy - print(world.tile_exists(self.location_x, self.location_y).intro_text()) + self.game.send_log_message(world.tile_exists(self.location_x, self.location_y).intro_text()) def move_up(self): self.move(dx=0, dy=-1) @@ -44,12 +47,12 @@ def attack(self, enemy): max_damage = item.damage best_weapon = item - print('You use {} against {}!'.format(best_weapon.name, enemy.name)) + self.game.send_log_message('You use {} against {}!'.format(best_weapon.name, enemy.name)) enemy.hp -= best_weapon.damage if not enemy.is_alive(): - print('You killed a {}!'.format(enemy.name)) + self.game.send_log_message('You killed a {}!'.format(enemy.name)) else: - print('{} HP is {}.'.format(enemy.name, enemy.hp)) + self.game.send_log_message('{} HP is {}.'.format(enemy.name, enemy.hp)) def do_action(self, action, **kwargs): action_method = getattr(self, action.method.__name__) From 72f3010bfc6a72f3e433f279798c0a939bcd21be Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Wed, 10 Jun 2020 06:52:49 -1000 Subject: [PATCH 04/39] start terminal game renderer --- renderer.py | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 renderer.py diff --git a/renderer.py b/renderer.py new file mode 100644 index 0000000..0cfe55c --- /dev/null +++ b/renderer.py @@ -0,0 +1,114 @@ + +import curses +import locale + +from game import Game + +class Renderer(): + + def __init__(self): + + self.game = Game() + + def start(self): + + # initialize terminal + + self.stdscr = curses.initscr() + curses.noecho() + curses.cbreak() + self.stdscr.keypad(True) + + # disable cursor + + curses.curs_set(False) + + # create window + + game_map_size = (20, 20) + center = game_map_size[0]//2, game_map_size[1]//2 + + game_map = curses.newwin(game_map_size[1], game_map_size[0], 1, 0) + + self.stdscr.refresh() + + key = None + pos = [0, 0] + + while True: + + try: + if key == "q": + self.end() + break + + self.game.send_key_press(key) + + pos = self.game.player.location_x, self.game.player.location_y + + world_pos = (pos[0] + center[0], pos[1] + center[1]) + + print("pos: " + str(world_pos)) + + # clear screen + + self.stdscr.clear() + game_map.clear() + + # render + + game_map.box() + + for x in range(center[0] - game_map_size[0], game_map_size[0] - center[0]): + for y in range(center[1] - game_map_size[1], game_map_size[1] - center[1]): + try: + room = self.game.get_room(x, y) + if room is None: + game_map.addstr(y + center[1], x + center[0], '█') + except: + pass + + self.draw_text(0, 0, "Welcome to Fuck You", curses.A_STANDOUT) + self.draw_text(21, 2, "Location: " + str(pos) + "\n" + "Last Keypress: " + str(key) + '\n' + 'Health: ' + str(self.game.player.hp)) + + self.draw_text(21, 5, self.game.status) + + self.draw_text(21, 9, '\n'.join([str(act) for act in self.game.actions])) + + self.draw_text(0, 23, '\n'.join(self.game.log)) + + if world_pos[0] >= 0 and world_pos[1] >= 0: + game_map.addstr(world_pos[1], + world_pos[0], + "@".encode("UTF-8")) + + + # display screen + + self.stdscr.refresh() + game_map.refresh() + + key = self.stdscr.getkey() + + except Exception as e: + self.end() + + def draw_text(self, x, y, text, attr=None): + """Draw text to the screen.""" + + for offset, line in enumerate(text.splitlines()): + self.stdscr.addstr(y + offset, x, line) + + def end(self): + # shut down terminal + curses.nocbreak() + self.stdscr.keypad(False) + curses.echo() + curses.endwin() + +if __name__ == '__main__': + + window = Renderer() + window.start() From 26f7e482fa04c62f4ce9bff1880adc6ffa9a6c23 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Mon, 15 Jun 2020 21:10:41 -1000 Subject: [PATCH 05/39] add vec2d class --- vector.py | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 vector.py diff --git a/vector.py b/vector.py new file mode 100644 index 0000000..92e908c --- /dev/null +++ b/vector.py @@ -0,0 +1,62 @@ + +import math + +from dataclasses import dataclass + + +@dataclass +class vec2(): + """A 2D vector.""" + + x: float + y: float + + + def distance(self, vec): + """Get the distance from this vector to another.""" + + return math.sqrt( (self.x + vec.x)**2 + (self.y + vec.y)**2 ) + + + def __add__(self, o): + + if type(o) is tuple and len(o) >= 2: + # try to add the first and second elts to our x and y + return vec2(self.x + o[0], self.y + o[1]) + + elif type(o) is vec2: + return vec2(self.x + o.x, self.y + o.y) + + else: + raise TypeError('Cannot add vec2 and ' + type(o).__name__) + + def __eq__(self, o): + + if type(o) is tuple and len(o) >= 2: + # try to add the first and second elts to our x and y + return self.x == o[0] and self.y == o[1] + + elif type(o) is vec2: + return self.x == o.x and self.y == o.y + + else: + return self == o + + + def __iter__(self): + + yield self.x + yield self.y + + + def __hash__(self): + + return hash((self.x, self.y)) + +class Direction: + + UP: vec2 = vec2(0, -1) + DOWN: vec2 = vec2(0, 1) + LEFT: vec2 = vec2(-1, 0) + RIGHT: vec2= vec2(1, 0) + From 370c45d3664f66caf11cff328faeaf20bc25d166 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Mon, 15 Jun 2020 21:10:51 -1000 Subject: [PATCH 06/39] start body part system --- body.py | 144 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 body.py diff --git a/body.py b/body.py new file mode 100644 index 0000000..4087129 --- /dev/null +++ b/body.py @@ -0,0 +1,144 @@ + +import random + + +class BodyPart: + + def __init__( + self, + name: str, + attachments: list, + main_attachments: list = [], *, + is_vital: bool = False, + weight: float = 100, + ): + + # the name of the part + self.name = name + + # a list of parts (by name) that this part is attached to. + # if this part is deattached, then these parts will be too. + self.attachments = attachments + + # a list of greater attachments that this part is attached to. + # if any part in this list is deattached, then this part will be + # deattached. + self.main_attachments = main_attachments + + # if true, will kill the entity instantly if disattached + self.is_vital = is_vital + + # the status of the body part + self.status: str = 'healthy' + + self.prob_weight: float = weight + + # TODO: per-part health + + +class Body: + """A representation of parts composing an entitys' body.""" + + def __init__(self, game, ent): + + # the game instance + self.game = game + + # the entity that this body belongs to + self.owner = ent + + self.parts = {} + + self.max_health = 100 + + self.health = self.max_health + + self.is_alive = True + + def get_health_perc(self): + """Get the health of the body as a percentage.""" + + return self.health / self.max_health + + def add_part(self, bodypart: BodyPart): + + self.parts[bodypart.name] = bodypart + + def deattach_part(self, partname: str): + + bodypart = self.parts.get(partname) + if bodypart: + + self.game.add_log(self.owner.str_possessive + ' ' + partname + ' has flown off !') + self.remove_part(bodypart) + + def get_targetable_parts(self): + """Get all body parts that can be targeted e.g. not missing.""" + + return [part for part in self.parts.values() + if part.status != 'missing'] + + def pick_part(self): + """Pick a random part of the body using weighted + probability distribution. + + Use this when not targeting any particular body part. + """ + + parts = self.get_targetable_parts() + weights = [part.prob_weight for part in parts] + + if len(parts) == 0: + return None + else: + return random.choices(parts, weights)[0] + + def remove_part(self, bodypart: BodyPart): + """Remove this part and any attachments from the body.""" + + bodypart.status = 'missing' + for partname in bodypart.attachments: + child = self.parts.get(partname) + if child: + self.remove_part(child) + + def hurt(self, damage: int, bodypart=None): + + self.health -= damage + + # choose a random bodypart to target + bodypart = bodypart or self.pick_part() + + if bodypart is None: + self.game.add_log('No body part to target!') + + else: + # self.deattach_part(bodypart.name) + # return the bodypart that was damaged + return bodypart + + +class HumanoidBody(Body): + + def __init__(self, game, ent): + + super().__init__(game, ent) + + self.add_part(BodyPart("head", [], is_vital=True, weight=10)) + + self.add_part(BodyPart("chest", [ + "head", "left_arm", "right_arm", "left_leg", "right_leg" + ], is_vital=True, weight=100)) + + self.add_part(BodyPart("left_arm", ["left_hand"], ["chest"], weight=20)) + self.add_part(BodyPart("left_hand", [], ["left_arm"], weight=5)) + + self.add_part(BodyPart("right_arm", ["right_hand"], ["chest"], weight=20)) + self.add_part(BodyPart("right_hand", [], ["right_arm"], weight=5)) + + self.add_part(BodyPart("left_leg", ["left_foot"], ["chest"], weight=20)) + self.add_part(BodyPart("left_foot", [], ["left_leg"], weight=5)) + + self.add_part(BodyPart("right_leg", ["right_foot"], ["chest"], weight=20)) + self.add_part(BodyPart("right_foot", [], ["right_leg"], weight=5)) + From d10573142c1c649eaab5dc93c37c3344b54ef88b Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Mon, 15 Jun 2020 21:14:29 -1000 Subject: [PATCH 07/39] start entity base class --- entity.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 entity.py diff --git a/entity.py b/entity.py new file mode 100644 index 0000000..bd689d6 --- /dev/null +++ b/entity.py @@ -0,0 +1,9 @@ + + +class LivingEntity: + """Represents an entity that has a body and is able to die.""" + + def __init__(self): + pass + + From 3e9d79618dd23fe484e7ce33f69dc99ab7912182 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Mon, 15 Jun 2020 21:15:41 -1000 Subject: [PATCH 08/39] update gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 01356bf..fafe0f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ __pycache__/ +node_modules/ .vscode/ -clicker.py/ \ No newline at end of file +clicker.py/ From fba0480edc4c94bde582e9dd5c4840623d8ce11a Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Mon, 15 Jun 2020 21:16:13 -1000 Subject: [PATCH 09/39] convert map file --- resources/map.txt | 16 ++++++++-------- resources/map.txt.old | 8 ++++++++ 2 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 resources/map.txt.old diff --git a/resources/map.txt b/resources/map.txt index 1aa05b8..8885b6c 100644 --- a/resources/map.txt +++ b/resources/map.txt @@ -1,8 +1,8 @@ - LeaveCaveRoom - FindGoldRoom EmptyCavePath GiantSpiderRoom - EmptyCavePath - EmptyCavePath -GiantSpiderRoom EmptyCavePath StartingRoom EmptyCavePath FindDaggerRoom - FindGoldRoom - EmptyCavePath - OgreRoom \ No newline at end of file + 5 + 1.3 + . + . +3.0.2 + 1 + . + 4 diff --git a/resources/map.txt.old b/resources/map.txt.old new file mode 100644 index 0000000..1aa05b8 --- /dev/null +++ b/resources/map.txt.old @@ -0,0 +1,8 @@ + LeaveCaveRoom + FindGoldRoom EmptyCavePath GiantSpiderRoom + EmptyCavePath + EmptyCavePath +GiantSpiderRoom EmptyCavePath StartingRoom EmptyCavePath FindDaggerRoom + FindGoldRoom + EmptyCavePath + OgreRoom \ No newline at end of file From 340fd6463da4f635a02b8beccdd4bbf0a9ab950b Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Tue, 16 Jun 2020 19:16:31 -1000 Subject: [PATCH 10/39] split game and renderer code --- main.py | 92 +++++++++++++++++++++++++ renderer.py | 114 ------------------------------- screen.py | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 280 insertions(+), 114 deletions(-) create mode 100644 main.py delete mode 100644 renderer.py create mode 100644 screen.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..82b77b3 --- /dev/null +++ b/main.py @@ -0,0 +1,92 @@ + +from vector import vec2 +from screen import CursesScreen +from game import Game + +import actions +import textwrap +import re + + +scr = CursesScreen() +game = Game() + +map_size = vec2(10, 10) +# map_center = map_size // 2 +map_center = vec2(0, 0) +map_view = None + + +@scr.init +def on_init(scr: CursesScreen): + + global map_view + map_view = scr.create_view(0, 1, *map_size) + + +@scr.render +def on_render(scr: CursesScreen, key: str): + + global map_view + + game.send_key_press(key) + + width, height = scr.get_size() + + title = "~ Welcome to Super Fuck You ~" + + scr.draw_text(0, 0, title.center(width), style='standout') + scr.draw_text(21, 2, ( + "Location: " + str(game.player.pos) + "\n" + "Last Keypress: " + str(key) + )) + + + desc = re.sub(' +', ' ', game.status) + + scr.draw_text(21, 5, '\n'.join(textwrap.wrap(desc, width-21))) + scr.draw_text(21, 9, actions.format_actions(game.get_actions())) + + # draw console messages + con_y = 15 # start y position of console + for msg in game.log: + scr.draw_text(0, con_y, msg.text, style=msg.style, fg=msg.fg, bg=msg.bg) + con_y += msg.text.count('\n') + 1 # increment y by # of lines + + # draw health + scr.draw_text(0, height-1, 'Health: ' + str(game.player.get_health()), + fg=0, bg=9) + + scr.set_view(map_view) + + # map_view.box() + + # fill screen except room locations + for x in range(-int(map_size.x // 2), int(map_size.x // 2)): + for y in range(-int(map_size.y // 2), int(map_size.y // 2)): + try: + room = game.get_room(x, y) + if room is None: + scr.draw_text(x + map_center.x, y + map_center.y, u'█') + except Exception: + pass + + world_pos = game.player.pos + map_center + scr.draw_text(world_pos.x, world_pos.y, "@".encode("UTF-8")) + + scr.draw_outline() + + # for x in range(center[0] - game_map_size[0], game_map_size[0] - center[0]): + # for y in range(center[1] - game_map_size[1], game_map_size[1] - center[1]): + # try: + # room = game.get_room(x, y) + # if room is None: + # draw_text(x + center[0], y + center[1], u'█') + # except: + # pass + + # if world_pos[0] >= 0 and world_pos[1] >= 0: + # draw_text(world_pos[0], world_pos[1], "@".encode("UTF-8")) + + +scr.start() diff --git a/renderer.py b/renderer.py deleted file mode 100644 index 0cfe55c..0000000 --- a/renderer.py +++ /dev/null @@ -1,114 +0,0 @@ - -import curses -import locale - -from game import Game - -class Renderer(): - - def __init__(self): - - self.game = Game() - - def start(self): - - # initialize terminal - - self.stdscr = curses.initscr() - curses.noecho() - curses.cbreak() - self.stdscr.keypad(True) - - # disable cursor - - curses.curs_set(False) - - # create window - - game_map_size = (20, 20) - center = game_map_size[0]//2, game_map_size[1]//2 - - game_map = curses.newwin(game_map_size[1], game_map_size[0], 1, 0) - - self.stdscr.refresh() - - key = None - pos = [0, 0] - - while True: - - try: - if key == "q": - self.end() - break - - self.game.send_key_press(key) - - pos = self.game.player.location_x, self.game.player.location_y - - world_pos = (pos[0] + center[0], pos[1] + center[1]) - - print("pos: " + str(world_pos)) - - # clear screen - - self.stdscr.clear() - game_map.clear() - - # render - - game_map.box() - - for x in range(center[0] - game_map_size[0], game_map_size[0] - center[0]): - for y in range(center[1] - game_map_size[1], game_map_size[1] - center[1]): - try: - room = self.game.get_room(x, y) - if room is None: - game_map.addstr(y + center[1], x + center[0], '█') - except: - pass - - self.draw_text(0, 0, "Welcome to Fuck You", curses.A_STANDOUT) - self.draw_text(21, 2, "Location: " + str(pos) + "\n" - "Last Keypress: " + str(key) + '\n' - 'Health: ' + str(self.game.player.hp)) - - self.draw_text(21, 5, self.game.status) - - self.draw_text(21, 9, '\n'.join([str(act) for act in self.game.actions])) - - self.draw_text(0, 23, '\n'.join(self.game.log)) - - if world_pos[0] >= 0 and world_pos[1] >= 0: - game_map.addstr(world_pos[1], - world_pos[0], - "@".encode("UTF-8")) - - - # display screen - - self.stdscr.refresh() - game_map.refresh() - - key = self.stdscr.getkey() - - except Exception as e: - self.end() - - def draw_text(self, x, y, text, attr=None): - """Draw text to the screen.""" - - for offset, line in enumerate(text.splitlines()): - self.stdscr.addstr(y + offset, x, line) - - def end(self): - # shut down terminal - curses.nocbreak() - self.stdscr.keypad(False) - curses.echo() - curses.endwin() - -if __name__ == '__main__': - - window = Renderer() - window.start() diff --git a/screen.py b/screen.py new file mode 100644 index 0000000..13c68f4 --- /dev/null +++ b/screen.py @@ -0,0 +1,188 @@ + +import curses +import traceback + + +class CursesScreen: + """A terminal managed by the curses API.""" + + def __init__(self): + + self.stdscr = None + + # the current view - any draw calls will use this view object + self.view = None + + # a list of views + self.views = [] + + # a map of colors + self.colors = {} + + # called on every frame + self.on_render = lambda: None + + # called before the render loop starts + self.on_init = lambda: None + + def render(self, func): + """Set the function used to render a frame.""" + self.on_render = func + return func + + def init(self, func): + """Set the method called before the render loop.""" + self.on_init = func + return func + + def set_view(self, view=None) -> None: + """Set the active view. + + Any draw calls will use the active view. + """ + + if view is None: + self.view = self.stdscr + else: + self.view = view + + def read_input(self) -> str: + """Read input from the user. + + This method will wait until the user presses a key. The string + representation of that key will be returned. + """ + + return self.stdscr.getkey() + + def create_view(self, x, y, width, height): + """Create a new view.""" + + win = curses.newwin(height, width, y, x) + self.views.append(win) + return win + + def init_colors(self): + """Initialize the color pairs used by curses.""" + + curses.start_color() + curses.use_default_colors() + color_id = 1 + for fg in range(0, 16): + for bg in range(0, 16): + self.colors[(fg, bg)] = color_id + curses.init_pair(color_id, fg, bg) + color_id += 1 + + def get_color(self, fg=None, bg=0): + """Get a color pair by its fg and bg color.""" + return curses.color_pair(self.colors.get((fg, bg), 0)) + + def get_size(self): + """Get the size of the window as a tuple.""" + + yx = self.stdscr.getmaxyx() + return yx[1], yx[0] # flip coordinates + + def start(self): + """Start the curses screen.""" + + # initialize terminal + + self.stdscr = curses.initscr() + curses.noecho() + curses.cbreak() + self.stdscr.keypad(True) + + # initialize colors (16 colors) + + self.init_colors() + + # disable cursor + + curses.curs_set(False) + + # create window + + self.stdscr.refresh() + + # game loop + # --------- + + self.on_init(self) + + # assign some of our methods to new variables to shorten them + + key = None + set_view = self.set_view + + while True: + + try: + if key == "Q": # debug quit key + self.end() + break + + # clear screen + # ------------ + + self.stdscr.erase() + for view in self.views: + view.erase() + + # render + # ------ + + # draws to the entire screen + + set_view() + self.on_render(self, key) + + # display screen + + self.stdscr.refresh() + for view in self.views: + view.refresh() + + key = self.stdscr.getkey() + + except Exception: + + self.end() + traceback.print_exc() + break + + def draw_text(self, x, y, text, + style='normal', + fg=None, bg=0): + """Draw text to the screen.""" + + style_map = { + 'normal': curses.A_NORMAL, + 'bold': curses.A_BOLD, + 'italic': curses.A_ITALIC, + 'underline': curses.A_UNDERLINE, + 'standout': curses.A_STANDOUT + } + + attr = style_map.get(style, curses.A_NORMAL) + + color = self.get_color(fg, bg) + + for offset, line in enumerate(text.splitlines()): + try: + self.view.addstr(y + offset, x, line, color | attr) + except Exception: + pass + + def draw_outline(self): + """Draw an outline around the screen.""" + + self.view.box() + + def end(self): + # shut down terminal + curses.nocbreak() + self.stdscr.keypad(False) + curses.echo() + curses.endwin() From f46279a387a6650dfac095baee691ac7685d2a89 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Tue, 16 Jun 2020 19:16:50 -1000 Subject: [PATCH 11/39] implement division operator for vec2 --- vector.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/vector.py b/vector.py index 92e908c..34119fd 100644 --- a/vector.py +++ b/vector.py @@ -42,13 +42,25 @@ def __eq__(self, o): else: return self == o + def __truediv__(self, o) -> 'vec2': + + if type(o) is vec2: + return vec2(self.x / o.x, self.y / o.y) + else: + return vec2(self.x / o, self.y / o) + + def __floordiv__(self, o) -> 'vec2': + + if type(o) is vec2: + return vec2(self.x // o.x, self.y // o.y) + else: + return vec2(self.x // o, self.y // o) def __iter__(self): yield self.x yield self.y - def __hash__(self): return hash((self.x, self.y)) From f7f1d3520e3475de62bb7d5c1eec6b7c121e5ac4 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Tue, 16 Jun 2020 19:49:26 -1000 Subject: [PATCH 12/39] implement automatic situation descriptor --- actions.py | 180 +++++++++++++++++++++++++++++++++++------------------ enemies.py | 7 +++ game.py | 125 ++++++++++++++++++++++++++++--------- main.py | 3 +- player.py | 61 +++++++++++++----- tiles.py | 127 ++++++++++++++++++++----------------- world.py | 102 +++++++++++++++++++++++++++++- 7 files changed, 435 insertions(+), 170 deletions(-) diff --git a/actions.py b/actions.py index 2e4a33d..6c5d3a5 100644 --- a/actions.py +++ b/actions.py @@ -1,79 +1,135 @@ #!/usr/bin/python3 env + from player import Player +from vector import vec2 + + +def format_actions(actions: list) -> str: + """Formats a list of actions as a user-readable string.""" + + lines = [f'{ action.key }: { action.desc }' for action in actions] + + return '\n'.join(lines) + + +def format_vector(vec: vec2) -> str: + """Formats a vector as a user-readable string.""" + + lines = [] + + if vec.x > 0: + lines.append(f'up by { vec.x }') + elif vec.x < 0: + lines.append(f'down by { -vec.x }') + + if vec.y > 0: + lines.append(f'right by { vec.y }') + elif vec.y < 0: + lines.append(f'left by { -vec.y }') + + return 'Move ' + 'and'.join(lines) class Action(): - def __init__(self, method, name, hotkey, **kwargs): - self.method = method - self.hotkey = hotkey - self.name = name - self.kwargs = kwargs + """An action that can be taken.""" + + def __init__(self, desc: str = None): + + # the description of this action + self.desc = desc or "unknown action" + + def do_action(self): + """Run the action.""" + pass + + +class PlayerAction(Action): + """An action that is run on a player.""" + + def __init__(self, player: Player, key: str, desc: str = None): + + super().__init__(desc) + + # the hotkey that will run this action + self.key = key + + # the player to run this action on + self.player: Player = player + - def __str__(self): - return '{}: {}'.format(self.hotkey, self.name) +class MoveUp(PlayerAction): + def __init__(self, player): + super().__init__(player, 'w', 'move up') -class MoveUp(Action): - def __init__(self): - super().__init__( - method=Player.move_up, - name='Move up', - hotkey='w' - ) + def do_action(self): + self.player.move_up() -class MoveDown(Action): - def __init__(self): - super().__init__( - method=Player.move_down, - name='Move down', - hotkey='s' - ) +class MoveDown(PlayerAction): + def __init__(self, player): + super().__init__(player, 's', 'move down') -class MoveRight(Action): - def __init__(self): - super().__init__( - method=Player.move_right, - name='Move right', - hotkey='d' - ) + def do_action(self): + self.player.move_down() -class MoveLeft(Action): - def __init__(self): - super().__init__( - method=Player.move_left, - name='Move left', - hotkey='a' - ) +class MoveRight(PlayerAction): + def __init__(self, player): + super().__init__(player, 'd', 'move right') -class ViewInventory(Action): + def do_action(self): + self.player.move_right() + + +class MoveLeft(PlayerAction): + + def __init__(self, player): + super().__init__(player, 'a', 'move left') + + def do_action(self): + self.player.move_left() + + +class ViewInventory(PlayerAction): """Prints the player's inventory""" - def __init__(self): - super().__init__( - method=Player.print_inventory, - name='View inventory', - hotkey='i' - ) - - -class Attack(Action): - def __init__(self, enemy): - super().__init__( - method=Player.attack, - name='Attack', - hotkey='k', - enemy=enemy - ) - - -class Flee(Action): - def __init__(self, tile): - super().__init__( - method=Player.flee, - name='Flee!', - hotkey='j', - tile=tile - ) + def __init__(self, player): + super().__init__(player, 'i', 'View inventory') + + def do_action(self): + self.player.print_inventory() + + +class Attack(PlayerAction): + + def __init__(self, player: Player, enemy): + super().__init__(player, 'k', 'Attack') + self.enemy = enemy + + def do_action(self): + self.player.attack(self.enemy) + + +class Flee(PlayerAction): + + def __init__(self, player: Player): + super().__init__(player, 'j', 'Flee!') + + def do_action(self): + self.player.flee() + + +class CheckBodyAction(PlayerAction): + + def __init__(self, player): + super().__init__(player, 'c', '(debug) check body') + self.player = player + + def do_action(self): + lines = ['You have:'] + for part in self.player.body.parts.values(): + lines.append(f'{part.name} ({ part.status }) -> {part.attachments}') + + self.player.game.add_log('\n'.join(lines)) diff --git a/enemies.py b/enemies.py index 9e42a75..620bea3 100644 --- a/enemies.py +++ b/enemies.py @@ -2,6 +2,7 @@ class Enemy: + def __init__(self, name, hp, damage): self.name = name self.hp = hp @@ -10,6 +11,12 @@ def __init__(self, name, hp, damage): def is_alive(self): return self.hp > 0 + def hurt(self, damage): + self.hp -= damage + + def get_health(self): + return self.hp + class GiantSpider(Enemy): def __init__(self): diff --git a/game.py b/game.py index f776e9e..aeeb49f 100644 --- a/game.py +++ b/game.py @@ -1,55 +1,69 @@ #!/usr/bin/python3 env + import world +import actions + +from tiles import Room +from dataclasses import dataclass +from vector import vec2 from player import Player +@dataclass +class Message(): + text: str + style: str = 'normal' + fg: int = 0 + bg: int = 0 + + class Game(): def __init__(self): - self.player = Player(self) # the current Player instance + # the current Player instance + self.player: Player = Player(self) - self.actions = [] # a list of possible actions - self.status = '' # a string describing the current situation + # a string describing the current situation + self.status = '' - self.next_key_press = "" # the next key to read from the user + # the next key to read from the user + self.next_key_press = "" - self.room = None # the current room the player is in + # the current room the player is in + self.room = None - self.log = [] # list of log messages + # list of log messages + self.log = [] # load the world - world.load_tiles() + self.world = world.parse_world(self) - self.player.location_x = 2 - self.player.location_y = 4 + self.player.pos = vec2(2, 4) # update the current room self.update_room() - print(f'loaded a new game') + print('loaded a new game') def update_room(self): - print(f'retrieving room @ { self.player.location_x, self.player.location_y }') + self.add_log(f'retrieving room @ { self.player.pos }', 8) # get the tile at the current position - self.room = world.tile_exists(self.player.location_x, self.player.location_y) - self.room.modify_player(self.player) - - # update the current status - self.status = self.room.intro_text() - - # update the current actions - self.actions = self.room.available_actions() + self.room = self.world.get_room_at(self.player.pos) + if self.room: + self.room.modify_player(self.player) - def send_log_message(self, msg): + # TODO: Room.intro_text() is deprecated - self.log.insert(0, msg) - if len(self.log) > 5: - del self.log[5] + def add_log(self, msg, fg=15, bg=0, style='normal'): + # insert to the front of the list + self.log.insert(0, Message(msg, style, fg, bg)) + if len(self.log) > 10: + del self.log[10] def send_key_press(self, key): """Set the next key to input and update the game.""" @@ -57,23 +71,74 @@ def send_key_press(self, key): self.next_key_press = key self.update() + def get_room(self, x, y) -> str: + """Get a room by its coordinates.""" - def get_room(self, x, y): - - return world.tile_exists(x, y) - + return self.world.get_room_at(vec2(x, y)) def update(self): if self.player.is_alive() and not self.player.victory: # check to do any actions - for action in self.actions: - if self.next_key_press == action.hotkey: - self.player.do_action(action, **action.kwargs) + for action in self.get_actions(): + if self.next_key_press == action.key: + action.do_action() break self.update_room() + def get_movement_actions(self) -> list: + """Get all movement actions that the player can do.""" + return self.world.get_movement_actions(self.player.pos, self.player) + + def get_actions(self) -> list: + """Get all actions that the player can do.""" + # self.add_log("getting actions...") + + action_list = [] + + # get movement actions + action_list += self.get_movement_actions() # get movement actions + + # get room actions + if self.room: + action_list += self.room.get_actions(self.player) # get room actions + + # debug actions + action_list.append(actions.CheckBodyAction(self.player)) + + return action_list + + def get_description(self, skill_check: int = 10): + """Get a description of the room based on what the player sees. + If a skill check is provided, change the description based + on the value of the skill check. + """ + + if not self.room: + return "You are out of bounds." + + lines = [] + + if skill_check <= 0: # failed check + lines += ["You cannot see anything."] + + else: + if self.room.light_level == 0: + lines += ["It is too dark to see anything."] + + else: + # get number of paths + num_paths = len(self.get_movement_actions()) + lines += [f"There are {num_paths} paths."] + + # get entities in room + for ent in self.room.entities: + lines += [f"You see a {ent.name}."] + + return ' '.join(lines) + + # if __name__ == '__main__': # play() diff --git a/main.py b/main.py index 82b77b3..8025669 100644 --- a/main.py +++ b/main.py @@ -41,8 +41,7 @@ def on_render(scr: CursesScreen, key: str): "Last Keypress: " + str(key) )) - - desc = re.sub(' +', ' ', game.status) + desc = re.sub(' +', ' ', game.get_description()) scr.draw_text(21, 5, '\n'.join(textwrap.wrap(desc, width-21))) scr.draw_text(21, 9, actions.format_actions(game.get_actions())) diff --git a/player.py b/player.py index 02b7d39..aed578c 100644 --- a/player.py +++ b/player.py @@ -1,30 +1,55 @@ #!/usr/bin/python3 env import items -import world import random +import body + +from vector import vec2 class Player(): + def __init__(self, game): - self.inventory = [items.Gold(15), items.Rock()] - self.hp = 100 - self.location_x, self.location_y = world.starting_position + # the position of the player + self.pos = vec2(0, 0) + + # the body of the player + self.body = body.HumanoidBody(game, self) + self.victory = False self.game = game - def is_alive(self): - return self.hp > 0 + self.inventory = [items.Gold(15), items.Rock()] + + # slots + self.item_slots = {} + + # strings + # ------- + + self.str_possessive = "Your" + + self.str_name = "You" + + def hurt(self, damage): + return self.body.hurt(damage) + + def get_health(self) -> int: + return self.body.health + + def is_alive(self) -> int: + return self.body.is_alive def print_inventory(self): - for item in self.inventory: - self.game.send_log_message(item, '\n') + line = '\n'.join([str(item) for item in self.inventory]) + self.game.add_log(line) def move(self, dx, dy): - self.location_x += dx - self.location_y += dy - self.game.send_log_message(world.tile_exists(self.location_x, self.location_y).intro_text()) + + self.pos += (dx, dy) + + # self.game.add_log(world.tile_exists(self.pos).intro_text()) def move_up(self): self.move(dx=0, dy=-1) @@ -47,21 +72,25 @@ def attack(self, enemy): max_damage = item.damage best_weapon = item - self.game.send_log_message('You use {} against {}!'.format(best_weapon.name, enemy.name)) + self.game.add_log('You use {} against {}!'.format(best_weapon.name, enemy.name)) enemy.hp -= best_weapon.damage if not enemy.is_alive(): - self.game.send_log_message('You killed a {}!'.format(enemy.name)) + self.game.add_log('You killed a {}!'.format(enemy.name)) else: - self.game.send_log_message('{} HP is {}.'.format(enemy.name, enemy.hp)) + self.game.add_log('{} HP is {}.'.format(enemy.name, enemy.hp)) def do_action(self, action, **kwargs): action_method = getattr(self, action.method.__name__) if action_method: action_method(**kwargs) - def flee(self, tile): + def flee(self): """Moves the player randomly to an adjacent tile""" - available_moves = tile.adjacent_moves() + + available_moves = self.game.get_movement_actions() print(f"av moves: { available_moves }") random_room = random.randint(0, len(available_moves) - 1) + + self.game.add_log('You have fled to another room') + self.do_action(available_moves[random_room]) diff --git a/tiles.py b/tiles.py index 0dc77b8..3991e9f 100644 --- a/tiles.py +++ b/tiles.py @@ -2,13 +2,33 @@ import items import enemies import actions -import world +from entity import LivingEntity +from vector import vec2, Direction -class MapTile: - def __init__(self, x, y): - self.x = x - self.y = y + +class Room: + def __init__(self): + + self.game = None + + # a list of entities in this room + self.entities = [] + + # a description of this room + self.desc = "a room" + + # the level of brightness in the room + # 0 => pitch black + # 50 => normal + # 100 => blinding + self.light_level = 50 + + # the temperature level in the room + # 0 => freezing + # 50 => normal + # 100 => extremely hot + self.temp_level = 50 def intro_text(self): raise NotImplementedError() @@ -16,45 +36,32 @@ def intro_text(self): def modify_player(self, player): raise NotImplementedError() - def adjacent_moves(self): - """Returns all move actions for adjacent tiles.""" - moves = [] - print(f"player is at {self.x},{self.y}") - if world.tile_exists(self.x + 1, self.y): - moves.append(actions.MoveRight()) - if world.tile_exists(self.x - 1, self.y): - moves.append(actions.MoveLeft()) - if world.tile_exists(self.x, self.y - 1): - moves.append(actions.MoveUp()) - if world.tile_exists(self.x, self.y + 1): - moves.append(actions.MoveDown()) - - return moves - - def available_actions(self): + def get_actions(self, ent: LivingEntity) -> list: """Returns all of the available actions in this room.""" - moves = self.adjacent_moves() - moves.append(actions.ViewInventory()) - return moves + return [] -class StartingRoom(MapTile): +class StartingRoom(Room): def intro_text(self): return """ You find yourself in a cave with a flickering torch on the wall. You can make out four paths, each equally as dark and foreboding. - """ + """.strip() def modify_player(self, player): # Room has no action on player pass -class LootRoom(MapTile): - def __init__(self, x, y, item): +class LootRoom(Room): + + def __init__(self, item): + + super().__init__() + + self.entities.append(item) self.item = item - super().__init__(x, y) def add_loot(self, player): player.inventory.append(self.item) @@ -63,30 +70,36 @@ def modify_player(self, player): self.add_loot(player) -class EnemyRoom(MapTile): - def __init__(self, x, y, enemy): +class EnemyRoom(Room): + + def __init__(self, enemy): + + super().__init__() + + self.entities.append(enemy) self.enemy = enemy - super().__init__(x, y) def modify_player(self, the_player): + if self.enemy.is_alive(): - the_player.hp = the_player.hp - self.enemy.damage - print('Enemy does {} damage. You have {} HP remaining.'.format( - self.enemy.damage, the_player.hp - )) + # TODO: put into an Enemy.attack method + part = the_player.hurt(self.enemy.damage) + self.game.add_log(f'An enemy hits your { part.name } and ' + f'does { self.enemy.damage } !') + + def get_actions(self, ent: LivingEntity): - def available_actions(self): if self.enemy.is_alive(): - return [actions.Flee(tile=self), actions.Attack(enemy=self.enemy)] + return [actions.Flee(ent), actions.Attack(ent, enemy=self.enemy)] else: - return self.adjacent_moves() + return [] -class EmptyCavePath(MapTile): +class EmptyCavePath(Room): def intro_text(self): return """ Another unremarkable part of the cave. You must forge onwards. - """ + """.strip() def modify_player(self, player): # Room has no action on player @@ -94,65 +107,65 @@ def modify_player(self, player): class GiantSpiderRoom(EnemyRoom): - def __init__(self, x, y): - super().__init__(x, y, enemies.GiantSpider()) + def __init__(self): + super().__init__(enemies.GiantSpider()) def intro_text(self): if self.enemy.is_alive(): return """ A giant spider jumps down from its web in front of you! - """ + """.strip() else: return """ The corpse of a dead spider rots on the ground. - """ + """.strip() class OgreRoom(EnemyRoom): - def __init__(self, x, y): - super().__init__(x, y, enemies.Ogre()) + def __init__(self): + super().__init__(enemies.Ogre()) def intro_text(self): if self.enemy.is_alive(): return """ An Ogre appears from the shadows! - """ + """.strip() else: return """ The corpse of a dead ogre rots on the ground. - """ + """.strip() class FindGoldRoom(LootRoom): - def __init__(self, x, y): - super().__init__(x, y, items.Gold(10)) + def __init__(self): + super().__init__(items.Gold(10)) def intro_text(self): return """ Your torch lits a faint gold coin in the room. What luck! - """ + """.strip() class FindDaggerRoom(LootRoom): - def __init__(self, x, y): - super().__init__(x, y, items.Dagger()) + def __init__(self): + super().__init__(items.Dagger()) def intro_text(self): return """ Your notice something shiny in the corner. It's a dagger! You pick it up. - """ + """.strip() -class LeaveCaveRoom(MapTile): +class LeaveCaveRoom(Room): def intro_text(self): return """ You see a bright light in the distance... ... it grows as you get closer! It's sunlight! Victory is yours! - """ + """.strip() def modify_player(self, player): player.victory = True diff --git a/world.py b/world.py index 70f58da..c9329b3 100644 --- a/world.py +++ b/world.py @@ -1,6 +1,98 @@ #!/usr/bin/python3 env + +from vector import vec2, Direction +import tiles +import actions + + +class World: + + def __init__(self, game): + + self.game = game + + # a dict of rooms with their position as the key + self.room_map = {} + + def set_room_at(self, pos: vec2, room: tiles.Room): + """Add a room to the map. Will overwrite a room if it exists in the same + location.""" + + self.room_map[pos] = room + + room.game = self.game + + def get_room_at(self, pos: vec2): + """Get a room from the map. Returns None if there is no room at that + location.""" + + return self.room_map.get(pos, None) + + def get_movement_actions(self, pos: vec2, player) -> list: + """Returns all move actions for adjacent tiles.""" + + mapping = { + Direction.UP: actions.MoveUp, + Direction.LEFT: actions.MoveLeft, + Direction.RIGHT: actions.MoveRight, + Direction.DOWN: actions.MoveDown + } + + move_actions = [] + + for direction, action in mapping.items(): + if self.get_room_at(pos + direction): + move_actions.append(action(player)) + + return move_actions + + +room_mapping = { + '.': tiles.EmptyCavePath, + '0': tiles.StartingRoom, + '1': tiles.FindGoldRoom, + '2': tiles.FindDaggerRoom, + '3': tiles.GiantSpiderRoom, + '4': tiles.OgreRoom, + '5': tiles.LeaveCaveRoom, +} + + +def parse_world(game, filename='resources/map.txt') -> World: + """Parse a world file.""" + + world = World(game) + + x, y = 0, 0 # the current world location we are reading at + + print('loading world...') + + with open(filename, 'r') as file: + for c in file.read(): + + if c == '\n': + x = 0 # reset x back to 0 + y += 1 # increment y + continue + + # try to find the room type in our mapping + room_type = room_mapping.get(c, None) + + if room_type: + print(f'\tsetting loc {x, y} to {room_type.__name__}') + world.set_room_at(vec2(x, y), room_type()) + + x += 1 + + print('world loaded.') + return world + + +# old parser +# ------------------------------------------------------------------------------ + _world = {} -starting_position = (0, 0) +starting_position = vec2(2, 4) def load_tiles(): @@ -23,5 +115,9 @@ def load_tiles(): print(f"{ x }, { y }: { _world[(x, y)] }") -def tile_exists(x, y): - return _world.get((x, y)) +def tile_exists(pos: vec2): + return _world.get((pos.x, pos.y)) + + +if __name__ == '__main__': + parse_world() From 08393de06a3722abbe5d91a6d443137ca114ed56 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Tue, 16 Jun 2020 20:03:57 -1000 Subject: [PATCH 13/39] start inventory holder class --- actions.py | 20 +++++++++++--------- game.py | 1 + inventory.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ player.py | 7 +++++-- 4 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 inventory.py diff --git a/actions.py b/actions.py index 6c5d3a5..9a151ab 100644 --- a/actions.py +++ b/actions.py @@ -93,15 +93,6 @@ def do_action(self): self.player.move_left() -class ViewInventory(PlayerAction): - """Prints the player's inventory""" - def __init__(self, player): - super().__init__(player, 'i', 'View inventory') - - def do_action(self): - self.player.print_inventory() - - class Attack(PlayerAction): def __init__(self, player: Player, enemy): @@ -121,6 +112,17 @@ def do_action(self): self.player.flee() +class CheckInventory(PlayerAction): + """Prints the player's inventory""" + def __init__(self, player): + super().__init__(player, 'i', 'View inventory') + + def do_action(self): + + desc = self.player.describe_inventory() + self.player.game.add_log(desc) + + class CheckBodyAction(PlayerAction): def __init__(self, player): diff --git a/game.py b/game.py index aeeb49f..84abf75 100644 --- a/game.py +++ b/game.py @@ -107,6 +107,7 @@ def get_actions(self) -> list: # debug actions action_list.append(actions.CheckBodyAction(self.player)) + action_list.append(actions.CheckInventory(self.player)) return action_list diff --git a/inventory.py b/inventory.py new file mode 100644 index 0000000..4966e55 --- /dev/null +++ b/inventory.py @@ -0,0 +1,44 @@ + + +class InventoryHolder: + """An object that can hold an inventory.""" + + def __init__(self): + + # the list of items in this inventory + self.inventory = [] + + def has_item(self, name: str): + """Return if this inventory has an item (by name).""" + + return any([item.name == name for item in self.inventory]) + + def give_item(self, *items): + """Add items to this inventory.""" + + self.inventory += items + + def remove_item(self, name: str): + """Remove an item from this inventory. + + If multiple items with the same name exist, only remove one of them. + """ + + found = False + for i, item in enumerate(self.inventory): + if item.name == name: + del self.inventory[i] + found = True + break + + return found + + def describe_inventory(self) -> str: + """Get a description of this inventory in readable format.""" + + lines = ["Inventory:"] + + for item in self.inventory: + lines += [f"- {item.name}"] + + return '\n'.join(lines) diff --git a/player.py b/player.py index aed578c..ee40c6e 100644 --- a/player.py +++ b/player.py @@ -4,12 +4,15 @@ import body from vector import vec2 +from inventory import InventoryHolder -class Player(): +class Player(InventoryHolder): def __init__(self, game): + InventoryHolder.__init__(self) + # the position of the player self.pos = vec2(0, 0) @@ -20,7 +23,7 @@ def __init__(self, game): self.game = game - self.inventory = [items.Gold(15), items.Rock()] + self.give_item(items.Gold(15), items.Rock()) # slots self.item_slots = {} From 96290dca2bfaa89b63f98e427efa7809c7dcb0ee Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Tue, 16 Jun 2020 20:59:45 -1000 Subject: [PATCH 14/39] split inventory and body functionality into separate classes --- entity.py | 36 +++++++++++++++++++++++++++++++++--- game.py | 6 +++--- inventory.py | 17 +++++++++++++++-- items.py | 1 + player.py | 24 ++++-------------------- tiles.py | 9 +++++++-- 6 files changed, 63 insertions(+), 30 deletions(-) diff --git a/entity.py b/entity.py index bd689d6..ca738fe 100644 --- a/entity.py +++ b/entity.py @@ -1,9 +1,39 @@ +import typing +import body -class LivingEntity: + +if typing.TYPE_CHECKING: + from game import Game + + +class Entity: + """Represents an entity.""" + + def __init__(self, game: 'Game', name: str): + + # a reference to the game + self.game: 'Game' = game + + # the name of the entity + self.name = name + + +class LivingEntity(Entity): """Represents an entity that has a body and is able to die.""" - def __init__(self): - pass + def __init__(self, game: 'Game', name: str): + + Entity.__init__(self, game, name) + + # the body of the entity + self.body = body.HumanoidBody(game, self) + + def hurt(self, damage): + return self.body.hurt(damage) + def get_health(self) -> int: + return self.body.health + def is_alive(self) -> int: + return self.body.is_alive diff --git a/game.py b/game.py index 84abf75..f37a227 100644 --- a/game.py +++ b/game.py @@ -21,9 +21,6 @@ class Game(): def __init__(self): - # the current Player instance - self.player: Player = Player(self) - # a string describing the current situation self.status = '' @@ -39,6 +36,9 @@ def __init__(self): # load the world self.world = world.parse_world(self) + # the current Player instance + self.player: Player = Player(self) + self.player.pos = vec2(2, 4) # update the current room diff --git a/inventory.py b/inventory.py index 4966e55..fc81bd9 100644 --- a/inventory.py +++ b/inventory.py @@ -1,9 +1,20 @@ +import typing +from entity import Entity -class InventoryHolder: + +if typing.TYPE_CHECKING: + from game import Game + + +class InventoryHolder(Entity): """An object that can hold an inventory.""" - def __init__(self): + def __init__(self, game: 'Game'): + + Entity.__init__(self, game, 'Unnamed Inventory') + + self.game: 'Game' = game # the list of items in this inventory self.inventory = [] @@ -17,6 +28,8 @@ def give_item(self, *items): """Add items to this inventory.""" self.inventory += items + item_str = ', '.join([item.name for item in items]) + self.game.add_log(f'{self.name} picked up {item_str}') def remove_item(self, name: str): """Remove an item from this inventory. diff --git a/items.py b/items.py index 8fef072..9c12589 100644 --- a/items.py +++ b/items.py @@ -4,6 +4,7 @@ class Item(): """The base class for all items""" def __init__(self, name, description, value): + self.name = name self.description = description self.value = value diff --git a/player.py b/player.py index ee40c6e..dacff30 100644 --- a/player.py +++ b/player.py @@ -1,24 +1,22 @@ #!/usr/bin/python3 env import items import random -import body from vector import vec2 +from entity import LivingEntity from inventory import InventoryHolder -class Player(InventoryHolder): +class Player(LivingEntity, InventoryHolder): def __init__(self, game): - InventoryHolder.__init__(self) + InventoryHolder.__init__(self, game) + LivingEntity.__init__(self, game, name='You') # the position of the player self.pos = vec2(0, 0) - # the body of the player - self.body = body.HumanoidBody(game, self) - self.victory = False self.game = game @@ -35,21 +33,7 @@ def __init__(self, game): self.str_name = "You" - def hurt(self, damage): - return self.body.hurt(damage) - - def get_health(self) -> int: - return self.body.health - - def is_alive(self) -> int: - return self.body.is_alive - - def print_inventory(self): - line = '\n'.join([str(item) for item in self.inventory]) - self.game.add_log(line) - def move(self, dx, dy): - self.pos += (dx, dy) # self.game.add_log(world.tile_exists(self.pos).intro_text()) diff --git a/tiles.py b/tiles.py index 3991e9f..3104e53 100644 --- a/tiles.py +++ b/tiles.py @@ -1,4 +1,5 @@ #!/usr/bin/python3 env +import typing import items import enemies import actions @@ -7,6 +8,10 @@ from vector import vec2, Direction +if typing.TYPE_CHECKING: + from player import Player + + class Room: def __init__(self): @@ -63,8 +68,8 @@ def __init__(self, item): self.entities.append(item) self.item = item - def add_loot(self, player): - player.inventory.append(self.item) + def add_loot(self, player: 'Player'): + player.give_item(self.item) def modify_player(self, player): self.add_loot(player) From 10ab3ddd013f919fd4c99b108cf1013e88f0643e Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Tue, 16 Jun 2020 22:09:39 -1000 Subject: [PATCH 15/39] improve item API --- items.py | 40 +++++++++++++++++++++++++++------------- player.py | 2 +- tiles.py | 38 ++++++++++++++++++++++++-------------- world.py | 2 +- 4 files changed, 53 insertions(+), 29 deletions(-) diff --git a/items.py b/items.py index 9c12589..1922d18 100644 --- a/items.py +++ b/items.py @@ -1,46 +1,59 @@ #!/usr/bin/python3 env +from entity import Entity -class Item(): + +class Item(Entity): """The base class for all items""" - def __init__(self, name, description, value): + def __init__(self, game, name, description, value, amount=1): + + Entity.__init__(self, game, name) - self.name = name + # a description of the item self.description = description + + # the price of this item (per item) self.value = value + # the amount of this item (stacking) + self.amount = amount + def __str__(self): return "{}\n=====\n{}\nValue: {}\n".format( - self.name, self.description, self.value + self.name, self.description, self.amount ) class Gold(Item): - def __init__(self, amount): - self.amount = amount + def __init__(self, game, value): super().__init__( + game, name="Gold", description="A round coin with {} stamped on the front.".format( - str(self.amount) + str(value) ), - value=self.amount + value=value, ) class Weapon(Item): - def __init__(self, name, description, value, damage): + def __init__(self, game, name, description, value, damage): + + super().__init__(game, name, description, value) + + # the base damage of the weapon self.damage = damage - super().__init__(name, description, value) def __str__(self): return "{}\n=====\n{}\nValue: {}\nDamage: {}".format( self.name, self.description, self.value, self.damage - ) + ) class Rock(Weapon): - def __init__(self): + def __init__(self, game): super().__init__( + game, name="Rock", description="A fist-sized rock, suitable for bludgeoning.", value=0, @@ -49,8 +62,9 @@ def __init__(self): class Dagger(Weapon): - def __init__(self): + def __init__(self, game): super().__init__( + game, name="Dagger", description="A small dagger with some rust." "Somewhat more dangerous than a rock.", diff --git a/player.py b/player.py index dacff30..a08cf75 100644 --- a/player.py +++ b/player.py @@ -21,7 +21,7 @@ def __init__(self, game): self.game = game - self.give_item(items.Gold(15), items.Rock()) + self.give_item(items.Gold(game, 15), items.Rock(game)) # slots self.item_slots = {} diff --git a/tiles.py b/tiles.py index 3104e53..12bb96a 100644 --- a/tiles.py +++ b/tiles.py @@ -10,12 +10,13 @@ if typing.TYPE_CHECKING: from player import Player + from game import Game class Room: - def __init__(self): + def __init__(self, game: 'Game'): - self.game = None + self.game: 'Game' = game # a list of entities in this room self.entities = [] @@ -48,6 +49,10 @@ def get_actions(self, ent: LivingEntity) -> list: class StartingRoom(Room): + + def __init__(self, game: 'Game'): + Room.__init__(self, game) + def intro_text(self): return """ You find yourself in a cave with a flickering torch on the wall. @@ -61,9 +66,9 @@ def modify_player(self, player): class LootRoom(Room): - def __init__(self, item): + def __init__(self, game: 'Game', item): - super().__init__() + Room.__init__(self, game) self.entities.append(item) self.item = item @@ -77,9 +82,9 @@ def modify_player(self, player): class EnemyRoom(Room): - def __init__(self, enemy): + def __init__(self, game: 'Game', enemy): - super().__init__() + Room.__init__(self, game) self.entities.append(enemy) self.enemy = enemy @@ -101,6 +106,10 @@ def get_actions(self, ent: LivingEntity): class EmptyCavePath(Room): + + def __init__(self, game: 'Game'): + Room.__init__(self, game) + def intro_text(self): return """ Another unremarkable part of the cave. You must forge onwards. @@ -112,8 +121,9 @@ def modify_player(self, player): class GiantSpiderRoom(EnemyRoom): - def __init__(self): - super().__init__(enemies.GiantSpider()) + + def __init__(self, game: 'Game'): + EnemyRoom.__init__(self, game, enemies.GiantSpider()) def intro_text(self): if self.enemy.is_alive(): @@ -127,8 +137,8 @@ def intro_text(self): class OgreRoom(EnemyRoom): - def __init__(self): - super().__init__(enemies.Ogre()) + def __init__(self, game): + super().__init__(game, enemies.Ogre()) def intro_text(self): if self.enemy.is_alive(): @@ -142,8 +152,8 @@ def intro_text(self): class FindGoldRoom(LootRoom): - def __init__(self): - super().__init__(items.Gold(10)) + def __init__(self, game): + super().__init__(game, items.Gold(game, 10)) def intro_text(self): return """ @@ -153,8 +163,8 @@ def intro_text(self): class FindDaggerRoom(LootRoom): - def __init__(self): - super().__init__(items.Dagger()) + def __init__(self, game): + super().__init__(game, items.Dagger(game)) def intro_text(self): return """ diff --git a/world.py b/world.py index c9329b3..1dd8752 100644 --- a/world.py +++ b/world.py @@ -80,7 +80,7 @@ def parse_world(game, filename='resources/map.txt') -> World: if room_type: print(f'\tsetting loc {x, y} to {room_type.__name__}') - world.set_room_at(vec2(x, y), room_type()) + world.set_room_at(vec2(x, y), room_type(game)) x += 1 From 05466c209c1520769f69e34dace994205c543652 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Tue, 16 Jun 2020 22:27:33 -1000 Subject: [PATCH 16/39] fix loot rooms giving infinite items --- entity.py | 10 ++++++++++ game.py | 12 ++++++++---- inventory.py | 17 +++++++++++------ items.py | 5 +---- tiles.py | 5 +++-- 5 files changed, 33 insertions(+), 16 deletions(-) diff --git a/entity.py b/entity.py index ca738fe..48ab94f 100644 --- a/entity.py +++ b/entity.py @@ -18,6 +18,16 @@ def __init__(self, game: 'Game', name: str): # the name of the entity self.name = name + def __eq__(self, o): + + if type(self) == type(o): + return self.name == o.name + else: + return self == o + + def __hash__(self): + return hash(self.name) + class LivingEntity(Entity): """Represents an entity that has a body and is able to die.""" diff --git a/game.py b/game.py index f37a227..13b5850 100644 --- a/game.py +++ b/game.py @@ -48,16 +48,18 @@ def __init__(self): def update_room(self): - self.add_log(f'retrieving room @ { self.player.pos }', 8) - + # self.add_log(f'retrieving room @ { self.player.pos }', 8) # get the tile at the current position self.room = self.world.get_room_at(self.player.pos) + # TODO: Room.intro_text() is deprecated + + def do_tick(self): + """Calculate one unit of time.""" + if self.room: self.room.modify_player(self.player) - # TODO: Room.intro_text() is deprecated - def add_log(self, msg, fg=15, bg=0, style='normal'): # insert to the front of the list @@ -78,6 +80,8 @@ def get_room(self, x, y) -> str: def update(self): + self.do_tick() + if self.player.is_alive() and not self.player.victory: # check to do any actions diff --git a/inventory.py b/inventory.py index fc81bd9..23e63a4 100644 --- a/inventory.py +++ b/inventory.py @@ -16,18 +16,23 @@ def __init__(self, game: 'Game'): self.game: 'Game' = game - # the list of items in this inventory - self.inventory = [] + # a dict of inventory items and stack amount + self.inventory = {} def has_item(self, name: str): """Return if this inventory has an item (by name).""" - return any([item.name == name for item in self.inventory]) + return any([item.name == name for item in list(self.inventory.keys())]) def give_item(self, *items): """Add items to this inventory.""" - self.inventory += items + for item in items: + if self.inventory.get(item): + self.inventory[item] += 1 # increment the stack + else: + self.inventory[item] = 1 # initialize the stack to 1 + item_str = ', '.join([item.name for item in items]) self.game.add_log(f'{self.name} picked up {item_str}') @@ -51,7 +56,7 @@ def describe_inventory(self) -> str: lines = ["Inventory:"] - for item in self.inventory: - lines += [f"- {item.name}"] + for item, amount in self.inventory.items(): + lines += [f"- {item.name} ({amount})"] return '\n'.join(lines) diff --git a/items.py b/items.py index 1922d18..089e076 100644 --- a/items.py +++ b/items.py @@ -15,9 +15,6 @@ def __init__(self, game, name, description, value, amount=1): # the price of this item (per item) self.value = value - # the amount of this item (stacking) - self.amount = amount - def __str__(self): return "{}\n=====\n{}\nValue: {}\n".format( self.name, self.description, self.amount @@ -28,7 +25,7 @@ class Gold(Item): def __init__(self, game, value): super().__init__( game, - name="Gold", + name=f"{value} Gold", description="A round coin with {} stamped on the front.".format( str(value) ), diff --git a/tiles.py b/tiles.py index 12bb96a..c354ab3 100644 --- a/tiles.py +++ b/tiles.py @@ -71,10 +71,11 @@ def __init__(self, game: 'Game', item): Room.__init__(self, game) self.entities.append(item) - self.item = item def add_loot(self, player: 'Player'): - player.give_item(self.item) + for item in [ent for ent in self.entities if isinstance(ent, items.Item)]: + player.give_item(item) + self.entities.remove(item) def modify_player(self, player): self.add_loot(player) From 76c869b5a6de64be159d324555ca9d6daa44b984 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Wed, 17 Jun 2020 04:04:10 -1000 Subject: [PATCH 17/39] add more metadata for bodyparts --- body.py | 62 +++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/body.py b/body.py index 4087129..09ac19c 100644 --- a/body.py +++ b/body.py @@ -1,39 +1,53 @@ import random +import typing +from dataclasses import dataclass, field + +if typing.TYPE_CHECKING: + from items import Item + + +@dataclass class BodyPart: - def __init__( - self, - name: str, - attachments: list, - main_attachments: list = [], *, - is_vital: bool = False, - weight: float = 100, - ): + # the name of the part + name: str + + # a list of parts (by name) that this part is attached to. + # if this part is deattached, then these parts will be too. + attachments: list = field(default_factory=lambda: []) + + # a list of greater attachments that this part is attached to. + # if any part in this list is deattached, then this part will be + # deattached. + main_attachments: list = field(default_factory=lambda: []) + + # the status of the body part + status: str = 'healthy' - # the name of the part - self.name = name + # the weight used in probability checks. + # the lower the number, the less likely this body part will get + # targeted + weight: float = 100 - # a list of parts (by name) that this part is attached to. - # if this part is deattached, then these parts will be too. - self.attachments = attachments + # if true, will kill the entity instantly if disattached + is_vital: bool = False - # a list of greater attachments that this part is attached to. - # if any part in this list is deattached, then this part will be - # deattached. - self.main_attachments = main_attachments + # can this part hold an item? + can_hold_item: bool = False - # if true, will kill the entity instantly if disattached - self.is_vital = is_vital + # can this part be used to attack? (unarmed) + can_attack_bare: bool = False - # the status of the body part - self.status: str = 'healthy' + # can this part attack with a weapon? + can_attack_armed: bool = False - self.prob_weight: float = weight + # the item this part is holding (if can_hold_item) + held_item: 'typing.Optional[Item]' = None - # TODO: per-part health + # TODO: per-part health class Body: @@ -86,7 +100,7 @@ def pick_part(self): """ parts = self.get_targetable_parts() - weights = [part.prob_weight for part in parts] + weights = [part.weight for part in parts] if len(parts) == 0: return None From 06221b3a6758d0ccbafa7a3096680181118bbb25 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Wed, 17 Jun 2020 04:59:22 -1000 Subject: [PATCH 18/39] implement action register decorator and implement item equipping --- actions.py | 30 +++++++++++++++++++++++++----- body.py | 19 +++++++++++++++++-- game.py | 51 +++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 85 insertions(+), 15 deletions(-) diff --git a/actions.py b/actions.py index 9a151ab..9c24bfe 100644 --- a/actions.py +++ b/actions.py @@ -33,25 +33,45 @@ def format_vector(vec: vec2) -> str: class Action(): """An action that can be taken.""" - def __init__(self, desc: str = None): + def __init__(self, desc: str, key: str): # the description of this action self.desc = desc or "unknown action" + # the hotkey that will run this action + self.key = key + def do_action(self): """Run the action.""" pass + @staticmethod + def register(actions: list, key: str, desc: str = 'a custom action'): + """Decorator that creates an Action and adds it to a list.""" + + def register_inner(func): + + # define a custom class that runs the wrapped fn + class CustomAction(Action): + def __init__(self): + Action.__init__(self, desc, key) + + def do_action(self): + func() + + # add an instance of this class to our list of actions + actions.append(CustomAction()) + + return func + return register_inner + class PlayerAction(Action): """An action that is run on a player.""" def __init__(self, player: Player, key: str, desc: str = None): - super().__init__(desc) - - # the hotkey that will run this action - self.key = key + super().__init__(desc, key) # the player to run this action on self.player: Player = player diff --git a/body.py b/body.py index 09ac19c..5c857d4 100644 --- a/body.py +++ b/body.py @@ -86,6 +86,21 @@ def deattach_part(self, partname: str): self.game.add_log(self.owner.str_possessive + ' ' + partname + ' has flown off !') self.remove_part(bodypart) + def get_equippable_parts(self) -> typing.List[BodyPart]: + """Get all body parts that can hold an item.""" + + return [part for part in self.parts.values() + if part.can_hold_item] + + def equip_item(self, part: BodyPart, item: 'Item') -> bool: + """Set an item to be held by a body part.""" + + if part.can_hold_item: + part.held_item = item + return True + else: + return False + def get_targetable_parts(self): """Get all body parts that can be targeted e.g. not missing.""" @@ -145,10 +160,10 @@ def __init__(self, game, ent): ], is_vital=True, weight=100)) self.add_part(BodyPart("left_arm", ["left_hand"], ["chest"], weight=20)) - self.add_part(BodyPart("left_hand", [], ["left_arm"], weight=5)) + self.add_part(BodyPart("left_hand", [], ["left_arm"], weight=5, can_hold_item=True)) self.add_part(BodyPart("right_arm", ["right_hand"], ["chest"], weight=20)) - self.add_part(BodyPart("right_hand", [], ["right_arm"], weight=5)) + self.add_part(BodyPart("right_hand", [], ["right_arm"], weight=5, can_hold_item=True)) self.add_part(BodyPart("left_leg", ["left_foot"], ["chest"], weight=20)) self.add_part(BodyPart("left_foot", [], ["left_leg"], weight=5)) diff --git a/game.py b/game.py index 13b5850..8de211d 100644 --- a/game.py +++ b/game.py @@ -8,6 +8,8 @@ from vector import vec2 from player import Player +Action = actions.Action + @dataclass class Message(): @@ -41,6 +43,13 @@ def __init__(self): self.player.pos = vec2(2, 4) + # the state of the action menu + # tells the game what actions to display + self.menu_state = 'main' + + # holds an item previously selected through the menu + self.menu_selected_item = None + # update the current room self.update_room() @@ -102,16 +111,42 @@ def get_actions(self) -> list: action_list = [] - # get movement actions - action_list += self.get_movement_actions() # get movement actions + if self.menu_state == 'main': - # get room actions - if self.room: - action_list += self.room.get_actions(self.player) # get room actions + # get movement actions + action_list += self.get_movement_actions() # get movement actions + + # get room actions + if self.room: + action_list += self.room.get_actions(self.player) # get room actions + + # debug actions + action_list.append(actions.CheckBodyAction(self.player)) + action_list.append(actions.CheckInventory(self.player)) + + @Action.register(action_list, 'e', 'equip an item') + def _equip(): + self.menu_state = 'equip_item' + + elif self.menu_state == 'equip_item': + + for i, item in enumerate(list(self.player.inventory.keys())): + + @Action.register(action_list, str(i+1), f'select {item.name}') + def _select_item(item=item): + self.menu_selected_item = item + self.menu_state = 'equip_part' + + elif self.menu_state == 'equip_part': + + for i, part in enumerate(self.player.body.get_equippable_parts()): - # debug actions - action_list.append(actions.CheckBodyAction(self.player)) - action_list.append(actions.CheckInventory(self.player)) + @Action.register(action_list, str(i+1), f'select {part.name}') + def _action(part=part): + item = self.menu_selected_item + self.add_log(f'your {part.name} is now holding {item.name}') + self.menu_selected_item = None + self.menu_state = 'main' return action_list From 5f884288b5707a9d9e147fe7effe6fa2772e3019 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Wed, 17 Jun 2020 05:32:33 -1000 Subject: [PATCH 19/39] start command system --- actions.py | 61 ++++++++++++++++++++++++++++++++++++++++++------- game.py | 66 ++++++++++++++++++++++++++++++++++++++++++++++-------- main.py | 8 +++++++ 3 files changed, 118 insertions(+), 17 deletions(-) diff --git a/actions.py b/actions.py index 9c24bfe..ca4d914 100644 --- a/actions.py +++ b/actions.py @@ -33,18 +33,26 @@ def format_vector(vec: vec2) -> str: class Action(): """An action that can be taken.""" - def __init__(self, desc: str, key: str): - + def __init__(self, desc: str): # the description of this action self.desc = desc or "unknown action" - # the hotkey that will run this action - self.key = key - def do_action(self): """Run the action.""" pass + + +class QuickAction(Action): + """An action that is bound to a hotkey.""" + + def __init__(self, key: str, desc: str): + Action.__init__(self, desc) + + # the hotkey that will run this action + self.key = key + + @staticmethod def register(actions: list, key: str, desc: str = 'a custom action'): """Decorator that creates an Action and adds it to a list.""" @@ -52,9 +60,9 @@ def register(actions: list, key: str, desc: str = 'a custom action'): def register_inner(func): # define a custom class that runs the wrapped fn - class CustomAction(Action): + class CustomAction(QuickAction): def __init__(self): - Action.__init__(self, desc, key) + QuickAction.__init__(self, key, desc) def do_action(self): func() @@ -65,13 +73,50 @@ def do_action(self): return func return register_inner +class CommandAction(Action): + """An action that is bound to a command.""" + + def __init__(self, terms: list, desc: str): + Action.__init__(self, desc) + + # the list of command terms that will run this action + self.terms = terms + + def do_command(self, *args): + pass + + def do_action(self): + self.do_command() + + @staticmethod + def register(actions: list, terms: list, desc: str = 'a custom action'): + """Decorator that creates an Action and adds it to a list.""" + + def register_inner(func): + + # define a custom class that runs the wrapped fn + class CustomAction(CommandAction): + def __init__(self): + CommandAction.__init__(self, terms, desc) + + def do_command(self, *args): + func(*args) + + # add an instance of this class to our list of actions + actions.append(CustomAction()) + + return func + return register_inner + class PlayerAction(Action): """An action that is run on a player.""" def __init__(self, player: Player, key: str, desc: str = None): - super().__init__(desc, key) + super().__init__(desc) + + self.key = key # the player to run this action on self.player: Player = player diff --git a/game.py b/game.py index 8de211d..f824273 100644 --- a/game.py +++ b/game.py @@ -8,7 +8,8 @@ from vector import vec2 from player import Player -Action = actions.Action +QuickAction = actions.QuickAction +CommandAction = actions.CommandAction @dataclass @@ -50,12 +51,18 @@ def __init__(self): # holds an item previously selected through the menu self.menu_selected_item = None + # holds the command string that is being typed by the user + self.menu_cmd_buffer = '' + + # if true, try to quit the game + self.quit = False + # update the current room - self.update_room() + self.set_current_room() print('loaded a new game') - def update_room(self): + def set_current_room(self): # self.add_log(f'retrieving room @ { self.player.pos }', 8) # get the tile at the current position @@ -89,17 +96,58 @@ def get_room(self, x, y) -> str: def update(self): - self.do_tick() + if self.menu_state == 'cmd': + key = self.next_key_press + # add the last pressed key to the cmd buffer + if key == 'KEY_BACKSPACE': + # remove last character + self.menu_cmd_buffer = self.menu_cmd_buffer[:-1] + elif key == '\n': + # TODO: run a command + self.add_log('running command: ' + self.menu_cmd_buffer) + self.run_command(self.menu_cmd_buffer) + self.menu_cmd_buffer = '' + self.menu_state = 'main' + else: + self.menu_cmd_buffer += self.next_key_press - if self.player.is_alive() and not self.player.victory: + elif self.next_key_press == ':': + # set the menu to cmd mode + self.menu_state = 'cmd' + elif self.player.is_alive() and not self.player.victory: # check to do any actions for action in self.get_actions(): if self.next_key_press == action.key: action.do_action() + self.do_tick() + self.set_current_room() break - self.update_room() + def get_commands(self) -> list: + """Get all commands that the player can do.""" + + commands = [] + + @CommandAction.register(commands, ['help'], 'get a list of commands') + def _cmd_help(*args): + self.add_log('There is no help.') + + @CommandAction.register(commands, ['quit'], 'quit the game') + def _cmd_quit(*args): + self.quit = True + + return commands + + def run_command(self, cmd: str): + + args = cmd.split(' ') + root = args[0] + args = args[1:] + + for command in self.get_commands(): + if root in command.terms: + command.do_command(args) def get_movement_actions(self) -> list: """Get all movement actions that the player can do.""" @@ -124,7 +172,7 @@ def get_actions(self) -> list: action_list.append(actions.CheckBodyAction(self.player)) action_list.append(actions.CheckInventory(self.player)) - @Action.register(action_list, 'e', 'equip an item') + @QuickAction.register(action_list, 'e', 'equip an item') def _equip(): self.menu_state = 'equip_item' @@ -132,7 +180,7 @@ def _equip(): for i, item in enumerate(list(self.player.inventory.keys())): - @Action.register(action_list, str(i+1), f'select {item.name}') + @QuickAction.register(action_list, str(i+1), f'select {item.name}') def _select_item(item=item): self.menu_selected_item = item self.menu_state = 'equip_part' @@ -141,7 +189,7 @@ def _select_item(item=item): for i, part in enumerate(self.player.body.get_equippable_parts()): - @Action.register(action_list, str(i+1), f'select {part.name}') + @QuickAction.register(action_list, str(i+1), f'select {part.name}') def _action(part=part): item = self.menu_selected_item self.add_log(f'your {part.name} is now holding {item.name}') diff --git a/main.py b/main.py index 8025669..b913de9 100644 --- a/main.py +++ b/main.py @@ -27,6 +27,10 @@ def on_init(scr: CursesScreen): @scr.render def on_render(scr: CursesScreen, key: str): + if game.quit: + scr.end() + return + global map_view game.send_key_press(key) @@ -56,6 +60,10 @@ def on_render(scr: CursesScreen, key: str): scr.draw_text(0, height-1, 'Health: ' + str(game.player.get_health()), fg=0, bg=9) + # draw command buffer + if game.menu_state == 'cmd': + scr.draw_text(12, height-1, ':' + game.menu_cmd_buffer, 'standout') + scr.set_view(map_view) # map_view.box() From 34065cb990c8ef7801ef2ec677688281b80134a3 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Fri, 19 Jun 2020 18:38:01 -1000 Subject: [PATCH 20/39] enable nodelay on curses --- screen.py | 50 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/screen.py b/screen.py index 13c68f4..c40f941 100644 --- a/screen.py +++ b/screen.py @@ -1,6 +1,7 @@ import curses import traceback +import time class CursesScreen: @@ -19,11 +20,17 @@ def __init__(self): # a map of colors self.colors = {} + # if false, stop the render loop + self.running = True + # called on every frame - self.on_render = lambda: None + self.on_render = lambda *args: None # called before the render loop starts - self.on_init = lambda: None + self.on_init = lambda *args: None + + # called when a key is pressed + self.on_key_pressed = lambda *args: None def render(self, func): """Set the function used to render a frame.""" @@ -35,6 +42,11 @@ def init(self, func): self.on_init = func return func + def key_pressed(self, func): + """Set the method called before the render loop.""" + self.on_key_pressed = func + return func + def set_view(self, view=None) -> None: """Set the active view. @@ -49,11 +61,15 @@ def set_view(self, view=None) -> None: def read_input(self) -> str: """Read input from the user. - This method will wait until the user presses a key. The string - representation of that key will be returned. + If the user has pressed a key, the string representation of that key + will be returned. + If not, then an empty string will be returned. """ - return self.stdscr.getkey() + try: + return self.stdscr.getkey() + except Exception: + return '' def create_view(self, x, y, width, height): """Create a new view.""" @@ -92,6 +108,7 @@ def start(self): self.stdscr = curses.initscr() curses.noecho() curses.cbreak() + self.stdscr.nodelay(True) self.stdscr.keypad(True) # initialize colors (16 colors) @@ -116,13 +133,22 @@ def start(self): key = None set_view = self.set_view - while True: + frate = (1.0 / 60.0) + last_ftime = time.time() + + while self.running: try: + + key = self.read_input() + if key == "Q": # debug quit key self.end() break + elif key != '': + self.on_key_pressed(self, key) + # clear screen # ------------ @@ -144,7 +170,16 @@ def start(self): for view in self.views: view.refresh() - key = self.stdscr.getkey() + # framerate limiter + + ftime = time.time() + if (ftime - last_ftime) < frate: + time.sleep(frate - (ftime - last_ftime)) + + last_ftime = time.time() + + # read input (blocks) + # key = self.read_input() except Exception: @@ -181,6 +216,7 @@ def draw_outline(self): self.view.box() def end(self): + self.running = False # shut down terminal curses.nocbreak() self.stdscr.keypad(False) From e49657a5c78c0b2b77f86b3eeaa85c72c8912741 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Fri, 19 Jun 2020 18:58:07 -1000 Subject: [PATCH 21/39] add fps calculation --- screen.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/screen.py b/screen.py index c40f941..3bc0d84 100644 --- a/screen.py +++ b/screen.py @@ -23,6 +23,8 @@ def __init__(self): # if false, stop the render loop self.running = True + self.fps = 0.0 + # called on every frame self.on_render = lambda *args: None @@ -170,13 +172,16 @@ def start(self): for view in self.views: view.refresh() - # framerate limiter - ftime = time.time() + + # framerate limiter if (ftime - last_ftime) < frate: time.sleep(frate - (ftime - last_ftime)) + ftime = time.time() + + self.fps = 1.0 / (ftime - last_ftime) - last_ftime = time.time() + last_ftime = ftime # read input (blocks) # key = self.read_input() From 26fc1e0b0b7b3be0ced3d99bcfa492eb9bfc1976 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Fri, 19 Jun 2020 18:58:38 -1000 Subject: [PATCH 22/39] add run task (vim-asynctasks) --- .tasks | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .tasks diff --git a/.tasks b/.tasks new file mode 100644 index 0000000..6ff54b7 --- /dev/null +++ b/.tasks @@ -0,0 +1,5 @@ + +[run] +command=termite --hold -e "bash -lic \"python main.py\"" +cwd=. +output=terminal From 19ca1f637f03b7e79273c43fbbc74b8cc5c4bcb4 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Fri, 19 Jun 2020 19:00:34 -1000 Subject: [PATCH 23/39] implement turn system, redesign input logic for player, add create_action function --- mixins/__init__.py | 0 mixins/logger.py | 39 +++++++++++++++++++++++++++++++ turns.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 mixins/__init__.py create mode 100644 mixins/logger.py create mode 100644 turns.py diff --git a/mixins/__init__.py b/mixins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mixins/logger.py b/mixins/logger.py new file mode 100644 index 0000000..3a345ba --- /dev/null +++ b/mixins/logger.py @@ -0,0 +1,39 @@ + +from dataclasses import dataclass +from typing import List + + +@dataclass +class Message(): + text: str + style: str = 'normal' + fg: int = 0 + bg: int = 0 + + +class Logger: + """An object that can log messages.""" + + def __init__(self, log_limit: int = 5): + + # the max amount of messages to display + self.log_limit = log_limit + + # contains all the logged messages + self.log_messages: List[Message] = [] + + def get_log(self) -> List[Message]: + return self.log_messages + + def log(self, text: str, fg=15, bg=0, style='normal'): + """Log a message.""" + + # insert to the front of the list + self.log_messages.insert(0, Message(text, style, fg, bg)) + + # delete last message if above log_limit + if len(self.log_messages) > self.log_limit: + del self.log_messages[self.log_limit] + + def info(self, text: str): + self.log(text, 8) # dark gray diff --git a/turns.py b/turns.py new file mode 100644 index 0000000..f660e84 --- /dev/null +++ b/turns.py @@ -0,0 +1,58 @@ + +from entity import LivingEntity +from typing import ( + List, + TYPE_CHECKING +) + + +if TYPE_CHECKING: + from game import Game + + +class TurnManager: + """A manager for the turn order of entities.""" + + def __init__(self, game: 'Game', ents: 'List[LivingEntity]'): + + self.game = game + self.ents = ents + self.order = [] + self.idx = 0 + self.last_idx = 0 + + def start_order(self): + """Decide the turn order of the given list of entities.""" + + # for now just use the same order as the list + order = [ent for ent in self.ents + if isinstance(ent, LivingEntity)] + + # if len(order): + # self.game.info(f'** order: {[ent.name for ent in order]} **') + + self.idx = 0 + self.last_idx = 0 + self.order = order + + def get_last_entity(self) -> LivingEntity: + return self.order[self.last_idx] + + def get_current_entity(self) -> LivingEntity: + return self.order[self.idx] + + def run_next(self): + """Run the next turn in the order.""" + + if len(self.order) > 0: + + ent = self.order[self.idx] + + if ent.is_alive(): + # self.game.info(f'it is now {ent.name} turn') + action = ent.think() + if action: action.do_action() + + # increment index + self.last_idx = self.idx + self.idx = (self.idx + 1) % len(self.order) From b81e72163901c62473168088c49343a18a976fa0 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Sat, 20 Jun 2020 06:29:22 -1000 Subject: [PATCH 24/39] start design document --- design.md | 261 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 design.md diff --git a/design.md b/design.md new file mode 100644 index 0000000..8e4af01 --- /dev/null +++ b/design.md @@ -0,0 +1,261 @@ +A text-based RPG. + +Gameplay +-------- + +The game will be designed to be turn-based and take place in a system made up of +rooms. + +- On the player's turn, they can perform an action. There will be two different + types of actions: "quick actions" triggered by pressing the key assigned to + that action, and "command actions" triggered by pressing a command entry + button (:) and typing a command. + +- On an NPC's (or enemy's) turn, they can think() in order to decide what action + to do, and afterwards will trigger that action. + +Turn Order +---------- + +When around other NPCs, a turn order will be decided. On an NPC's turn, they +will decide what action to do, and on the player's turn, a list of avaliable +actions will be displayed before prompting the player for input. + +Logic Loop +----------- + +On enter new room: init turn order + +For NPC turn => run think() to decide action then do action (takes 1 game tick) +For player turn => change gamestate to "input", process player input while in +this state, do decided action, then change gamestate to "running" + +Body Part System +---------------- + +All creatures have a body made up of a set of body parts. Internally this +is defined as a list of part names, each with references to what other body +parts they are connected to. + +A body part has different flags that define how it can be used and how it +behaves. + +`status: str`: the state of the part, + - `'healthy'`: the body part is functioning normally. + - `'missing'`: the body part is no longer attached to the rest of the body. +The body part cannot be targeted if it is missing. + - `'burnt'` + - `'poisoned'` + - `'mangled'`: the body part is attached but no longer functions. +`weight`: the weight to use in probability checks (not actual weight). +These weights are used to determine a targeted body part when an enemy attacks +blindly. +`is_vital`: if true, damaging/removing this part will kill the entity +instantly. +`can_hold_item`: if true, is able to hold items i.e. hands +`can_attack_unarmed`: if true, is able to attack unarmed i.e. hands and feet +`can_attack_armed`: if true, is able to attack armed i.e. hands + +Creature Stats +-------------- + +All creatures have a set of stats which can be used when processing certain +actions. In general, `0` indicates no skill in that stat, `10` indicates average +skill, and `20` indicates professional skill. + +NOTE: right now there is no upper limit for a stat. + +For now the list of stats are similar to DnD. + +- `STR`: strength - used to determine power when attacking +- `DEX`: dexterity - used to determine profeciency with weapons and the result + of certain actions such as dodging or jumping +- `CON`: constituition - used to determine effect of getting hit +- `INT`: intelligence - used to determine result of reading or examining + objects, and also effeciency when talking to NPCs +- `WIS`: wisdom - used to determine general awareness of the sitation, to + determine results of looking around (or whether to try to look around) +- `CHR`: charisma - used to determine effect when talking to NPCs, possibly to + persuade, barter, etc. + +Stat Checks +----------- + +Some actions may require checks against one or more stats. These actions will +provide a "difficulty" for each checked stat to determine the stats needed to +successfully perform the action. + +#### Check Algorithm #### + +When a check is needed for an action (`difficulty` is set by the action): + +(?) The base value of the stat is added to two D6 rolls. + +`(base stat) + (2D6) = (total stat)` + +For example, if the action is to jump a small gap with the difficulty being +`{DEX: 5}`, and the player's base DEX stat is `2`, and the result of two D6 +rolls are a `4` and `3`, then the `total stat` is: + +`2 + 4 + 3 = 9` + +and since `9 >= 5`, the player passes the check. + +(?) Catastrophic Failure: if a player rolls snake eyes +(double 1s => a 1/36 chance of 2.778%), then the `total stat` will be half the +base stat. + +#### Check Results #### + +The result are a set of _success rates_ (percentages) which can be used to +determine how successful the check was. + +If a result is needed to check if an action passes/fails, then check if +`(success rate) >= 1.0`. + +If a result is needed to check the effectiveness of an action, then use the +success rate on its own. (`1.0 => 100% effectiveness, 2.0 => 200% +effectiveness`) + + +Items +----- + +Items are a type of entity and represent any object that can be picked up, held, +wielded, worn, etc. + +All items carry this metadata: + +`name`: the actual name of the item i.e `Gold Coin` +`description`: the description of this item (what does this item look like?) +i.e. `A flat, round object made of gold` +`explanation`: an explanation of this item (what is the purpose of this item?) +i.e. `Used as currency` +`value`: the price value per item i.e. `1 Gold (or whichever +currency system is being used)` + +examine stats: + +`describe_req`: the INT required to describe this object +`explain_req`: the INT required to explain this object + +damage-related stats: + +`phys_damage`: the damage that this item can deal if wielded +`proj_damage`: the damage that this item can deal if thrown/shot + +`is_blunt`: if true, this item can deal `blunt force` damage +`is_sharp`: if true, this item can `cut` or `slice` +`is_pointed`: if true, this item can `stab` + +Action: Examining +----------------- + +[`examine`, `ex`, `x`] + +Requirements: `INT` + +Attempt to examine something. + +Examining an entity will show the description and explanation of it if `INT >= +item.explain_req`. +Examining an entity will show only the description if `INT >= item.describe_req`. +Examining an entity will show `"You fail to identify what this object is"` if +`INT < item.describe_req`. + +- `examine `: try to examine this object + +Action: Looking +--------------- + +[`look`, `l`] + +Requirements: `INT` + +Attempt to look around the room. Can be affected by lighting conditions and/or +visibility of entities. + +Action: Attacking +----------------- + +[`attack`, `k`] + +Requirements: `STR`, `DEX`, `CON` + +Attack an entity physically i.e. swinging a sword or punching + +`STR` and `DEX` are checked (when attacking) to determine the damage output and +accuracy. +`DEX` is checked (when being attacked) to determine if the attack can be dodged. +`CON` is checked (when getting hit) to determine the damage taken. + +- `attack blindly... `: will attack an enemy at a random part (easy roll) +- `attack precisely... ... `: will attack an enemy at a specific + part (harder roll, but easier to hit a specific part) + +Inanimate objects (like items) can be attacked with mixed results. + +Action: Throw +------------- + +[`throw`, `shoot`] + +Requirements: `DEX` + +Throw or shoot something. Counts as a ranged attack. + +`STR` and `DEX` are checked (when attacking) to determine the damage output and +accuracy. +`DEX` is checked (when being attacked) to determine if the attack can be dodged. +`CON` is checked (when getting hit) to determine the damage taken. + +- `throw... ... `: will throw or shoot an object at an entity. + +NOTE: `shooting` an object that is a tool to shoot ammo will instead shoot the +ammo. + +Action: Open +------------ + +[`open`] + +Requirements: `STR`, `INT`, `WIS` + +Open something, like a door, chest, etc. + +`INT` is checked to identify that the object can be opened. +(on fail, assume it can) +`WIS` is checked to identify if the object should be opened. (if rigged etc.) +(on fail, open it anyway) +If locked, `INT` is checked to see if it can be picked. +(otherwise, try `attacking` the object instead) +`STR` is checked to open the object. (might be high for heavy objects like stone +doors etc.) + +(?) Creatures can be "opened" with mixed results. + +Action: Jump +------------ + +[`jump`, `leap`, `cross`] + +Requirements: `INT`, `DEX` + +Jump a gap or similar obstacle. Also extends to navigating traps, etc. + +`DEX` is checked to determine success of navigating the obstacle. + +Action: Scream +-------------- + +[`scream`, `yell`, `scare`] + +Requirements: `CHR` + +Scream. + +Screaming will make anything living notice you. +`CHR` is checked to determine if it will also scare them. + + + From 0540b458dab7668fce3b9d1a5bdd4ace6c5d9b58 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Sun, 21 Jun 2020 05:11:00 -1000 Subject: [PATCH 25/39] various updates --- actions.py | 55 +++++++++++++++++++++----- body.py | 30 ++++++++++----- enemies.py | 30 +++++++-------- entity.py | 52 ++++++++++++++++++++++++- game.py | 106 +++++++++++++++++++++++++++++++++------------------ inventory.py | 2 +- items.py | 3 ++ main.py | 27 +++++++++---- player.py | 24 +++--------- tiles.py | 9 +++-- 10 files changed, 233 insertions(+), 105 deletions(-) diff --git a/actions.py b/actions.py index ca4d914..7da2a33 100644 --- a/actions.py +++ b/actions.py @@ -1,7 +1,13 @@ #!/usr/bin/python3 env -from player import Player from vector import vec2 +from typing import ( + TYPE_CHECKING, + Callable +) + +if TYPE_CHECKING: + from player import Player def format_actions(actions: list) -> str: @@ -42,7 +48,6 @@ def do_action(self): pass - class QuickAction(Action): """An action that is bound to a hotkey.""" @@ -52,7 +57,6 @@ def __init__(self, key: str, desc: str): # the hotkey that will run this action self.key = key - @staticmethod def register(actions: list, key: str, desc: str = 'a custom action'): """Decorator that creates an Action and adds it to a list.""" @@ -73,6 +77,7 @@ def do_action(self): return func return register_inner + class CommandAction(Action): """An action that is bound to a command.""" @@ -109,17 +114,49 @@ def do_command(self, *args): return register_inner +def create_action(desc: str, fn: Callable, key: str = '', terms: list = []) -> Action: + """Create a new action.""" + if key: + # make a quick action + class CustomAction(QuickAction): + def __init__(self): + super().__init__(key, desc) + + def do_action(self): + fn() + + elif terms: + # make a command action + class CustomAction(CommandAction): + def __init__(self): + super().__init__(terms, desc) + + def do_command(self, *args): + fn(*args) + + else: + # make a normal action + class CustomAction(Action): + def __init__(self): + super().__init__(desc) + + def do_action(self): + fn() + + return CustomAction() + + class PlayerAction(Action): """An action that is run on a player.""" - def __init__(self, player: Player, key: str, desc: str = None): + def __init__(self, player: 'Player', key: str, desc: str): super().__init__(desc) self.key = key # the player to run this action on - self.player: Player = player + self.player = player class MoveUp(PlayerAction): @@ -160,7 +197,7 @@ def do_action(self): class Attack(PlayerAction): - def __init__(self, player: Player, enemy): + def __init__(self, player: 'Player', enemy): super().__init__(player, 'k', 'Attack') self.enemy = enemy @@ -170,7 +207,7 @@ def do_action(self): class Flee(PlayerAction): - def __init__(self, player: Player): + def __init__(self, player: 'Player'): super().__init__(player, 'j', 'Flee!') def do_action(self): @@ -185,7 +222,7 @@ def __init__(self, player): def do_action(self): desc = self.player.describe_inventory() - self.player.game.add_log(desc) + self.player.game.log(desc) class CheckBodyAction(PlayerAction): @@ -199,4 +236,4 @@ def do_action(self): for part in self.player.body.parts.values(): lines.append(f'{part.name} ({ part.status }) -> {part.attachments}') - self.player.game.add_log('\n'.join(lines)) + self.player.game.log('\n'.join(lines)) diff --git a/body.py b/body.py index 5c857d4..bfd54de 100644 --- a/body.py +++ b/body.py @@ -1,11 +1,16 @@ import random -import typing from dataclasses import dataclass, field +from typing import ( + TYPE_CHECKING, + Optional, + List, + Dict +) -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from items import Item @@ -45,7 +50,7 @@ class BodyPart: can_attack_armed: bool = False # the item this part is holding (if can_hold_item) - held_item: 'typing.Optional[Item]' = None + held_item: 'Optional[Item]' = None # TODO: per-part health @@ -61,13 +66,14 @@ def __init__(self, game, ent): # the entity that this body belongs to self.owner = ent - self.parts = {} + self.parts: Dict[str, BodyPart] = {} self.max_health = 100 self.health = self.max_health - self.is_alive = True + def is_alive(self) -> bool: + return self.health > 0 def get_health_perc(self): """Get the health of the body as a percentage.""" @@ -83,10 +89,10 @@ def deattach_part(self, partname: str): bodypart = self.parts.get(partname) if bodypart: - self.game.add_log(self.owner.str_possessive + ' ' + partname + ' has flown off !') + self.game.log(self.owner.str_possessive + ' ' + partname + ' has flown off !') self.remove_part(bodypart) - def get_equippable_parts(self) -> typing.List[BodyPart]: + def get_equippable_parts(self) -> List[BodyPart]: """Get all body parts that can hold an item.""" return [part for part in self.parts.values() @@ -139,7 +145,7 @@ def hurt(self, damage: int, bodypart=None): bodypart = bodypart or self.pick_part() if bodypart is None: - self.game.add_log('No body part to target!') + self.game.log('No body part to target!') else: # self.deattach_part(bodypart.name) @@ -160,10 +166,14 @@ def __init__(self, game, ent): ], is_vital=True, weight=100)) self.add_part(BodyPart("left_arm", ["left_hand"], ["chest"], weight=20)) - self.add_part(BodyPart("left_hand", [], ["left_arm"], weight=5, can_hold_item=True)) + self.add_part(BodyPart("left_hand", [], ["left_arm"], weight=5, + can_attack_armed=True, can_attack_bare=True, can_hold_item=True + )) self.add_part(BodyPart("right_arm", ["right_hand"], ["chest"], weight=20)) - self.add_part(BodyPart("right_hand", [], ["right_arm"], weight=5, can_hold_item=True)) + self.add_part(BodyPart("right_hand", [], ["right_arm"], weight=5, + can_attack_armed=True, can_attack_bare=True, can_hold_item=True + )) self.add_part(BodyPart("left_leg", ["left_foot"], ["chest"], weight=20)) self.add_part(BodyPart("left_foot", [], ["left_leg"], weight=5)) diff --git a/enemies.py b/enemies.py index 620bea3..87de5d7 100644 --- a/enemies.py +++ b/enemies.py @@ -1,36 +1,32 @@ #!/usr/bin/python3 env +from entity import LivingEntity -class Enemy: - def __init__(self, name, hp, damage): - self.name = name - self.hp = hp - self.damage = damage +class Enemy(LivingEntity): + + def __init__(self, game, name, damage): - def is_alive(self): - return self.hp > 0 + super().__init__(game, name) - def hurt(self, damage): - self.hp -= damage + self.damage = damage - def get_health(self): - return self.hp + #TODO: be able to set HP class GiantSpider(Enemy): - def __init__(self): - super().__init__( + def __init__(self, game): + super().__init__(game, name='Giant Spider', - hp=10, + # hp=10, damage=2 ) class Ogre(Enemy): - def __init__(self): - super().__init__( + def __init__(self, game): + super().__init__(game, name='Ogre', - hp=30, + # hp=30, damage=15 ) diff --git a/entity.py b/entity.py index 48ab94f..883dbec 100644 --- a/entity.py +++ b/entity.py @@ -1,6 +1,8 @@ import typing import body +import random +from actions import Action, create_action if typing.TYPE_CHECKING: @@ -46,4 +48,52 @@ def get_health(self) -> int: return self.body.health def is_alive(self) -> int: - return self.body.is_alive + return self.body.is_alive() + + def attack(self, ent: Entity): + """Attack an entity.""" + + # get body parts that can attack + parts = [part for part in list(self.body.parts.values()) + if part.can_attack_bare or + (part.can_attack_armed and part.held_item)] + + if len(parts) == 0: + self.game.log( + f'{self.name} tries to attack, but has nothing to attack with!' + ) + return + + # TODO: be able to choose a specific way to attack + # for now pick one at random + part = random.choice(parts) + + # calculate damage to deal + + dmg = 1 + if part.can_attack_armed and part.held_item: + self.game.log( + f'{self.name} attacks {ent.name} with {part.name} holding {part.held_item.name}!' + ) + dmg = part.held_item.damage + elif part.can_attack_bare: + self.game.log( + f'{self.name} attacks {ent.name} with the {part.name}!' + ) + # TODO: implement unarmed damage stat + + # calculate effect of damage + + if isinstance(ent, LivingEntity): + hurt_part = ent.hurt(dmg) + self.game.log(f'{self.name} does {dmg} damage to the {hurt_part.name}!') + else: + self.game.log('It does nothing!') + + def think(self) -> 'Action': + """Decide an action to take for this entity's turn.""" + + def _do_nothing(): + self.game.log(f'{self.name} is doing nothing.') + + return create_action('do nothing', _do_nothing) diff --git a/game.py b/game.py index f824273..1d76bce 100644 --- a/game.py +++ b/game.py @@ -2,39 +2,41 @@ import world import actions +import typing from tiles import Room +from turns import TurnManager from dataclasses import dataclass from vector import vec2 from player import Player +from mixins.logger import Logger QuickAction = actions.QuickAction CommandAction = actions.CommandAction -@dataclass -class Message(): - text: str - style: str = 'normal' - fg: int = 0 - bg: int = 0 +if typing.TYPE_CHECKING: + from screen import CursesScreen -class Game(): +class Game(Logger): - def __init__(self): + def __init__(self, scr: 'CursesScreen'): + + Logger.__init__(self) + + self.scr: 'CursesScreen' = scr # a string describing the current situation self.status = '' # the next key to read from the user - self.next_key_press = "" + self.input_key = "" # the current room the player is in self.room = None - # list of log messages - self.log = [] + self.turns = None # load the world self.world = world.parse_world(self) @@ -44,6 +46,11 @@ def __init__(self): self.player.pos = vec2(2, 4) + # the state of the game + # 'input' -> game is waiting for player input + # 'running' -> time is progressing + self.game_state = 'input' + # the state of the action menu # tells the game what actions to display self.menu_state = 'main' @@ -64,9 +71,11 @@ def __init__(self): def set_current_room(self): - # self.add_log(f'retrieving room @ { self.player.pos }', 8) + # self.log(f'retrieving room @ { self.player.pos }', 8) # get the tile at the current position self.room = self.world.get_room_at(self.player.pos) + self.turns = TurnManager(self, self.room.entities + [self.player]) + self.turns.start_order() # TODO: Room.intro_text() is deprecated @@ -76,54 +85,52 @@ def do_tick(self): if self.room: self.room.modify_player(self.player) - def add_log(self, msg, fg=15, bg=0, style='normal'): - - # insert to the front of the list - self.log.insert(0, Message(msg, style, fg, bg)) - if len(self.log) > 10: - del self.log[10] - - def send_key_press(self, key): + def on_key_pressed(self, key): """Set the next key to input and update the game.""" - self.next_key_press = key - self.update() - - def get_room(self, x, y) -> str: - """Get a room by its coordinates.""" - - return self.world.get_room_at(vec2(x, y)) - - def update(self): + if self.game_state != 'input': return # noqa: E701 if self.menu_state == 'cmd': - key = self.next_key_press # add the last pressed key to the cmd buffer if key == 'KEY_BACKSPACE': # remove last character self.menu_cmd_buffer = self.menu_cmd_buffer[:-1] elif key == '\n': # TODO: run a command - self.add_log('running command: ' + self.menu_cmd_buffer) + # self.log('running command: ' + self.menu_cmd_buffer) self.run_command(self.menu_cmd_buffer) self.menu_cmd_buffer = '' self.menu_state = 'main' else: - self.menu_cmd_buffer += self.next_key_press + self.menu_cmd_buffer += self.input_key - elif self.next_key_press == ':': + elif key == ':': # set the menu to cmd mode self.menu_state = 'cmd' elif self.player.is_alive() and not self.player.victory: # check to do any actions for action in self.get_actions(): - if self.next_key_press == action.key: + if key == action.key: action.do_action() - self.do_tick() self.set_current_room() + # resume game + self.game_state = 'running' + # self.do_tick() break + def get_room(self, x, y) -> str: + """Get a room by its coordinates.""" + + return self.world.get_room_at(vec2(x, y)) + + def update(self): + """Update the game.""" + + if self.game_state == 'running': + # run next game tick + self.turns.run_next() + def get_commands(self) -> list: """Get all commands that the player can do.""" @@ -131,7 +138,7 @@ def get_commands(self) -> list: @CommandAction.register(commands, ['help'], 'get a list of commands') def _cmd_help(*args): - self.add_log('There is no help.') + self.log('There is no help.') @CommandAction.register(commands, ['quit'], 'quit the game') def _cmd_quit(*args): @@ -155,10 +162,14 @@ def get_movement_actions(self) -> list: def get_actions(self) -> list: """Get all actions that the player can do.""" - # self.add_log("getting actions...") + # self.log("getting actions...") action_list = [] + if self.game_state == 'running': + # game is running; no actions to do now + return action_list + if self.menu_state == 'main': # get movement actions @@ -176,6 +187,10 @@ def get_actions(self) -> list: def _equip(): self.menu_state = 'equip_item' + @QuickAction.register(action_list, 'k', 'attack') + def _attack(): + self.menu_state = 'attack' + elif self.menu_state == 'equip_item': for i, item in enumerate(list(self.player.inventory.keys())): @@ -192,10 +207,25 @@ def _select_item(item=item): @QuickAction.register(action_list, str(i+1), f'select {part.name}') def _action(part=part): item = self.menu_selected_item - self.add_log(f'your {part.name} is now holding {item.name}') + self.log(f'your {part.name} is now holding {item.name}') + part.held_item = item self.menu_selected_item = None self.menu_state = 'main' + elif self.menu_state == 'attack': + + @QuickAction.register(action_list, '1', 'yourself') + def _self_attack(): + self.player.attack(self.player) + self.menu_state = 'main' + + for i, ent in enumerate(self.room.entities): + + @QuickAction.register(action_list, str(i+2), f'{ent.name}') + def _attack(ent=ent): + self.player.attack(ent) + self.menu_state = 'main' + return action_list def get_description(self, skill_check: int = 10): diff --git a/inventory.py b/inventory.py index 23e63a4..eebd23c 100644 --- a/inventory.py +++ b/inventory.py @@ -34,7 +34,7 @@ def give_item(self, *items): self.inventory[item] = 1 # initialize the stack to 1 item_str = ', '.join([item.name for item in items]) - self.game.add_log(f'{self.name} picked up {item_str}') + self.game.log(f'{self.name} picked up {item_str}') def remove_item(self, name: str): """Remove an item from this inventory. diff --git a/items.py b/items.py index 089e076..681c82e 100644 --- a/items.py +++ b/items.py @@ -15,6 +15,9 @@ def __init__(self, game, name, description, value, amount=1): # the price of this item (per item) self.value = value + # damage when attacking + self.damage = 1 + def __str__(self): return "{}\n=====\n{}\nValue: {}\n".format( self.name, self.description, self.amount diff --git a/main.py b/main.py index b913de9..c6ef316 100644 --- a/main.py +++ b/main.py @@ -9,7 +9,7 @@ scr = CursesScreen() -game = Game() +game = Game(scr) map_size = vec2(10, 10) # map_center = map_size // 2 @@ -33,7 +33,8 @@ def on_render(scr: CursesScreen, key: str): global map_view - game.send_key_press(key) + game.on_key_pressed(key) + game.update() width, height = scr.get_size() @@ -42,17 +43,19 @@ def on_render(scr: CursesScreen, key: str): scr.draw_text(0, 0, title.center(width), style='standout') scr.draw_text(21, 2, ( "Location: " + str(game.player.pos) + "\n" - "Last Keypress: " + str(key) + "Last Keypress: " + str(key) + "\n" + "State: " + game.game_state )) - desc = re.sub(' +', ' ', game.get_description()) + # desc = re.sub(' +', ' ', game.get_description()) + # scr.draw_text(21, 5, '\n'.join(textwrap.wrap(desc, width-21))) - scr.draw_text(21, 5, '\n'.join(textwrap.wrap(desc, width-21))) - scr.draw_text(21, 9, actions.format_actions(game.get_actions())) + # draw actions + scr.draw_text(width - 24, 2, actions.format_actions(game.get_actions())) # draw console messages con_y = 15 # start y position of console - for msg in game.log: + for msg in game.get_log(): scr.draw_text(0, con_y, msg.text, style=msg.style, fg=msg.fg, bg=msg.bg) con_y += msg.text.count('\n') + 1 # increment y by # of lines @@ -60,6 +63,16 @@ def on_render(scr: CursesScreen, key: str): scr.draw_text(0, height-1, 'Health: ' + str(game.player.get_health()), fg=0, bg=9) + # draw turns + scr.draw_text(16, height-1, + f' Turn: {game.turns.get_last_entity().name}, Order: {[ent.name for ent in game.turns.order]} ', + fg=0, bg=10) + + # draw fps + scr.draw_text(width - 16, height - 1, + ' FPS: {:.2f} '.format(scr.fps), + fg=0, bg=11) + # draw command buffer if game.menu_state == 'cmd': scr.draw_text(12, height-1, ':' + game.menu_cmd_buffer, 'standout') diff --git a/player.py b/player.py index a08cf75..d9c5c8a 100644 --- a/player.py +++ b/player.py @@ -36,7 +36,7 @@ def __init__(self, game): def move(self, dx, dy): self.pos += (dx, dy) - # self.game.add_log(world.tile_exists(self.pos).intro_text()) + # self.game.log(world.tile_exists(self.pos).intro_text()) def move_up(self): self.move(dx=0, dy=-1) @@ -50,22 +50,6 @@ def move_right(self): def move_left(self): self.move(dx=-1, dy=0) - def attack(self, enemy): - best_weapon = None - max_damage = 0 - for item in self.inventory: - if isinstance(item, items.Weapon): - if item.damage > max_damage: - max_damage = item.damage - best_weapon = item - - self.game.add_log('You use {} against {}!'.format(best_weapon.name, enemy.name)) - enemy.hp -= best_weapon.damage - if not enemy.is_alive(): - self.game.add_log('You killed a {}!'.format(enemy.name)) - else: - self.game.add_log('{} HP is {}.'.format(enemy.name, enemy.hp)) - def do_action(self, action, **kwargs): action_method = getattr(self, action.method.__name__) if action_method: @@ -78,6 +62,10 @@ def flee(self): print(f"av moves: { available_moves }") random_room = random.randint(0, len(available_moves) - 1) - self.game.add_log('You have fled to another room') + self.game.log('You have fled to another room') self.do_action(available_moves[random_room]) + + def think(self): + + self.game.game_state = 'input' diff --git a/tiles.py b/tiles.py index c354ab3..3d4d615 100644 --- a/tiles.py +++ b/tiles.py @@ -95,13 +95,14 @@ def modify_player(self, the_player): if self.enemy.is_alive(): # TODO: put into an Enemy.attack method part = the_player.hurt(self.enemy.damage) - self.game.add_log(f'An enemy hits your { part.name } and ' + self.game.log(f'An enemy hits your { part.name } and ' f'does { self.enemy.damage } !') def get_actions(self, ent: LivingEntity): if self.enemy.is_alive(): - return [actions.Flee(ent), actions.Attack(ent, enemy=self.enemy)] + # return [actions.Flee(ent), actions.Attack(ent, enemy=self.enemy)] + return [actions.Flee(ent)] else: return [] @@ -124,7 +125,7 @@ def modify_player(self, player): class GiantSpiderRoom(EnemyRoom): def __init__(self, game: 'Game'): - EnemyRoom.__init__(self, game, enemies.GiantSpider()) + EnemyRoom.__init__(self, game, enemies.GiantSpider(game)) def intro_text(self): if self.enemy.is_alive(): @@ -139,7 +140,7 @@ def intro_text(self): class OgreRoom(EnemyRoom): def __init__(self, game): - super().__init__(game, enemies.Ogre()) + super().__init__(game, enemies.Ogre(game)) def intro_text(self): if self.enemy.is_alive(): From ed410601cdfaeefe6b9875f23b38b423acbb7f0a Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Sun, 21 Jun 2020 16:44:40 -1000 Subject: [PATCH 26/39] revise build task --- .tasks | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.tasks b/.tasks index 6ff54b7..55c6a7d 100644 --- a/.tasks +++ b/.tasks @@ -1,5 +1,10 @@ [run] +command=DISPLAY=:0 python main.py +cwd=. +output=terminal + +[run-tty] command=termite --hold -e "bash -lic \"python main.py\"" cwd=. output=terminal From c276a344f784b80a3c9afbace901556030cd7a72 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Sun, 21 Jun 2020 16:45:01 -1000 Subject: [PATCH 27/39] start GL renderer --- elements/map.py | 27 ++++++ game.py | 19 ++-- main.py | 133 +++++++++++++++------------- main_curses.py | 221 +++++++++++++++++++++++++++++++++++++++++++++++ mixins/logger.py | 16 ++-- renderer/gl.py | 22 +++++ 6 files changed, 364 insertions(+), 74 deletions(-) create mode 100644 elements/map.py create mode 100644 main_curses.py create mode 100644 renderer/gl.py diff --git a/elements/map.py b/elements/map.py new file mode 100644 index 0000000..67bf09f --- /dev/null +++ b/elements/map.py @@ -0,0 +1,27 @@ + +from vector import vec2 +from pyglet import (text, shapes, graphics) +from typing import (TYPE_CHECKING) + + +if TYPE_CHECKING: + from game import Game + + +class Map: + """A top-down map showing the player's current location.""" + + def __init__(self, game: 'Game', x: int, y: int) + + self.pos: vec2 = vec2(x, y) + + self.game = game + + self.batch = graphics.Batch() + + self.e_box = shapes.Rectangle(x, y, 100, 100, color=(50, 50, 50), batch=self.batch) + + self.e_player = shapes.Rectangle(x, y, 10, 10, color(255, 255, 255), batch=self.batch) + + def draw(self): + self.batch.draw() diff --git a/game.py b/game.py index 1d76bce..96cc5ca 100644 --- a/game.py +++ b/game.py @@ -21,11 +21,11 @@ class Game(Logger): - def __init__(self, scr: 'CursesScreen'): + def __init__(self): - Logger.__init__(self) + Logger.__init__(self, 20) - self.scr: 'CursesScreen' = scr + # self.scr: 'CursesScreen' = scr # a string describing the current situation self.status = '' @@ -92,19 +92,21 @@ def on_key_pressed(self, key): if self.menu_state == 'cmd': # add the last pressed key to the cmd buffer - if key == 'KEY_BACKSPACE': + if key == 'KEY_BACKSPACE' or key == 'backspace': # remove last character self.menu_cmd_buffer = self.menu_cmd_buffer[:-1] - elif key == '\n': + elif key == 'space': + self.menu_cmd_buffer += ' ' + elif key == '\n' or key == 'enter': # TODO: run a command # self.log('running command: ' + self.menu_cmd_buffer) self.run_command(self.menu_cmd_buffer) self.menu_cmd_buffer = '' self.menu_state = 'main' else: - self.menu_cmd_buffer += self.input_key + self.menu_cmd_buffer += key - elif key == ':': + elif key == ':' or key == 'colon': # set the menu to cmd mode self.menu_state = 'cmd' @@ -155,6 +157,9 @@ def run_command(self, cmd: str): for command in self.get_commands(): if root in command.terms: command.do_command(args) + return + + self.log(f'I don\'t know what you mean by "{cmd}"') def get_movement_actions(self) -> list: """Get all movement actions that the player can do.""" diff --git a/main.py b/main.py index c6ef316..b6ea708 100644 --- a/main.py +++ b/main.py @@ -3,13 +3,13 @@ from screen import CursesScreen from game import Game -import actions -import textwrap +from pyglet import (window, app, text, clock) import re +import actions -scr = CursesScreen() -game = Game(scr) +win = window.Window(1280, 720) +game = Game() map_size = vec2(10, 10) # map_center = map_size // 2 @@ -17,96 +17,105 @@ map_view = None -@scr.init -def on_init(scr: CursesScreen): +@win.event +def on_key_press(key, modifiers): - global map_view - map_view = scr.create_view(0, 1, *map_size) + if key == window.key.Q: + win.close() + keymap = { + '_1': '1', + '_2': '2', + '_3': '3', + '_4': '4', + '_5': '5', + '_6': '6', + '_7': '7', + '_8': '8', + '_9': '9', + } -@scr.render -def on_render(scr: CursesScreen, key: str): + strkey = str(window.key.symbol_string(key)).lower() + strkey = keymap.get(strkey, strkey) - if game.quit: - scr.end() - return + game.on_key_pressed(strkey) - global map_view - game.on_key_pressed(key) - game.update() +l_title = text.Label( + 'Welcome to Super Fuck You', + font_name='Fantasque Sans Mono', + font_size=12, + x=win.width//2, y=win.height-20, + anchor_x='center' +) + + +def draw_text(x, y, t: str, *args, **kwargs): + + label = text.Label(t, + font_name='Fantasque Sans Mono', + font_size=12, + x=x, y=win.height-y, + width=800, + multiline=True, + color=kwargs.get('color', (255, 255, 255, 255)) + # anchor_y='bottom' + ) + + label.draw() + - width, height = scr.get_size() +@win.event +def on_draw(): - title = "~ Welcome to Super Fuck You ~" + width, height = win.width, win.height - scr.draw_text(0, 0, title.center(width), style='standout') - scr.draw_text(21, 2, ( + win.clear() + + l_title.draw() + + draw_text(21, 100, ( "Location: " + str(game.player.pos) + "\n" - "Last Keypress: " + str(key) + "\n" "State: " + game.game_state )) - # desc = re.sub(' +', ' ', game.get_description()) - # scr.draw_text(21, 5, '\n'.join(textwrap.wrap(desc, width-21))) - # draw actions - scr.draw_text(width - 24, 2, actions.format_actions(game.get_actions())) + draw_text(width - 400, 200, actions.format_actions(game.get_actions())) # draw console messages - con_y = 15 # start y position of console + con_y = height - 60 # start y position of console + color = [255, 255, 255, 255] for msg in game.get_log(): - scr.draw_text(0, con_y, msg.text, style=msg.style, fg=msg.fg, bg=msg.bg) - con_y += msg.text.count('\n') + 1 # increment y by # of lines + draw_text(50, con_y, msg.text, style=msg.style, fg=msg.fg, bg=msg.bg, color=tuple(color)) + con_y -= (msg.text.count('\n') + 1) * 20 # increment y by # of lines + color[3] -= 10 # draw health - scr.draw_text(0, height-1, 'Health: ' + str(game.player.get_health()), + draw_text(0, height, 'Health: ' + str(game.player.get_health()), fg=0, bg=9) # draw turns - scr.draw_text(16, height-1, + draw_text(16 * 12, height-1, f' Turn: {game.turns.get_last_entity().name}, Order: {[ent.name for ent in game.turns.order]} ', fg=0, bg=10) # draw fps - scr.draw_text(width - 16, height - 1, - ' FPS: {:.2f} '.format(scr.fps), - fg=0, bg=11) + # draw_text(width - 16, height - 1, + # ' FPS: {:.2f} '.format(scr.fps), + # fg=0, bg=11) # draw command buffer if game.menu_state == 'cmd': - scr.draw_text(12, height-1, ':' + game.menu_cmd_buffer, 'standout') - - scr.set_view(map_view) + draw_text(12, height-20, ':' + game.menu_cmd_buffer, 'standout') - # map_view.box() + win.flip() - # fill screen except room locations - for x in range(-int(map_size.x // 2), int(map_size.x // 2)): - for y in range(-int(map_size.y // 2), int(map_size.y // 2)): - try: - room = game.get_room(x, y) - if room is None: - scr.draw_text(x + map_center.x, y + map_center.y, u'█') - except Exception: - pass - world_pos = game.player.pos + map_center - scr.draw_text(world_pos.x, world_pos.y, "@".encode("UTF-8")) - - scr.draw_outline() - - # for x in range(center[0] - game_map_size[0], game_map_size[0] - center[0]): - # for y in range(center[1] - game_map_size[1], game_map_size[1] - center[1]): - # try: - # room = game.get_room(x, y) - # if room is None: - # draw_text(x + center[0], y + center[1], u'█') - # except: - # pass +def on_update(dt): + game.update() - # if world_pos[0] >= 0 and world_pos[1] >= 0: - # draw_text(world_pos[0], world_pos[1], "@".encode("UTF-8")) +clock.schedule_interval(on_update, 1.0/60.0) -scr.start() +# scr.start() +app.run() diff --git a/main_curses.py b/main_curses.py new file mode 100644 index 0000000..5338ee5 --- /dev/null +++ b/main_curses.py @@ -0,0 +1,221 @@ + +from vector import vec2 +from screen import CursesScreen +from game import Game + +from pyglet import (window, app, text, clock) +import re +import actions + + +scr = CursesScreen() +win = window.Window(1280, 720) +game = Game() + +map_size = vec2(10, 10) +# map_center = map_size // 2 +map_center = vec2(0, 0) +map_view = None + + +# @scr.init +def on_init(scr: CursesScreen): + pass + + # global map_view + # map_view = scr.create_view(0, 1, *map_size) + +# gl draw loop +# ------------ + + +@win.event +def on_key_press(key, modifiers): + + if key == window.key.Q: + win.close() + + keymap = { + '_1': '1', + '_2': '2', + '_3': '3', + '_4': '4', + '_5': '5', + '_6': '6', + '_7': '7', + '_8': '8', + '_9': '9', + } + + strkey = str(window.key.symbol_string(key)).lower() + strkey = keymap.get(strkey, strkey) + + game.on_key_pressed(strkey) + + +l_title = text.Label( + 'Welcome to Super Fuck You', + font_name='Fantasque Sans Mono', + font_size=12, + x=win.width//2, y=win.height-20, + anchor_x='center' +) + + +def draw_text(x, y, t: str, *args, **kwargs): + + label = text.Label(t, + font_name='Fantasque Sans Mono', + font_size=12, + x=x, y=win.height-y, + width=800, + multiline=True, + color=kwargs.get('color', (255, 255, 255, 255)) + # anchor_y='bottom' + ) + + label.draw() + + +@win.event +def on_draw(): + + width, height = win.width, win.height + + win.clear() + + l_title.draw() + + draw_text(21, 100, ( + "Location: " + str(game.player.pos) + "\n" + "State: " + game.game_state + )) + + # draw actions + draw_text(width - 400, 200, actions.format_actions(game.get_actions())) + + # draw console messages + con_y = height - 60 # start y position of console + color = [255, 255, 255, 255] + for msg in game.get_log(): + draw_text(50, con_y, msg.text, style=msg.style, fg=msg.fg, bg=msg.bg, color=tuple(color)) + con_y -= (msg.text.count('\n') + 1) * 20 # increment y by # of lines + color[3] -= 10 + + # draw health + draw_text(0, height, 'Health: ' + str(game.player.get_health()), + fg=0, bg=9) + + # draw turns + draw_text(16 * 12, height-1, + f' Turn: {game.turns.get_last_entity().name}, Order: {[ent.name for ent in game.turns.order]} ', + fg=0, bg=10) + + # draw fps + # draw_text(width - 16, height - 1, + # ' FPS: {:.2f} '.format(scr.fps), + # fg=0, bg=11) + + # draw command buffer + if game.menu_state == 'cmd': + draw_text(12, height-20, ':' + game.menu_cmd_buffer, 'standout') + + win.flip() + + +def on_update(dt): + game.update() + + +clock.schedule_interval(on_update, 1.0/60.0) + + +# curses draw loop +# ---------------- + +# @scr.render +def on_draw(scr: CursesScreen, key: str): + + if game.quit: + scr.end() + return + + global map_view + + game.on_key_pressed(key) + game.update() + + width, height = scr.get_size() + + title = "~ Welcome to Super Fuck You ~" + + scr.draw_text(0, 0, title.center(width), style='standout') + scr.draw_text(21, 2, ( + "Location: " + str(game.player.pos) + "\n" + "Last Keypress: " + str(key) + "\n" + "State: " + game.game_state + )) + + # desc = re.sub(' +', ' ', game.get_description()) + # scr.draw_text(21, 5, '\n'.join(textwrap.wrap(desc, width-21))) + + # draw actions + scr.draw_text(width - 24, 2, actions.format_actions(game.get_actions())) + + # draw console messages + con_y = 15 # start y position of console + for msg in game.get_log(): + scr.draw_text(0, con_y, msg.text, style=msg.style, fg=msg.fg, bg=msg.bg) + con_y += msg.text.count('\n') + 1 # increment y by # of lines + + # draw health + scr.draw_text(0, height-1, 'Health: ' + str(game.player.get_health()), + fg=0, bg=9) + + # draw turns + scr.draw_text(16, height-1, + f' Turn: {game.turns.get_last_entity().name}, Order: {[ent.name for ent in game.turns.order]} ', + fg=0, bg=10) + + # draw fps + scr.draw_text(width - 16, height - 1, + ' FPS: {:.2f} '.format(scr.fps), + fg=0, bg=11) + + # draw command buffer + if game.menu_state == 'cmd': + scr.draw_text(12, height-1, ':' + game.menu_cmd_buffer, 'standout') + + scr.set_view(map_view) + + # map_view.box() + + # fill screen except room locations + for x in range(-int(map_size.x // 2), int(map_size.x // 2)): + for y in range(-int(map_size.y // 2), int(map_size.y // 2)): + try: + room = game.get_room(x, y) + if room is None: + scr.draw_text(x + map_center.x, y + map_center.y, u'█') + except Exception: + pass + + world_pos = game.player.pos + map_center + scr.draw_text(world_pos.x, world_pos.y, "@".encode("UTF-8")) + + scr.draw_outline() + + # for x in range(center[0] - game_map_size[0], game_map_size[0] - center[0]): + # for y in range(center[1] - game_map_size[1], game_map_size[1] - center[1]): + # try: + # room = game.get_room(x, y) + # if room is None: + # draw_text(x + center[0], y + center[1], u'█') + # except: + # pass + + # if world_pos[0] >= 0 and world_pos[1] >= 0: + # draw_text(world_pos[0], world_pos[1], "@".encode("UTF-8")) + +# scr.start() +app.run() diff --git a/mixins/logger.py b/mixins/logger.py index 3a345ba..63cf65d 100644 --- a/mixins/logger.py +++ b/mixins/logger.py @@ -28,12 +28,18 @@ def get_log(self) -> List[Message]: def log(self, text: str, fg=15, bg=0, style='normal'): """Log a message.""" - # insert to the front of the list - self.log_messages.insert(0, Message(text, style, fg, bg)) + lines = text.splitlines() - # delete last message if above log_limit - if len(self.log_messages) > self.log_limit: - del self.log_messages[self.log_limit] + for line in lines: + + # insert to the front of the list + self.log_messages.insert(0, Message(line, style, fg, bg)) + # self.log_messages.append(Message(text, style, fg, bg)) + + # delete last message if above log_limit + if len(self.log_messages) > self.log_limit: + del self.log_messages[self.log_limit] + # del self.log_messages[0] def info(self, text: str): self.log(text, 8) # dark gray diff --git a/renderer/gl.py b/renderer/gl.py new file mode 100644 index 0000000..2b05daa --- /dev/null +++ b/renderer/gl.py @@ -0,0 +1,22 @@ +""" +A GL renderer using Pyglet. +""" + +import pyglet + + +class GLScreen: + """A window managed by Pyglet.""" + + def __init__(self): + + + def set_view(self, view=None): + pass + + def read_input(self) -> str: + return '' + + def create_view + + From 267bc954eb2f776fe62af056a10ecf5201e6eef8 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Mon, 22 Jun 2020 00:34:56 -1000 Subject: [PATCH 28/39] WIP: work on in-game map and fonts --- elements/map.py | 15 ++++++++++++--- main.py | 10 ++++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/elements/map.py b/elements/map.py index 67bf09f..017db56 100644 --- a/elements/map.py +++ b/elements/map.py @@ -10,8 +10,11 @@ class Map: """A top-down map showing the player's current location.""" + cols: int = 10 + rows: int = 10 + size: int = 20 - def __init__(self, game: 'Game', x: int, y: int) + def __init__(self, game: 'Game', x: int, y: int): self.pos: vec2 = vec2(x, y) @@ -19,9 +22,15 @@ def __init__(self, game: 'Game', x: int, y: int) self.batch = graphics.Batch() - self.e_box = shapes.Rectangle(x, y, 100, 100, color=(50, 50, 50), batch=self.batch) + self.e_box = shapes.Rectangle(x, y, Map.cols * Map.size, Map.rows * Map.size, color=(50, 50, 50), batch=self.batch) - self.e_player = shapes.Rectangle(x, y, 10, 10, color(255, 255, 255), batch=self.batch) + self.e_player = shapes.Rectangle(x, y, Map.size, Map.size, color=(255, 255, 255), batch=self.batch) def draw(self): + + x, y = self.game.player.pos + + self.e_player.x = self.pos.x + (x * Map.size) + self.e_player.y = self.pos.y + ((Map.rows - y) * Map.size) + self.batch.draw() diff --git a/main.py b/main.py index b6ea708..d6fc28d 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ from vector import vec2 from screen import CursesScreen +from elements.map import Map from game import Game from pyglet import (window, app, text, clock) @@ -53,7 +54,7 @@ def on_key_press(key, modifiers): def draw_text(x, y, t: str, *args, **kwargs): label = text.Label(t, - font_name='Fantasque Sans Mono', + font_name=['Fantasque Sans Mono', 'Courier New'], font_size=12, x=x, y=win.height-y, width=800, @@ -65,6 +66,9 @@ def draw_text(x, y, t: str, *args, **kwargs): label.draw() +e_map = Map(game, 800, 20) + + @win.event def on_draw(): @@ -74,6 +78,8 @@ def on_draw(): l_title.draw() + e_map.draw() + draw_text(21, 100, ( "Location: " + str(game.player.pos) + "\n" "State: " + game.game_state @@ -108,7 +114,7 @@ def on_draw(): if game.menu_state == 'cmd': draw_text(12, height-20, ':' + game.menu_cmd_buffer, 'standout') - win.flip() + # win.flip() def on_update(dt): From 351ed11b8e20155c498e557ba1aa3e2aad0587ce Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Mon, 22 Jun 2020 03:56:52 -1000 Subject: [PATCH 29/39] WIP: try to use pywal colors as colorscheme --- elements/__init__.py | 3 ++ elements/text.py | 71 ++++++++++++++++++++++++++++++++++++++++++++ main.py | 28 ++++++++--------- 3 files changed, 87 insertions(+), 15 deletions(-) create mode 100644 elements/__init__.py create mode 100644 elements/text.py diff --git a/elements/__init__.py b/elements/__init__.py new file mode 100644 index 0000000..85acff0 --- /dev/null +++ b/elements/__init__.py @@ -0,0 +1,3 @@ +""" +Contains objects that can be drawn to the screen. +""" diff --git a/elements/text.py b/elements/text.py new file mode 100644 index 0000000..195d89c --- /dev/null +++ b/elements/text.py @@ -0,0 +1,71 @@ + +import os +import subprocess +import pyglet + +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from game import Game + + +def get_wal_colors(): + """Attempt to get the current colors used by pywal.""" + + res = subprocess.run(['cat', os.path.expanduser('~/.cache/wal/colors')], + stdout=subprocess.PIPE) + lines = res.stdout + colors = [] + + if lines: + for line in lines.splitlines(): + # print('{} {} {}'.format(line[1:3], line[3:5], line[5:7])) + r = int(line[1:3], 16) + g = int(line[3:5], 16) + b = int(line[5:7], 16) + colors.append((r, g, b)) + + return colors + + +_colors: list = get_wal_colors() + + +class Text: + + def __init__(self, game: 'Game', x, y, text: str, style: str = 'normal', + fg=None, bg=None): + + def _get_color(idx, default): + try: + return _colors[idx] + except Exception: + return default + + self.fg_color = _get_color(fg, (255, 255, 255)) + (255,) + self.bg_color = _get_color(bg, (0, 0, 0)) + + self.e_label = pyglet.text.Label( + text, + font_name=['Courier New', 'Hack'], + font_size=12, + x=x, y=y, + width=800, + multiline=True, + color=self.fg_color + ) + + self.e_background = pyglet.shapes.Rectangle( + x, y - 4, self.e_label.content_width, self.e_label.content_height, + color=self.bg_color + ) + + def draw(self): + self.e_background.draw() + self.e_label.draw() + + +if __name__ == '__main__': + # test get_wal_colors() + get_wal_colors() diff --git a/main.py b/main.py index d6fc28d..da74d08 100644 --- a/main.py +++ b/main.py @@ -2,9 +2,10 @@ from vector import vec2 from screen import CursesScreen from elements.map import Map +from elements.text import Text, _colors from game import Game -from pyglet import (window, app, text, clock) +from pyglet import (window, app, text, clock, gl) import re import actions @@ -51,23 +52,17 @@ def on_key_press(key, modifiers): ) -def draw_text(x, y, t: str, *args, **kwargs): - - label = text.Label(t, - font_name=['Fantasque Sans Mono', 'Courier New'], - font_size=12, - x=x, y=win.height-y, - width=800, - multiline=True, - color=kwargs.get('color', (255, 255, 255, 255)) - # anchor_y='bottom' - ) +def draw_text(x, y, t: str, style: str = 'normal', fg=None, bg=None, *args, **kwargs): + label = Text(game, x, win.height-y, t, style, fg, bg) label.draw() e_map = Map(game, 800, 20) +# set clear color +gl.glClearColor(_colors[0][0]/255.0, _colors[0][1]/255.0, _colors[0][2]/255.0, 1) + @win.event def on_draw(): @@ -91,10 +86,13 @@ def on_draw(): # draw console messages con_y = height - 60 # start y position of console color = [255, 255, 255, 255] - for msg in game.get_log(): + for i, msg in enumerate(game.get_log()): draw_text(50, con_y, msg.text, style=msg.style, fg=msg.fg, bg=msg.bg, color=tuple(color)) - con_y -= (msg.text.count('\n') + 1) * 20 # increment y by # of lines - color[3] -= 10 + con_y -= 20 + if i == 0: + color[3] -= 20 + else: + color[3] -= 10 # draw health draw_text(0, height, 'Health: ' + str(game.player.get_health()), From 2fad69d98db5c3938cf851070292c7aed0cad9d3 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Mon, 22 Jun 2020 17:09:25 -1000 Subject: [PATCH 30/39] WIP: fix text backgrounds and colors --- elements/text.py | 9 ++++++--- main.py | 13 +++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/elements/text.py b/elements/text.py index 195d89c..108f376 100644 --- a/elements/text.py +++ b/elements/text.py @@ -35,7 +35,8 @@ def get_wal_colors(): class Text: def __init__(self, game: 'Game', x, y, text: str, style: str = 'normal', - fg=None, bg=None): + fg=None, bg=None, + padding=10): def _get_color(idx, default): try: @@ -53,11 +54,13 @@ def _get_color(idx, default): x=x, y=y, width=800, multiline=True, - color=self.fg_color + anchor_y='bottom', + color=self.fg_color, ) self.e_background = pyglet.shapes.Rectangle( - x, y - 4, self.e_label.content_width, self.e_label.content_height, + x - padding/2, y - padding/2, + self.e_label.content_width + padding, self.e_label.content_height + padding, color=self.bg_color ) diff --git a/main.py b/main.py index da74d08..2e4af29 100644 --- a/main.py +++ b/main.py @@ -43,12 +43,9 @@ def on_key_press(key, modifiers): game.on_key_pressed(strkey) -l_title = text.Label( - 'Welcome to Super Fuck You', - font_name='Fantasque Sans Mono', - font_size=12, - x=win.width//2, y=win.height-20, - anchor_x='center' +l_title = Text(game, win.width // 2, win.height - 20, + 'Welcome to Super Fuck You', 'normal', + fg=0, bg=7 ) @@ -81,14 +78,14 @@ def on_draw(): )) # draw actions - draw_text(width - 400, 200, actions.format_actions(game.get_actions())) + draw_text(width - 400, 200, actions.format_actions(game.get_actions()), bg=8) # draw console messages con_y = height - 60 # start y position of console color = [255, 255, 255, 255] for i, msg in enumerate(game.get_log()): draw_text(50, con_y, msg.text, style=msg.style, fg=msg.fg, bg=msg.bg, color=tuple(color)) - con_y -= 20 + con_y -= 25 if i == 0: color[3] -= 20 else: From b3989004e14ee0f7de92279115d326f38b6c9636 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Tue, 23 Jun 2020 15:58:57 -1000 Subject: [PATCH 31/39] WIP: add default colorscheme if pywal doesn't exist --- elements/text.py | 48 ++++++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/elements/text.py b/elements/text.py index 108f376..ec1cdab 100644 --- a/elements/text.py +++ b/elements/text.py @@ -10,26 +10,37 @@ from game import Game -def get_wal_colors(): - """Attempt to get the current colors used by pywal.""" +def get_colors(): - res = subprocess.run(['cat', os.path.expanduser('~/.cache/wal/colors')], - stdout=subprocess.PIPE) - lines = res.stdout - colors = [] + try: # attempt to get the current colors used by pywal. + res = subprocess.run(['cat', os.path.expanduser('~/.cache/wal/colors')], + stdout=subprocess.PIPE) + lines = res.stdout + colors = [] - if lines: - for line in lines.splitlines(): - # print('{} {} {}'.format(line[1:3], line[3:5], line[5:7])) - r = int(line[1:3], 16) - g = int(line[3:5], 16) - b = int(line[5:7], 16) - colors.append((r, g, b)) + if lines: + for line in lines.splitlines(): + # print('{} {} {}'.format(line[1:3], line[3:5], line[5:7])) + r = int(line[1:3], 16) + g = int(line[3:5], 16) + b = int(line[5:7], 16) + colors.append((r, g, b)) - return colors + return colors + except Exception: + return [(0, 0, 0), + (255, 255, 255), + (255, 255, 255), + (255, 255, 255), + (255, 255, 255), + (255, 255, 255), + (255, 255, 255), + (255, 255, 255) + ] * 2 -_colors: list = get_wal_colors() + +_colors: list = get_colors() class Text: @@ -60,7 +71,8 @@ def _get_color(idx, default): self.e_background = pyglet.shapes.Rectangle( x - padding/2, y - padding/2, - self.e_label.content_width + padding, self.e_label.content_height + padding, + self.e_label.content_width + padding, + self.e_label.content_height + padding, color=self.bg_color ) @@ -70,5 +82,5 @@ def draw(self): if __name__ == '__main__': - # test get_wal_colors() - get_wal_colors() + # test get_colors() + get_colors() From 511a61d7d3d7e61524f03f5a08b2af29fa16b71a Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Tue, 23 Jun 2020 16:13:08 -1000 Subject: [PATCH 32/39] WIP: fix fading color in console --- elements/map.py | 11 ++++++++++- elements/text.py | 11 +++++++++-- main.py | 2 +- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/elements/map.py b/elements/map.py index 017db56..ee7b2cf 100644 --- a/elements/map.py +++ b/elements/map.py @@ -22,7 +22,16 @@ def __init__(self, game: 'Game', x: int, y: int): self.batch = graphics.Batch() - self.e_box = shapes.Rectangle(x, y, Map.cols * Map.size, Map.rows * Map.size, color=(50, 50, 50), batch=self.batch) + self.e_boxes = [] + + for pos in self.game.world.room_map.keys(): + + box = shapes.Rectangle( + x + (pos.x * Map.size), y + ((Map.rows - pos.y) * Map.size), + Map.size, Map.size, + color=(50, 50, 50), batch=self.batch + ) + self.e_boxes.append(box) self.e_player = shapes.Rectangle(x, y, Map.size, Map.size, color=(255, 255, 255), batch=self.batch) diff --git a/elements/text.py b/elements/text.py index ec1cdab..2869dc9 100644 --- a/elements/text.py +++ b/elements/text.py @@ -55,8 +55,15 @@ def _get_color(idx, default): except Exception: return default - self.fg_color = _get_color(fg, (255, 255, 255)) + (255,) - self.bg_color = _get_color(bg, (0, 0, 0)) + if not fg or type(fg) is int: + self.fg_color = _get_color(fg, (255, 255, 255)) + (255,) + else: + self.fg_color = fg + + if not bg or type(bg) is int: + self.bg_color = _get_color(bg, (0, 0, 0)) + else: + self.bg_color = bg self.e_label = pyglet.text.Label( text, diff --git a/main.py b/main.py index 2e4af29..60b255a 100644 --- a/main.py +++ b/main.py @@ -84,7 +84,7 @@ def on_draw(): con_y = height - 60 # start y position of console color = [255, 255, 255, 255] for i, msg in enumerate(game.get_log()): - draw_text(50, con_y, msg.text, style=msg.style, fg=msg.fg, bg=msg.bg, color=tuple(color)) + draw_text(50, con_y, msg.text, style=msg.style, fg=tuple(color), bg=msg.bg) con_y -= 25 if i == 0: color[3] -= 20 From 7bb38596787dd3a7eb1f81ebfb4f4e23da9f1053 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Tue, 23 Jun 2020 16:47:08 -1000 Subject: [PATCH 33/39] implement attack action for entities --- docs/ai.md | 10 ++++++++++ enemies.py | 16 ++++++++++++++++ game.py | 5 +++++ 3 files changed, 31 insertions(+) create mode 100644 docs/ai.md diff --git a/docs/ai.md b/docs/ai.md new file mode 100644 index 0000000..55438d2 --- /dev/null +++ b/docs/ai.md @@ -0,0 +1,10 @@ + +# Behavior of AI + +Enemies +------- + +If enemies are not aware of you, they will roll to look around every turn. + +If enemies _notice_ your presence, they will choose to attack you every turn. + diff --git a/enemies.py b/enemies.py index 87de5d7..4185959 100644 --- a/enemies.py +++ b/enemies.py @@ -1,6 +1,8 @@ #!/usr/bin/python3 env from entity import LivingEntity +from player import Player +from actions import create_action class Enemy(LivingEntity): @@ -13,6 +15,20 @@ def __init__(self, game, name, damage): #TODO: be able to set HP + def think(self): + + # find player + for ent in self.game.get_entities(): + if type(ent) is Player: + + def _attack(): + self.game.log(f'{self.name} is attacking {ent.name}!') + self.attack(ent) + + return create_action('', _attack) + + return super().think() + class GiantSpider(Enemy): def __init__(self, game): diff --git a/game.py b/game.py index 96cc5ca..e710ba4 100644 --- a/game.py +++ b/game.py @@ -79,6 +79,11 @@ def set_current_room(self): # TODO: Room.intro_text() is deprecated + def get_entities(self): + """Get all entities (in the current room).""" + + return self.room.entities + [self.player] + def do_tick(self): """Calculate one unit of time.""" From e87aca44c97481f0f194b27c5cbbcccdda788151 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Tue, 23 Jun 2020 17:47:13 -1000 Subject: [PATCH 34/39] show enemy health in HUD --- colors.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ elements/text.py | 43 ++++--------------------------------------- enemies.py | 2 +- game.py | 4 ++++ main.py | 25 ++++++++++++++++++------- 5 files changed, 71 insertions(+), 47 deletions(-) create mode 100644 colors.py diff --git a/colors.py b/colors.py new file mode 100644 index 0000000..c441c5e --- /dev/null +++ b/colors.py @@ -0,0 +1,44 @@ + +import os +import subprocess + + +def _get_colors(): + + try: # attempt to get the current colors used by pywal. + res = subprocess.run(['cat', os.path.expanduser('~/.cache/wal/colors')], + stdout=subprocess.PIPE) + lines = res.stdout + colors = [] + + if lines: + for line in lines.splitlines(): + # print('{} {} {}'.format(line[1:3], line[3:5], line[5:7])) + r = int(line[1:3], 16) + g = int(line[3:5], 16) + b = int(line[5:7], 16) + colors.append((r, g, b)) + + return colors + + except Exception: + return [(0, 0, 0), + (255, 255, 255), + (255, 255, 255), + (255, 255, 255), + (255, 255, 255), + (255, 255, 255), + (255, 255, 255), + (255, 255, 255) + ] * 2 + + +class Colors: + """A colorscheme to use for the game.""" + + def __init__(self): + # try to automatically find a colorscheme + self.colors: list = _get_colors() + + def __getitem__(self, i): + return self.colors[i] diff --git a/elements/text.py b/elements/text.py index 2869dc9..822cecb 100644 --- a/elements/text.py +++ b/elements/text.py @@ -1,6 +1,4 @@ -import os -import subprocess import pyglet from typing import TYPE_CHECKING @@ -10,53 +8,20 @@ from game import Game -def get_colors(): - - try: # attempt to get the current colors used by pywal. - res = subprocess.run(['cat', os.path.expanduser('~/.cache/wal/colors')], - stdout=subprocess.PIPE) - lines = res.stdout - colors = [] - - if lines: - for line in lines.splitlines(): - # print('{} {} {}'.format(line[1:3], line[3:5], line[5:7])) - r = int(line[1:3], 16) - g = int(line[3:5], 16) - b = int(line[5:7], 16) - colors.append((r, g, b)) - - return colors - - except Exception: - return [(0, 0, 0), - (255, 255, 255), - (255, 255, 255), - (255, 255, 255), - (255, 255, 255), - (255, 255, 255), - (255, 255, 255), - (255, 255, 255) - ] * 2 - - -_colors: list = get_colors() - - class Text: def __init__(self, game: 'Game', x, y, text: str, style: str = 'normal', - fg=None, bg=None, + fg=None, bg=None, alpha: float = 1.0, padding=10): def _get_color(idx, default): try: - return _colors[idx] + return game.colorscheme[idx] except Exception: return default if not fg or type(fg) is int: - self.fg_color = _get_color(fg, (255, 255, 255)) + (255,) + self.fg_color = _get_color(fg, (255, 255, 255)) + (int(255 * alpha),) else: self.fg_color = fg @@ -68,7 +33,7 @@ def _get_color(idx, default): self.e_label = pyglet.text.Label( text, font_name=['Courier New', 'Hack'], - font_size=12, + font_size=11, x=x, y=y, width=800, multiline=True, diff --git a/enemies.py b/enemies.py index 4185959..3f54ca9 100644 --- a/enemies.py +++ b/enemies.py @@ -22,7 +22,7 @@ def think(self): if type(ent) is Player: def _attack(): - self.game.log(f'{self.name} is attacking {ent.name}!') + self.game.log(f'{self.name} is attacking {ent.name}!', fg=1) self.attack(ent) return create_action('', _attack) diff --git a/game.py b/game.py index e710ba4..9981628 100644 --- a/game.py +++ b/game.py @@ -4,6 +4,7 @@ import actions import typing +from colors import Colors from tiles import Room from turns import TurnManager from dataclasses import dataclass @@ -27,6 +28,9 @@ def __init__(self): # self.scr: 'CursesScreen' = scr + # the current colorscheme to use + self.colorscheme = Colors() + # a string describing the current situation self.status = '' diff --git a/main.py b/main.py index 60b255a..59e8e54 100644 --- a/main.py +++ b/main.py @@ -2,8 +2,9 @@ from vector import vec2 from screen import CursesScreen from elements.map import Map -from elements.text import Text, _colors +from elements.text import Text from game import Game +from entity import LivingEntity from pyglet import (window, app, text, clock, gl) import re @@ -49,16 +50,18 @@ def on_key_press(key, modifiers): ) -def draw_text(x, y, t: str, style: str = 'normal', fg=None, bg=None, *args, **kwargs): +def draw_text(x, y, t: str, style: str = 'normal', fg=None, bg=None, alpha=1.0, *args, **kwargs): - label = Text(game, x, win.height-y, t, style, fg, bg) + label = Text(game, x, win.height-y, t, style, fg, bg, alpha=alpha) label.draw() e_map = Map(game, 800, 20) # set clear color -gl.glClearColor(_colors[0][0]/255.0, _colors[0][1]/255.0, _colors[0][2]/255.0, 1) +gl.glClearColor(game.colorscheme[0][0]/255.0, + game.colorscheme[0][1]/255.0, + game.colorscheme[0][2]/255.0, 1) @win.event @@ -83,18 +86,26 @@ def on_draw(): # draw console messages con_y = height - 60 # start y position of console color = [255, 255, 255, 255] + alpha = 1.0 for i, msg in enumerate(game.get_log()): - draw_text(50, con_y, msg.text, style=msg.style, fg=tuple(color), bg=msg.bg) + draw_text(50, con_y, msg.text, style=msg.style, fg=msg.fg, bg=msg.bg, + alpha=alpha) con_y -= 25 if i == 0: - color[3] -= 20 + alpha -= 0.1 else: - color[3] -= 10 + alpha -= 0.03 # draw health draw_text(0, height, 'Health: ' + str(game.player.get_health()), fg=0, bg=9) + # draw health of other entities + hp_y = height + for ent in [ent for ent in game.room.entities if isinstance(ent, LivingEntity)]: + draw_text(width - 200, hp_y, f'{ent.name}: {ent.get_health()}', fg=0, bg=11) + hp_y -= 25 + # draw turns draw_text(16 * 12, height-1, f' Turn: {game.turns.get_last_entity().name}, Order: {[ent.name for ent in game.turns.order]} ', From b4515c5e26edb4a97712ecc09d31744d7162d84f Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Tue, 23 Jun 2020 20:58:53 -1000 Subject: [PATCH 35/39] start menu system --- actionmenu.py | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++ actions.py | 25 ++++--- game.py | 89 ++++-------------------- main.py | 3 +- 4 files changed, 219 insertions(+), 87 deletions(-) create mode 100644 actionmenu.py diff --git a/actionmenu.py b/actionmenu.py new file mode 100644 index 0000000..49946cb --- /dev/null +++ b/actionmenu.py @@ -0,0 +1,189 @@ +""" +Action Menu +----------- + +An action menu holds a list of actions (a key binding and a function), +a dictionary of arbitrary arguments to pass between menus, +and a menu queue (a list defining what the next menu(s) should be). + +* * * + +The next_menu() method pops the next menu off the queue and returns it. +If the menu queue is empty, next_menu() will return 'self', causing the next +menu to be itself. + +* * * + +The handle_input(cmd) method should be called whenever the Player either +presses a key (then 'cmd' will be the character corresponding to that key), or +types a command (activated by clicking ':' and typing a command, then 'cmd' +will be the entered command) + +If 'cmd' matches any of the bindings in the menu's list of actions, then the +corresponding action should be called. + +If an action was called, return next_menu(), otherwise return 'self'. + +* * * + +The ActionMenu class should be subclassed to create different menus. +In the __init__() method of the subclass, use the @self.add_option +decorator to add the decorated function as an action to the menu. + +If the action needs to pass an argument to the next menu, set a key in the +'self.args' dictionary. 'self.args' will be passed to the next menu. +""" + +from actions import QuickAction + + +class ActionMenu: + """A handler for the action menu.""" + + def __init__(self, game, **kwargs): + + # a reference to the game + self.game = game + + # a list of actions in this menu + self.action_list = [] + + # an optional dict of arguments to pass state between menus + self.args: dict = kwargs or {} + + def handle_input(self, cmd: str) -> 'ActionMenu': + """Handle an input from the player (a key or a command). + Return the menu to display after handling the input. """ + + for action in self.action_list: + if cmd == action.key: + action() + return self.next_menu() + + # no key matches; don't change the menu + return self + + def next_menu(self) -> 'ActionMenu': + """Pops the next menu in the stack and return it. + + If the popped menu is the last menu in the stack, additionally + run the function inside args['end_fn']. + """ + + if 'menu_stack' in self.args: + + # pop the next menu from the queue + menu_cls = self.args['menu_stack'].pop(0) + + # print(f'going to menu: {menu_cls.__name__} {[menu.__name__ for menu in self.args["menu_stack"]]}') + # print(self.args) + + # if the queue is empty, run the end function + if len(self.args['menu_stack']) == 0: + self.args['end_fn'](self.args) + # if the end function changed the player's location, + # update the room + self.game.set_current_room() + # then continue the game + self.game.game_state = 'running' + + return menu_cls(self.game, **self.args) + + return self + + def add_option(self, key: str, desc: str = 'a custom action', menu_stack=None): + """Decorator that creates a QuickAction and adds it to this menu.""" + + if menu_stack: + main = True + else: + main = False + menu_stack = self.args['menu_stack'] + + def register_inner(func): + + def do_action(): + self.args['menu_stack'] = menu_stack + if main: + # set the wrapped function to run at the end; + # then goto the next menu + self.args['end_fn'] = func + else: + # run the wrapped function; + # then goto the next menu + func() + + # add an instance of this class to our list of actions + self.action_list.append(QuickAction(key, desc, do_action)) + + return func + + return register_inner + + +class InventoryMenu(ActionMenu): + """A menu that lists all items in the player's inventory.""" + + def __init__(self, game, **kwargs): + super().__init__(game, **kwargs) + + for i, item in enumerate(list(self.game.player.inventory.keys())): + + @self.add_option(str(i+1), f'select {item.name}') + def _select_item(self=self, item=item): + # self.game.log(f'chose {item.name}') + self.args['item'] = item + + +class BodyPartMenu(ActionMenu): + """A menu that lists all the player's body parts that can hold an item.""" + + def __init__(self, game, **kwargs): + super().__init__(game, **kwargs) + + for i, part in enumerate(self.game.player.body.get_equippable_parts()): + + @self.add_option(str(i+1), f'select {part.name}') + def _select_part(self=self, part=part): + # self.game.log(f'chose {part.name}') + self.args['part'] = part + + +class EntityMenu(ActionMenu): + + def __init__(self, game, **kwargs): + super().__init__(game, **kwargs) + + # add 'yourself' as an option + @self.add_option('1', 'yourself') + def _self_attack(): + self.args['ent'] = self.game.player + + for i, ent in enumerate(self.game.room.entities): + + @self.add_option(str(i+2), f'{ent.name}') + def _attack(ent=ent): + self.args['ent'] = ent + + +class MainMenu(ActionMenu): + + def __init__(self, game, **kwargs): + super().__init__(game, **kwargs) + + # movement action + for action in self.game.get_movement_actions(): + self.add_option(action.key, action.desc, [MainMenu])(action.do_action) + + # TODO: look action + # TODO: examine action + + @self.add_option('e', 'Equip an item...', [InventoryMenu, BodyPartMenu, MainMenu]) + def _equip(args): + item, part = args['item'], args['part'] + part.held_item = item + self.game.log(f'your {part.name} is now holding {item.name}') + + @self.add_option('k', 'Attack...', [EntityMenu, MainMenu]) + def _attack(args): + self.game.player.attack(args['ent']) diff --git a/actions.py b/actions.py index 7da2a33..8599283 100644 --- a/actions.py +++ b/actions.py @@ -39,20 +39,25 @@ def format_vector(vec: vec2) -> str: class Action(): """An action that can be taken.""" - def __init__(self, desc: str): + def __init__(self, desc: str, fn=None): # the description of this action self.desc = desc or "unknown action" + self.fn = fn or self.do_action + def do_action(self): """Run the action.""" pass + def __call__(self): + self.fn() + class QuickAction(Action): """An action that is bound to a hotkey.""" - def __init__(self, key: str, desc: str): - Action.__init__(self, desc) + def __init__(self, key: str, desc: str, fn=None): + Action.__init__(self, desc, fn) # the hotkey that will run this action self.key = key @@ -69,7 +74,7 @@ def __init__(self): QuickAction.__init__(self, key, desc) def do_action(self): - func() + return func() # add an instance of this class to our list of actions actions.append(CustomAction()) @@ -91,7 +96,7 @@ def do_command(self, *args): pass def do_action(self): - self.do_command() + return self.do_command() @staticmethod def register(actions: list, terms: list, desc: str = 'a custom action'): @@ -105,7 +110,7 @@ def __init__(self): CommandAction.__init__(self, terms, desc) def do_command(self, *args): - func(*args) + return func(*args) # add an instance of this class to our list of actions actions.append(CustomAction()) @@ -164,7 +169,7 @@ class MoveUp(PlayerAction): def __init__(self, player): super().__init__(player, 'w', 'move up') - def do_action(self): + def do_action(self, *args): self.player.move_up() @@ -173,7 +178,7 @@ class MoveDown(PlayerAction): def __init__(self, player): super().__init__(player, 's', 'move down') - def do_action(self): + def do_action(self, *args): self.player.move_down() @@ -182,7 +187,7 @@ class MoveRight(PlayerAction): def __init__(self, player): super().__init__(player, 'd', 'move right') - def do_action(self): + def do_action(self, *args): self.player.move_right() @@ -191,7 +196,7 @@ class MoveLeft(PlayerAction): def __init__(self, player): super().__init__(player, 'a', 'move left') - def do_action(self): + def do_action(self, *args): self.player.move_left() diff --git a/game.py b/game.py index 9981628..c635db7 100644 --- a/game.py +++ b/game.py @@ -4,6 +4,7 @@ import actions import typing +from actionmenu import MainMenu from colors import Colors from tiles import Room from turns import TurnManager @@ -55,6 +56,8 @@ def __init__(self): # 'running' -> time is progressing self.game_state = 'input' + self.menu = MainMenu(self) + # the state of the action menu # tells the game what actions to display self.menu_state = 'main' @@ -121,14 +124,15 @@ def on_key_pressed(self, key): elif self.player.is_alive() and not self.player.victory: # check to do any actions - for action in self.get_actions(): - if key == action.key: - action.do_action() - self.set_current_room() - # resume game - self.game_state = 'running' - # self.do_tick() - break + self.menu = self.menu.handle_input(key) + # for action in self.get_actions(): + # if key == action.key: + # action.do_action() + # self.set_current_room() + # # resume game + # self.game_state = 'running' + # # self.do_tick() + # break def get_room(self, x, y) -> str: """Get a room by its coordinates.""" @@ -172,76 +176,9 @@ def run_command(self, cmd: str): def get_movement_actions(self) -> list: """Get all movement actions that the player can do.""" + # TODO: move this into actionmenu.py return self.world.get_movement_actions(self.player.pos, self.player) - def get_actions(self) -> list: - """Get all actions that the player can do.""" - # self.log("getting actions...") - - action_list = [] - - if self.game_state == 'running': - # game is running; no actions to do now - return action_list - - if self.menu_state == 'main': - - # get movement actions - action_list += self.get_movement_actions() # get movement actions - - # get room actions - if self.room: - action_list += self.room.get_actions(self.player) # get room actions - - # debug actions - action_list.append(actions.CheckBodyAction(self.player)) - action_list.append(actions.CheckInventory(self.player)) - - @QuickAction.register(action_list, 'e', 'equip an item') - def _equip(): - self.menu_state = 'equip_item' - - @QuickAction.register(action_list, 'k', 'attack') - def _attack(): - self.menu_state = 'attack' - - elif self.menu_state == 'equip_item': - - for i, item in enumerate(list(self.player.inventory.keys())): - - @QuickAction.register(action_list, str(i+1), f'select {item.name}') - def _select_item(item=item): - self.menu_selected_item = item - self.menu_state = 'equip_part' - - elif self.menu_state == 'equip_part': - - for i, part in enumerate(self.player.body.get_equippable_parts()): - - @QuickAction.register(action_list, str(i+1), f'select {part.name}') - def _action(part=part): - item = self.menu_selected_item - self.log(f'your {part.name} is now holding {item.name}') - part.held_item = item - self.menu_selected_item = None - self.menu_state = 'main' - - elif self.menu_state == 'attack': - - @QuickAction.register(action_list, '1', 'yourself') - def _self_attack(): - self.player.attack(self.player) - self.menu_state = 'main' - - for i, ent in enumerate(self.room.entities): - - @QuickAction.register(action_list, str(i+2), f'{ent.name}') - def _attack(ent=ent): - self.player.attack(ent) - self.menu_state = 'main' - - return action_list - def get_description(self, skill_check: int = 10): """Get a description of the room based on what the player sees. If a skill check is provided, change the description based diff --git a/main.py b/main.py index 59e8e54..ea3f93c 100644 --- a/main.py +++ b/main.py @@ -81,7 +81,8 @@ def on_draw(): )) # draw actions - draw_text(width - 400, 200, actions.format_actions(game.get_actions()), bg=8) + # draw_text(width - 400, 200, actions.format_actions(game.get_actions()), bg=8) + draw_text(width - 400, 200, actions.format_actions(game.menu.action_list), bg=8) # draw console messages con_y = height - 60 # start y position of console From b95ecad9e941a16589543138fe245d95372f2dd5 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Tue, 23 Jun 2020 22:07:30 -1000 Subject: [PATCH 36/39] implement ability scores --- actionmenu.py | 11 ++++++++ entity.py | 4 ++- stats.py | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 stats.py diff --git a/actionmenu.py b/actionmenu.py index 49946cb..5c4fc2c 100644 --- a/actionmenu.py +++ b/actionmenu.py @@ -177,6 +177,17 @@ def __init__(self, game, **kwargs): # TODO: look action # TODO: examine action + @self.add_option('x', 'Examine...', [EntityMenu, MainMenu]) + def _examine(args): + ent = args['ent'] + check = self.game.player.ability_check(10, 'INT') + if check: + self.game.log(f'You examine the {ent.name}. {check}') + self.game.log(f'The abilities of {ent.name} are:') + for ability, score in ent.ability_stats.items(): + self.game.log(f'{ability}: {score}') + else: + self.game.log(f'You fail to examine the {ent.name}. {check}') @self.add_option('e', 'Equip an item...', [InventoryMenu, BodyPartMenu, MainMenu]) def _equip(args): diff --git a/entity.py b/entity.py index 883dbec..30c4d36 100644 --- a/entity.py +++ b/entity.py @@ -2,6 +2,7 @@ import typing import body import random +from stats import AbilityStats from actions import Action, create_action @@ -31,12 +32,13 @@ def __hash__(self): return hash(self.name) -class LivingEntity(Entity): +class LivingEntity(Entity, AbilityStats): """Represents an entity that has a body and is able to die.""" def __init__(self, game: 'Game', name: str): Entity.__init__(self, game, name) + AbilityStats.__init__(self) # the body of the entity self.body = body.HumanoidBody(game, self) diff --git a/stats.py b/stats.py new file mode 100644 index 0000000..9ec7766 --- /dev/null +++ b/stats.py @@ -0,0 +1,74 @@ + +import random +from dataclasses import dataclass + + +@dataclass +class StatCheck: + + # the ability that was checked + ability: str + + # the minimum stat to "pass" this check + difficulty: int + + # the base stat of the creature + base: int + + # a list of rolls to add onto this base stat + rolls: list + + def get_total(self): + return (self.base + sum(self.rolls)) + + def is_passed(self): + return self.get_total() >= self.difficulty + + def get_percent(self): + return self.get_total() / self.difficulty + + def __str__(self): + eq = str(self.base) + for roll in self.rolls: + eq += f'+{roll}' + + if self.is_passed(): + return f'({self.ability}?{self.difficulty} => {eq} success)' + else: + return f'({self.ability}?{self.difficulty} => {eq} failure)' + + def __bool__(self): + return self.is_passed() + + +class AbilityStats: + + def __init__(self): + + self.ability_stats = {} + + points = 20 + abilities = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHR'] + + for ability in abilities: + self.ability_stats[ability] = 0 + + for _ in range(points): + self.ability_stats[random.choice(abilities)] += 1 + + def get_base_ability_score(self, ability): + """Get the score for an ability.""" + return self.ability_stats[ability] + + def ability_check(self, difficulty, ability): + + rolls = [] + for _ in range(2): # number of rolls + rolls.append(random.randint(1, 6)) + + return StatCheck( + ability, + difficulty, + self.get_base_ability_score(ability), + rolls + ) From 7059e60f4c418045d21d4a3bced8c743d17b8587 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Tue, 23 Jun 2020 22:44:25 -1000 Subject: [PATCH 37/39] implement attacking with ability checks --- body.py | 13 +++++++++++++ enemies.py | 2 +- entity.py | 41 +++++++++++++++++++++++++++++++---------- stats.py | 8 ++++++++ 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/body.py b/body.py index bfd54de..21ede02 100644 --- a/body.py +++ b/body.py @@ -54,6 +54,19 @@ class BodyPart: # TODO: per-part health + def get_base_damage(self): + """Get the damage of the weapon this part is holding, or the damage of + this body part if it can attack unarmed.""" + + if self.can_attack_armed and self.held_item: + return self.held_item.damage + + elif self.can_attack_bare: + return 1.0 + + else: + return 0.0 + class Body: """A representation of parts composing an entitys' body.""" diff --git a/enemies.py b/enemies.py index 3f54ca9..ca0e03e 100644 --- a/enemies.py +++ b/enemies.py @@ -22,7 +22,7 @@ def think(self): if type(ent) is Player: def _attack(): - self.game.log(f'{self.name} is attacking {ent.name}!', fg=1) + # self.game.log(f'{self.name} is attacking {ent.name}!', fg=1) self.attack(ent) return create_action('', _attack) diff --git a/entity.py b/entity.py index 30c4d36..1ff04d2 100644 --- a/entity.py +++ b/entity.py @@ -55,6 +55,10 @@ def is_alive(self) -> int: def attack(self, ent: Entity): """Attack an entity.""" + # attacker checks + dmg_check = self.ability_check(10, 'STR') + hit_check = self.ability_roll('DEX', 3) + # get body parts that can attack parts = [part for part in list(self.body.parts.values()) if part.can_attack_bare or @@ -67,28 +71,45 @@ def attack(self, ent: Entity): return # TODO: be able to choose a specific way to attack - # for now pick one at random - part = random.choice(parts) + # for now automatically find the best weapon + part = parts[0] + for _part in parts: + if _part.get_base_damage() > part.get_base_damage(): + part = _part # calculate damage to deal dmg = 1 + if part.can_attack_armed and part.held_item: - self.game.log( - f'{self.name} attacks {ent.name} with {part.name} holding {part.held_item.name}!' - ) + # self.game.log( + # f'{self.name} attacks {ent.name} with {part.name} holding {part.held_item.name}!' + # ) dmg = part.held_item.damage + weapon = part.held_item elif part.can_attack_bare: - self.game.log( - f'{self.name} attacks {ent.name} with the {part.name}!' - ) + # self.game.log( + # f'{self.name} attacks {ent.name} with the {part.name}!' + # ) + weapon = part # TODO: implement unarmed damage stat # calculate effect of damage if isinstance(ent, LivingEntity): - hurt_part = ent.hurt(dmg) - self.game.log(f'{self.name} does {dmg} damage to the {hurt_part.name}!') + # defender checks + dodge_check = ent.ability_check(sum(hit_check), 'DEX') + + if dodge_check: + # self.game.log(f'The attack misses!') + self.game.log(f'{self.name} misses an attack on {ent.name}!') + else: + self.game.log(f'{self.name} hits {ent.name} with a {weapon.name}!') + def_check = ent.ability_check(10, 'CON') + + dmg = int(dmg * dmg_check.get_percent()) + 1 + hurt_part = ent.hurt(dmg) + self.game.info(f'{ent.name} takes {dmg} damage to the {hurt_part.name}!') else: self.game.log('It does nothing!') diff --git a/stats.py b/stats.py index 9ec7766..4da8dd0 100644 --- a/stats.py +++ b/stats.py @@ -60,6 +60,14 @@ def get_base_ability_score(self, ability): """Get the score for an ability.""" return self.ability_stats[ability] + def ability_roll(self, ability, num_dice=2): + + rolls = [] + for _ in range(num_dice): # number of rolls + rolls.append(random.randint(1, 6)) + + return [self.get_base_ability_score(ability)] + rolls + def ability_check(self, difficulty, ability): rolls = [] From 4120c7bd2b2eeb1bf6e302d6bfb95014224ee810 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Tue, 23 Jun 2020 22:59:21 -1000 Subject: [PATCH 38/39] improvements on attack action --- actionmenu.py | 14 ++++++++++---- enemies.py | 2 +- entity.py | 22 +++++++++++++++++----- game.py | 2 +- main.py | 4 ++-- main_curses.py | 4 ++-- turns.py | 4 ++-- 7 files changed, 35 insertions(+), 17 deletions(-) diff --git a/actionmenu.py b/actionmenu.py index 5c4fc2c..f64cdab 100644 --- a/actionmenu.py +++ b/actionmenu.py @@ -161,7 +161,7 @@ def _self_attack(): for i, ent in enumerate(self.game.room.entities): - @self.add_option(str(i+2), f'{ent.name}') + @self.add_option(str(i+2), f'{ent.get_name()}') def _attack(ent=ent): self.args['ent'] = ent @@ -182,12 +182,12 @@ def _examine(args): ent = args['ent'] check = self.game.player.ability_check(10, 'INT') if check: - self.game.log(f'You examine the {ent.name}. {check}') - self.game.log(f'The abilities of {ent.name} are:') + self.game.log(f'You examine the {ent.get_name()}. {check}') + self.game.log(f'The abilities of {ent.get_name()} are:') for ability, score in ent.ability_stats.items(): self.game.log(f'{ability}: {score}') else: - self.game.log(f'You fail to examine the {ent.name}. {check}') + self.game.log(f'You fail to examine the {ent.get_name()}. {check}') @self.add_option('e', 'Equip an item...', [InventoryMenu, BodyPartMenu, MainMenu]) def _equip(args): @@ -195,6 +195,12 @@ def _equip(args): part.held_item = item self.game.log(f'your {part.name} is now holding {item.name}') + @self.add_option('t', 'Take...', [EntityMenu, MainMenu]) + def _take(args): + ent = args['ent'] + self.game.player.give_item(ent) + # self.game.log(f'You take the {ent.get_name()}') + @self.add_option('k', 'Attack...', [EntityMenu, MainMenu]) def _attack(args): self.game.player.attack(args['ent']) diff --git a/enemies.py b/enemies.py index ca0e03e..6d04a75 100644 --- a/enemies.py +++ b/enemies.py @@ -22,7 +22,7 @@ def think(self): if type(ent) is Player: def _attack(): - # self.game.log(f'{self.name} is attacking {ent.name}!', fg=1) + # self.game.log(f'{self.name} is attacking {ent.get_name()}!', fg=1) self.attack(ent) return create_action('', _attack) diff --git a/entity.py b/entity.py index 1ff04d2..3047b84 100644 --- a/entity.py +++ b/entity.py @@ -21,6 +21,9 @@ def __init__(self, game: 'Game', name: str): # the name of the entity self.name = name + def get_name(self) -> str: + return self.name + def __eq__(self, o): if type(self) == type(o): @@ -49,6 +52,12 @@ def hurt(self, damage): def get_health(self) -> int: return self.body.health + def get_name(self) -> str: + if self.is_alive(): + return self.name + else: + return 'corpse of a ' + self.name + def is_alive(self) -> int: return self.body.is_alive() @@ -83,13 +92,13 @@ def attack(self, ent: Entity): if part.can_attack_armed and part.held_item: # self.game.log( - # f'{self.name} attacks {ent.name} with {part.name} holding {part.held_item.name}!' + # f'{self.name} attacks {ent.get_name()} with {part.name} holding {part.held_item.name}!' # ) dmg = part.held_item.damage weapon = part.held_item elif part.can_attack_bare: # self.game.log( - # f'{self.name} attacks {ent.name} with the {part.name}!' + # f'{self.name} attacks {ent.get_name()} with the {part.name}!' # ) weapon = part # TODO: implement unarmed damage stat @@ -102,14 +111,17 @@ def attack(self, ent: Entity): if dodge_check: # self.game.log(f'The attack misses!') - self.game.log(f'{self.name} misses an attack on {ent.name}!') + self.game.log(f'{self.name} misses an attack on {ent.get_name()}!') else: - self.game.log(f'{self.name} hits {ent.name} with a {weapon.name}!') + self.game.log(f'{self.name} hits {ent.get_name()} with a {weapon.name}!') def_check = ent.ability_check(10, 'CON') dmg = int(dmg * dmg_check.get_percent()) + 1 hurt_part = ent.hurt(dmg) - self.game.info(f'{ent.name} takes {dmg} damage to the {hurt_part.name}!') + self.game.info(f'{ent.get_name()} takes {dmg} damage to the {hurt_part.name}!') + if not ent.is_alive(): + self.game.info(f'{ent.get_name()} has died') + else: self.game.log('It does nothing!') diff --git a/game.py b/game.py index c635db7..40288fb 100644 --- a/game.py +++ b/game.py @@ -204,7 +204,7 @@ def get_description(self, skill_check: int = 10): # get entities in room for ent in self.room.entities: - lines += [f"You see a {ent.name}."] + lines += [f"You see a {ent.get_name()}."] return ' '.join(lines) diff --git a/main.py b/main.py index ea3f93c..9358d66 100644 --- a/main.py +++ b/main.py @@ -104,12 +104,12 @@ def on_draw(): # draw health of other entities hp_y = height for ent in [ent for ent in game.room.entities if isinstance(ent, LivingEntity)]: - draw_text(width - 200, hp_y, f'{ent.name}: {ent.get_health()}', fg=0, bg=11) + draw_text(width - 200, hp_y, f'{ent.get_name()}: {ent.get_health()}', fg=0, bg=11) hp_y -= 25 # draw turns draw_text(16 * 12, height-1, - f' Turn: {game.turns.get_last_entity().name}, Order: {[ent.name for ent in game.turns.order]} ', + f' Turn: {game.turns.get_last_entity().name}, Order: {[ent.get_name() for ent in game.turns.order]} ', fg=0, bg=10) # draw fps diff --git a/main_curses.py b/main_curses.py index 5338ee5..ee529ef 100644 --- a/main_curses.py +++ b/main_curses.py @@ -108,7 +108,7 @@ def on_draw(): # draw turns draw_text(16 * 12, height-1, - f' Turn: {game.turns.get_last_entity().name}, Order: {[ent.name for ent in game.turns.order]} ', + f' Turn: {game.turns.get_last_entity().name}, Order: {[ent.get_name() for ent in game.turns.order]} ', fg=0, bg=10) # draw fps @@ -174,7 +174,7 @@ def on_draw(scr: CursesScreen, key: str): # draw turns scr.draw_text(16, height-1, - f' Turn: {game.turns.get_last_entity().name}, Order: {[ent.name for ent in game.turns.order]} ', + f' Turn: {game.turns.get_last_entity().name}, Order: {[ent.get_name() for ent in game.turns.order]} ', fg=0, bg=10) # draw fps diff --git a/turns.py b/turns.py index f660e84..71e62d2 100644 --- a/turns.py +++ b/turns.py @@ -29,7 +29,7 @@ def start_order(self): if isinstance(ent, LivingEntity)] # if len(order): - # self.game.info(f'** order: {[ent.name for ent in order]} **') + # self.game.info(f'** order: {[ent.get_name() for ent in order]} **') self.idx = 0 self.last_idx = 0 @@ -49,7 +49,7 @@ def run_next(self): ent = self.order[self.idx] if ent.is_alive(): - # self.game.info(f'it is now {ent.name} turn') + # self.game.info(f'it is now {ent.get_name()} turn') action = ent.think() if action: action.do_action() From d13cc6777d5dfaa32445dca04908922d2fe48b72 Mon Sep 17 00:00:00 2001 From: Branden Akana Date: Wed, 24 Jun 2020 01:26:33 -1000 Subject: [PATCH 39/39] implement look action --- actionmenu.py | 38 +++++++++++++++++++++++++++++++++++++- game.py | 37 ++++--------------------------------- stats.py | 6 ++++-- 3 files changed, 45 insertions(+), 36 deletions(-) diff --git a/actionmenu.py b/actionmenu.py index f64cdab..7b7c9d3 100644 --- a/actionmenu.py +++ b/actionmenu.py @@ -185,7 +185,7 @@ def _examine(args): self.game.log(f'You examine the {ent.get_name()}. {check}') self.game.log(f'The abilities of {ent.get_name()} are:') for ability, score in ent.ability_stats.items(): - self.game.log(f'{ability}: {score}') + self.game.log(f'{ability}: {score} ({score+2} - {score+12})') else: self.game.log(f'You fail to examine the {ent.get_name()}. {check}') @@ -204,3 +204,39 @@ def _take(args): @self.add_option('k', 'Attack...', [EntityMenu, MainMenu]) def _attack(args): self.game.player.attack(args['ent']) + + @self.add_option('l', 'Look around', [MainMenu]) + def _look(args): + """Get a description of the room based on what the player sees. + If a skill check is provided, change the description based + on the value of the skill check. + """ + room = self.game.room + + if not room: + return "You are out of bounds." + + lines = [] + + check = self.game.player.ability_check(2, 'WIS') + + self.game.info(f'You look around the room... {check}') + + if not check: # failed check + lines += ["You cannot see anything."] + + else: + if room.light_level == 0: + lines += ["It is too dark to see anything."] + + else: + # get number of paths + num_paths = len(self.game.get_movement_actions()) + lines += [f"There are {num_paths} paths."] + + # get entities in room + for ent in room.entities: + lines += [f"You see a {ent.get_name()}."] + + self.game.log(' '.join(lines)) + diff --git a/game.py b/game.py index 40288fb..724892c 100644 --- a/game.py +++ b/game.py @@ -2,7 +2,7 @@ import world import actions -import typing +from typing import (TYPE_CHECKING, List) from actionmenu import MainMenu from colors import Colors @@ -17,7 +17,7 @@ CommandAction = actions.CommandAction -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from screen import CursesScreen @@ -146,10 +146,10 @@ def update(self): # run next game tick self.turns.run_next() - def get_commands(self) -> list: + def get_commands(self) -> List[CommandAction]: """Get all commands that the player can do.""" - commands = [] + commands: List[CommandAction] = [] @CommandAction.register(commands, ['help'], 'get a list of commands') def _cmd_help(*args): @@ -179,35 +179,6 @@ def get_movement_actions(self) -> list: # TODO: move this into actionmenu.py return self.world.get_movement_actions(self.player.pos, self.player) - def get_description(self, skill_check: int = 10): - """Get a description of the room based on what the player sees. - If a skill check is provided, change the description based - on the value of the skill check. - """ - - if not self.room: - return "You are out of bounds." - - lines = [] - - if skill_check <= 0: # failed check - lines += ["You cannot see anything."] - - else: - if self.room.light_level == 0: - lines += ["It is too dark to see anything."] - - else: - # get number of paths - num_paths = len(self.get_movement_actions()) - lines += [f"There are {num_paths} paths."] - - # get entities in room - for ent in self.room.entities: - lines += [f"You see a {ent.get_name()}."] - - return ' '.join(lines) - # if __name__ == '__main__': # play() diff --git a/stats.py b/stats.py index 4da8dd0..f866d22 100644 --- a/stats.py +++ b/stats.py @@ -33,9 +33,11 @@ def __str__(self): eq += f'+{roll}' if self.is_passed(): - return f'({self.ability}?{self.difficulty} => {eq} success)' + # return f'({self.ability}?{self.difficulty} => {eq} success)' + return f'({self.ability} {self.difficulty}: success)' else: - return f'({self.ability}?{self.difficulty} => {eq} failure)' + # return f'({self.ability}?{self.difficulty} => {eq} failure)' + return f'({self.ability} {self.difficulty}: failure)' def __bool__(self): return self.is_passed()