├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── donsol.py ├── game ├── __init__.py └── models.py ├── requirements.txt └── tests ├── __init__.py ├── test_card.py ├── test_deck.py ├── test_dungeon.py ├── test_player.py ├── test_renderer.py ├── test_room.py └── test_shield.py /.gitignore: -------------------------------------------------------------------------------- 1 | # python 2 | __pycache__ 3 | 4 | # pytest 5 | .cache/ 6 | 7 | # coverage 8 | .coverage 9 | *,cover 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.5" 6 | - "3.5-dev" # 3.5 development branch 7 | - "3.6" 8 | - "3.6-dev" # 3.6 development branch 9 | - "3.7-dev" # 3.7 development branch 10 | - "nightly" 11 | 12 | install: 13 | - pip install -r requirements.txt 14 | 15 | script: 16 | - pytest 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Stephen Lindberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Donsol 2 | --- 3 | 4 | a blessed port of John Eternal and Devine Lu Linvega's card game [Donsol](http://wiki.xxiivv.com/donsol) 5 | 6 | *Confused?* Read the [rules](https://wiki.xxiivv.com/site/donsol.html) 7 | 8 | ![donsol game](http://i.imgur.com/NgemFMt.png) 9 | 10 | play in your terminal: 11 | ``` 12 | git clone https://github.com/setphen/Donsol 13 | cd Donsol 14 | pip install -r requirements.txt 15 | python donsol.py 16 | ``` 17 | 18 | Getting non-ASCII or unicode related errors? Use Python 3. 19 | 20 | development 21 | === 22 | 23 | - run tests with `pytest` 24 | - run coverage with `coverage run --source game -m pytest` then `coverage report` 25 | 26 | -------------------------------------------------------------------------------- /donsol.py: -------------------------------------------------------------------------------- 1 | #Donsol game 2 | import random 3 | import math 4 | from time import sleep 5 | import argparse 6 | from blessed import Terminal 7 | 8 | from game.models import * 9 | 10 | parser = argparse.ArgumentParser() 11 | 12 | TERM = Terminal() 13 | 14 | def main(): 15 | 16 | print(TERM.enter_fullscreen) 17 | 18 | print(TERM.clear) 19 | 20 | dungeon = Dungeon() 21 | renderer = Renderer(dungeon, TERM) 22 | 23 | history = [] 24 | 25 | with TERM.cbreak(): 26 | keyval = '' 27 | 28 | while keyval.lower() != 'q': 29 | 30 | #HANDLE INPUT 31 | dungeon.handle_input(keyval) 32 | 33 | #RENDER 34 | renderer.render() 35 | 36 | # with TERM.location(margin,margin+2): 37 | # if can_escape and PLAYER.health > 0: 38 | # print( PALETTE['shield'] (TERM.black_on_white('f>') + ' flee')) 39 | # else: 40 | # print(TERM.black('f> flee')) 41 | # 42 | # #Print my stats! 43 | # with TERM.location(y=margin*2): 44 | # healthbar = '#' * PLAYER.health 45 | # print( 46 | # TERM.move_x(margin) + "HEALTH: " + str(PLAYER.health) 47 | # + TERM.move_x(margin+12) + PALETTE['potion'] (healthbar) 48 | # ) 49 | # if PLAYER.shield: 50 | # if PLAYER.last_monster_value: 51 | # print(TERM.move_x(margin) + 52 | # "SHIELD: " 53 | # + str(PLAYER.shield.value) + " " 54 | # + TERM.move_x(margin+12) + PALETTE['shield'] ('#' * PLAYER.shield.value) 55 | # + TERM.red(" ≠" + str(PLAYER.last_monster_value))) 56 | # 57 | # else: 58 | # print( 59 | # TERM.move_x(margin) + "SHIELD: " + 60 | # str(PLAYER.shield.value) + 61 | # TERM.move_x(margin+12) + PALETTE['shield'] ('#' * PLAYER.shield.value)) 62 | # 63 | # #Print my messages! 64 | # with TERM.location(y=margin*3): 65 | # for message in history[-6:]: 66 | # print(TERM.move_x(margin) + TERM.yellow(message)) 67 | 68 | #SLEEP before getting INPUT 69 | # time.sleep(0.11) 70 | 71 | #GET INPUT 72 | keyval = TERM.inkey() 73 | 74 | 75 | print(TERM.exit_fullscreen) 76 | 77 | if __name__ == "__main__": 78 | main() 79 | -------------------------------------------------------------------------------- /game/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/setphen/Donsol/d4c158dec08051467a7d74579d74d223008222b9/game/__init__.py -------------------------------------------------------------------------------- /game/models.py: -------------------------------------------------------------------------------- 1 | # Donsol game 2 | import random 3 | import sys 4 | from time import time 5 | import logging 6 | 7 | 8 | HEART = 1 9 | DIAMOND = 2 10 | SPADE = 3 11 | CLUB = 4 12 | JOKER = 5 13 | 14 | SUIT_NAMES = {HEART: "Hearts", 15 | SPADE: "Spades", 16 | DIAMOND: "Diamonds", 17 | CLUB: "Clubs", 18 | JOKER: "Joker"} 19 | 20 | SUIT_TYPES = {HEART: "Potion", 21 | SPADE: "Monster", 22 | DIAMOND: "Shield", 23 | CLUB: "Monster", 24 | JOKER: "Donsol"} 25 | 26 | VALUES = {HEART: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 11, 11, 11], 27 | DIAMOND: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 11, 11, 11], 28 | CLUB: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17], 29 | SPADE: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17], 30 | JOKER: [21, 21]} 31 | 32 | CARD_POSITIONS = { 33 | 'j': 0, 34 | 'k': 1, 35 | 'l': 2, 36 | ';': 3 37 | } 38 | 39 | SUIT_ART = { 40 | SPADE: 41 | [ 42 | " ∭∭ ", 43 | " ◢ ⁕ ) ", 44 | " ╖▓▓╖ ", 45 | " m▟ m▟ ", 46 | ] 47 | , 48 | CLUB: 49 | [ 50 | " ∭∭ ", 51 | " ◢ ⁕ ) ", 52 | " ╖▓▓╖ ", 53 | " m▟ m▟ ", 54 | ] 55 | , 56 | JOKER: 57 | [ 58 | " ╱ ╲ ", 59 | " [ ۞ ] ", 60 | " ╲ ╱ ", 61 | " ҈ ҈ ҈ ҈ ", 62 | ] 63 | , 64 | HEART: 65 | [ 66 | " ╮◎╭ ", 67 | " ┋⇡┋ ", 68 | " ╭╛ ╘╮ ", 69 | " ╰━━━╯ ", 70 | ] 71 | , 72 | DIAMOND: 73 | [ 74 | " ▞▲▚ ", 75 | " ╱┏ ┓╲ ", 76 | " ╲┗ ┛╱ ", 77 | " ▚▼▞ ", 78 | ] 79 | , 80 | } 81 | 82 | class Card: 83 | 84 | def __init__(self, suit, value, name=None): 85 | self.suit = suit 86 | self.name = name or suit 87 | self.value = value 88 | 89 | def is_monster(self): 90 | return self.suit in [SPADE, CLUB, JOKER] 91 | 92 | def __str__(self): 93 | if self.suit == JOKER: 94 | return "Joker" 95 | return "{} of {}".format(self.value, SUIT_NAMES[self.suit]) 96 | 97 | def __repr__(self): 98 | return str(self) 99 | 100 | 101 | class Player: 102 | 103 | def __init__(self): 104 | self.max_health = 21 105 | self.health = self.max_health 106 | self.shield = None 107 | self.can_drink_potion = True 108 | self.escaped_last_room = False 109 | 110 | def handle_monster(self, monster_value): 111 | damage = monster_value 112 | if self.shield: 113 | broken, damage = self.shield.handle_monster(monster_value) 114 | if broken: 115 | self.shield = None 116 | self.health = max(0, self.health - damage) 117 | logging.getLogger('history').info('Fought %s monster, took %s damage' % (monster_value,damage)) 118 | self.can_drink_potion = True 119 | 120 | def equip_shield(self, value): 121 | self.shield = Shield(value) 122 | logging.getLogger('history').info('Equipped %s shield' % value) 123 | self.can_drink_potion = True 124 | 125 | def drink_potion(self, potion_value): 126 | if self.can_drink_potion: 127 | previous_health = self.health 128 | self.health = min(self.max_health, self.health+potion_value) 129 | #Log it 130 | msg = 'Drank potion, plus %s health' % (self.health - previous_health) 131 | logging.getLogger('history').info(msg) 132 | self.can_drink_potion = False 133 | else: 134 | logging.getLogger('history').info('Potion had no effect…') 135 | pass 136 | 137 | def enter_new_room(self, fled=False): 138 | self.escaped_last_room = fled 139 | 140 | 141 | class Shield: 142 | 143 | def __init__(self, value): 144 | self.value = value 145 | self.previous_value = None 146 | 147 | def handle_monster(self, monster_value): 148 | """Return tuple of broken boolean and damage taken""" 149 | broken = False 150 | if (self.previous_value is not None and 151 | monster_value >= self.previous_value): 152 | logging.getLogger('history').info('Shield Broke!') 153 | broken = True 154 | self.value = 0 155 | self.previous_value = monster_value 156 | return (broken, max(0, monster_value - self.value)) 157 | 158 | 159 | class Room: 160 | 161 | def __init__(self, cards, player_escaped_previous_room=False): 162 | self.slots = dict(zip('jkl;', cards)) 163 | self.player_escaped_previous_room = player_escaped_previous_room 164 | 165 | def flee(self): 166 | """ 167 | Fleeing returns None if unable to flee, else returns the set of 168 | cards to return to the deck 169 | """ 170 | if self.escapable(): 171 | #TODO: print flavor text here 172 | logging.getLogger('history').info('Fleeing the room') 173 | return self.slots.values() 174 | return None 175 | 176 | def select_card(self, key): 177 | """ 178 | Return the card at 'key' from the room, and remove it from the rooms' 179 | storage. If given an invalid key, return None 180 | """ 181 | try: 182 | return self.slots.pop(key) 183 | except KeyError: 184 | return None 185 | 186 | def completed(self): 187 | if self.slots: 188 | return False 189 | return True 190 | 191 | def escapable(self): 192 | """Check whether or not the room is escapable""" 193 | if self.player_escaped_previous_room: 194 | if self.has_monsters(): 195 | return False 196 | return True 197 | 198 | def has_monsters(self): 199 | if any(c.is_monster() for c in self.slots.values()): 200 | return True 201 | return False 202 | 203 | 204 | class Deck: 205 | """A deck holds an ordered set of cards and can pop or shuffle them""" 206 | 207 | def __init__(self, cards, seed=None): 208 | """Optionally accept a seed to make the deck deterministic""" 209 | self.cards = cards 210 | self.random = random.Random() 211 | if seed is None: 212 | seed = time() 213 | 214 | self.random.seed(seed) 215 | logging.getLogger('history').info("Deck's random seed is: {}".format(seed)) 216 | #print("Deck's random seed is: {}".format(seed)) 217 | 218 | 219 | def count(self): 220 | """Return how many cards remain in the deck""" 221 | return len(self.cards) 222 | 223 | def draw(self, count): 224 | """ 225 | Draw cards from the deck, or as many as are available. 226 | return a list of cards drawn. 227 | """ 228 | drawn = [] 229 | while len(drawn) < count: 230 | try: 231 | drawn.append(self.cards.pop()) 232 | except IndexError: 233 | break 234 | return drawn 235 | 236 | def shuffle(self): 237 | self.random.shuffle(self.cards) 238 | 239 | def add(self, cards): 240 | """Add the passed list of cards to the deck""" 241 | self.cards.extend(cards) 242 | 243 | 244 | def make_standard_deck(): 245 | cards = [] 246 | for suit in VALUES.keys(): 247 | for value in VALUES[suit]: 248 | cards.append(Card(suit, value)) 249 | return cards 250 | 251 | 252 | class Dungeon: 253 | """Handle deck, room and player creation and interaction""" 254 | 255 | def __init__(self, cards=None, seed=None): 256 | if not cards: 257 | cards = make_standard_deck() 258 | self.deck = Deck(cards=cards, seed=seed) 259 | self.deck.shuffle() 260 | self.player = Player() 261 | self.room_history = [] 262 | #self.event_history = [] 263 | self.generate_room() 264 | 265 | def generate_room(self, fled=False): 266 | self.player.enter_new_room(fled=fled) 267 | self.room_history.append(Room(self.deck.draw(4), fled)) 268 | 269 | def handle_input(self, input): 270 | if input == 'q': 271 | sys.exit() 272 | elif input in ['j', 'k', 'l', ';'] and self.player.health > 0: 273 | card = self.room_history[-1].select_card(input) 274 | if card: 275 | self.handle_card(card) 276 | if self.room_history[-1].completed(): 277 | self.generate_room() 278 | elif input == 'f' and self.room_history[-1].escapable() and self.player.health > 0: 279 | cards = self.room_history[-1].flee() 280 | self.handle_flee(cards) 281 | self.generate_room(fled=True) 282 | else: 283 | pass 284 | 285 | def handle_card(self, card): 286 | if card.suit == HEART: 287 | self.player.drink_potion(card.value) 288 | elif card.suit == DIAMOND: 289 | self.player.equip_shield(card.value) 290 | else: 291 | self.player.handle_monster(card.value) 292 | 293 | if self.player.health == 0: 294 | logging.getLogger('history').info('You died!') 295 | return 296 | 297 | if self.deck.count() == 0 and not self.room.has_monsters: 298 | logging.getLogger('history').info('You WON!') 299 | 300 | def handle_flee(self, cards): 301 | self.deck.add(cards) 302 | self.deck.shuffle() 303 | 304 | class GameLogHandler(logging.Handler): 305 | """Keep track of what is going on in the Dungeon""" 306 | 307 | def __init__(self, *args, **kwargs): 308 | super(GameLogHandler, self).__init__(*args, **kwargs) 309 | self.history = [] 310 | 311 | def get_history(self): 312 | return self.history 313 | 314 | def emit(self, record): 315 | self.history.append(record.getMessage()) 316 | 317 | 318 | 319 | 320 | class Renderer: 321 | """Render the dungeon and controls""" 322 | 323 | def __init__(self, dungeon, terminal, margin = 6, card_spacing = 18): 324 | self.dungeon = dungeon 325 | self.term = terminal 326 | 327 | # card positions: 328 | self.margin = margin 329 | self.spacing = card_spacing 330 | 331 | self.palette = {HEART: self.term.white, 332 | SPADE: self.term.bright_red, 333 | DIAMOND: self.term.white, 334 | CLUB: self.term.bright_red, 335 | JOKER: self.term.bright_blue} 336 | 337 | self.history_handler = GameLogHandler() 338 | logging.getLogger('history').setLevel(logging.INFO) 339 | logging.getLogger('history').addHandler(self.history_handler) 340 | 341 | def render(self): 342 | print(self.term.clear) 343 | 344 | slots = self.dungeon.room_history[-1].slots 345 | 346 | """ 347 | Print artwork 348 | """ 349 | 350 | for slot in slots: 351 | card = slots[slot] 352 | pos = CARD_POSITIONS[slot] * self.spacing 353 | color = self.palette[card.suit] 354 | 355 | for (y, line) in enumerate(SUIT_ART[card.suit]): 356 | print(self.term.move(1+y,self.margin + pos) + color(line)) 357 | 358 | """ 359 | Print card information 360 | """ 361 | 362 | 363 | for slot in slots: 364 | card = slots[slot] 365 | pos = CARD_POSITIONS[slot] * self.spacing 366 | color = self.palette[card.suit] 367 | #info = str(card.name).capitalize() + " " + str(card) 368 | # 369 | # for (y, line) in enumerate(SUIT_ART[card.suit]): 370 | # print(self.term.move(1+y,pos[0]) + line) 371 | 372 | print( 373 | self.term.move(self.margin, self.margin + pos) + 374 | self.term.black_on_white (slot + '>') + 375 | color(' ' + str(SUIT_TYPES[card.suit]) + ' ' + 376 | str(card.value)) + 377 | self.term.move(self.margin + 1, self.margin + pos) + 378 | self.term.blue (str(slots[slot])) 379 | ) 380 | 381 | #flee command 382 | if self.dungeon.room_history[-1].escapable(): 383 | print( 384 | self.term.move(self.margin + 5, self.margin) + 385 | self.term.black_on_white('f> flee') ) 386 | else: 387 | print( 388 | self.term.move(self.margin + 5, self.margin) + 389 | self.term.red('Can\'t flee') ) 390 | 391 | 392 | 393 | """ 394 | Print stats like shield, health, etc 395 | """ 396 | 397 | # shield 398 | if self.dungeon.player.shield: 399 | if self.dungeon.player.shield.previous_value: 400 | print(self.term.move(self.margin + 7, self.margin) + 401 | "Shield: " 402 | + str(self.dungeon.player.shield.value) + " " 403 | + self.term.move_x(self.margin + 12) + '#' * self.dungeon.player.shield.value 404 | + self.term.red(" ≠" + str(self.dungeon.player.shield.previous_value))) 405 | else: 406 | print( 407 | self.term.move(self.margin + 7, self.margin) + "Shield: " + 408 | str(self.dungeon.player.shield.value) + 409 | self.term.move_x(self.margin + 12) + '#' * self.dungeon.player.shield.value) 410 | 411 | # health 412 | if self.dungeon.player: 413 | print(self.term.move(self.margin + 8, self.margin) + 414 | "Health: " 415 | + str(self.dungeon.player.health) + " " 416 | + self.term.move_x(self.margin + 12) + '#' * self.dungeon.player.health) 417 | 418 | if self.dungeon.deck: 419 | print(self.term.move(self.margin + 9, self.margin) + 420 | "Cards Remaining: " 421 | + str(self.dungeon.deck.count())) 422 | 423 | 424 | 425 | #Print event history 426 | print(self.term.move(self.margin + 12, self.margin) + "History:") 427 | for index,x in enumerate(self.history_handler.get_history()[-4:]): 428 | print(self.term.move(self.margin + 12 + index, self.margin) + 429 | x) 430 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | blessed==1.14.2 2 | pytest 3 | coverage 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/setphen/Donsol/d4c158dec08051467a7d74579d74d223008222b9/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_card.py: -------------------------------------------------------------------------------- 1 | from game.models import (Card, 2 | HEART, 3 | SPADE, 4 | CLUB, 5 | DIAMOND) 6 | 7 | def test_card_creation_without_name(): 8 | c = Card('heart', 5) 9 | assert c.name == 'heart' 10 | assert c.suit == 'heart' 11 | assert c.value == 5 12 | 13 | 14 | def test_card_creation_with_name(): 15 | c = Card('spade', 5, name='jabberwock') 16 | assert c.name == 'jabberwock' 17 | assert c.suit == 'spade' 18 | assert c.value == 5 19 | 20 | 21 | def test_card_spade_is_monster(): 22 | c = Card(SPADE, 5) 23 | assert c.is_monster() == True 24 | 25 | 26 | def test_card_club_is_monster(): 27 | c = Card(CLUB, 5) 28 | assert c.is_monster() == True 29 | 30 | 31 | def test_card_heart_is_not_monster(): 32 | c = Card(HEART, 5) 33 | assert c.is_monster() == False 34 | 35 | 36 | def test_card_diamond_is_not_monster(): 37 | c = Card(DIAMOND, 5) 38 | assert c.is_monster() == False 39 | -------------------------------------------------------------------------------- /tests/test_deck.py: -------------------------------------------------------------------------------- 1 | from game.models import Deck, make_standard_deck 2 | 3 | def test_deck_with_seed_is_repeatable(): 4 | d = Deck(list(range(52)), seed=847576) 5 | d.shuffle() 6 | assert d.draw(10) == [11, 24, 46, 37, 25, 47, 18, 14, 8, 39] 7 | d2 = Deck(list(range(52)), seed=234987) 8 | d2.shuffle() 9 | assert d2.draw(10) == [14, 17, 23, 9, 10, 24, 36, 20, 49, 7] 10 | 11 | 12 | def test_deck_draw_removes_items(): 13 | d = Deck(list(range(16))) 14 | assert len(d.cards) == 16 15 | d.shuffle() 16 | d.draw(4) 17 | assert len(d.cards) == 12 18 | 19 | 20 | def test_drawing_past_end_of_deck(): 21 | d = Deck(list(range(3))) 22 | drawn = d.draw(4) 23 | assert len(d.cards) == 0 24 | assert len(drawn) == 3 25 | 26 | 27 | def test_deck_add(): 28 | d = Deck(list(range(5))) 29 | assert len(d.cards) == 5 30 | d.add(list(range(5))) 31 | assert len(d.cards) == 10 32 | 33 | 34 | def test_standard_deck(): 35 | cards = make_standard_deck() 36 | assert len(cards) == 54 37 | -------------------------------------------------------------------------------- /tests/test_dungeon.py: -------------------------------------------------------------------------------- 1 | from game.models import (Dungeon, 2 | Deck, 3 | Player, 4 | make_standard_deck) 5 | import pytest 6 | 7 | 8 | @pytest.fixture 9 | def dungeon(): 10 | return Dungeon(seed=123456789) 11 | 12 | 13 | def test_deck_order(dungeon): 14 | """this check ensures that we can plan for the first three rooms having 15 | known cards and thus we can check the availability of certain actions or 16 | sequences of actions""" 17 | # room 1 18 | assert str(dungeon.room_history[-1].slots['j']) == "4 of Spades" 19 | assert str(dungeon.room_history[-1].slots['k']) == "4 of Clubs" 20 | assert str(dungeon.room_history[-1].slots['l']) == "10 of Clubs" 21 | assert str(dungeon.room_history[-1].slots[';']) == "8 of Spades" 22 | dungeon.generate_room() 23 | # room 2 24 | assert str(dungeon.room_history[-1].slots['j']) == "17 of Clubs" 25 | assert str(dungeon.room_history[-1].slots['k']) == "11 of Diamonds" 26 | assert str(dungeon.room_history[-1].slots['l']) == "8 of Diamonds" 27 | assert str(dungeon.room_history[-1].slots[';']) == "7 of Spades" 28 | # room 3 29 | dungeon.generate_room() 30 | assert str(dungeon.room_history[-1].slots['j']) == "5 of Clubs" 31 | assert str(dungeon.room_history[-1].slots['k']) == "11 of Spades" 32 | assert str(dungeon.room_history[-1].slots['l']) == "17 of Spades" 33 | assert str(dungeon.room_history[-1].slots[';']) == "11 of Diamonds" 34 | # room 4 35 | dungeon.generate_room() 36 | assert str(dungeon.room_history[-1].slots['j']) == "9 of Spades" 37 | assert str(dungeon.room_history[-1].slots['k']) == "Joker" 38 | assert str(dungeon.room_history[-1].slots['l']) == "6 of Spades" 39 | assert str(dungeon.room_history[-1].slots[';']) == "2 of Diamonds" 40 | 41 | 42 | def test_dungeon_valid_flee_unconditioned(dungeon): 43 | dungeon.handle_input('f') 44 | assert len(dungeon.room_history) == 2 45 | 46 | 47 | def test_cannot_flee_twice(dungeon): 48 | assert dungeon.room_history[-1].escapable() == True 49 | dungeon.handle_input('f') 50 | assert dungeon.player.escaped_last_room == True 51 | assert dungeon.room_history[-1].escapable() == False 52 | dungeon.handle_input('f') 53 | assert len(dungeon.room_history) == 2 54 | 55 | 56 | def test_can_flee_after_clearing_monsters(dungeon): 57 | # skip the first room 58 | dungeon.generate_room() 59 | # second room 60 | print(dungeon.room_history[-1].slots) 61 | dungeon.handle_input('k') # equip the 11 shield 62 | assert dungeon.player.shield.value == 11 63 | dungeon.handle_input('f') # this causes the deck to be reshuffled 64 | # third room 65 | assert len(dungeon.room_history) == 3 66 | dungeon.handle_input('f') # try to flee again 67 | assert len(dungeon.room_history) == 3 # couldn't escape 68 | dungeon.handle_input('l') # handle club 5 69 | dungeon.handle_input('j') # handle spade 2 70 | dungeon.handle_input('f') # try to flee 71 | assert len(dungeon.room_history) == 3 # couldn't escape 72 | dungeon.handle_input(';') # handle last monster 73 | dungeon.handle_input('f') # try to flee 74 | assert len(dungeon.room_history) == 4 # escaped! 75 | -------------------------------------------------------------------------------- /tests/test_player.py: -------------------------------------------------------------------------------- 1 | from game.models import Player, Shield 2 | 3 | 4 | def test_player_handle_monster(): 5 | p = Player() 6 | p.handle_monster(5) 7 | assert p.health == 16 8 | p.handle_monster(10) 9 | assert p.health == 6 10 | 11 | 12 | def test_player_handle_monster_with_shield(): 13 | p = Player() 14 | p.equip_shield(8) 15 | p.handle_monster(10) 16 | assert p.health == 19 17 | assert p.shield.previous_value == 10 18 | 19 | 20 | def test_player_shield_breaks(): 21 | p = Player() 22 | p.equip_shield(3) 23 | p.handle_monster(3) 24 | p.handle_monster(3) 25 | assert p.shield is None 26 | 27 | 28 | def test_player_equip_shield(): 29 | p = Player() 30 | p.equip_shield(4) 31 | p.handle_monster(4) 32 | assert p.health == 21 33 | 34 | def test_player_drink_potion_heals(): 35 | p = Player() 36 | p.health = 15 37 | p.drink_potion(5) 38 | assert p.health == 20 39 | -------------------------------------------------------------------------------- /tests/test_renderer.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import logging 3 | import sys 4 | from game.models import GameLogHandler 5 | 6 | @pytest.fixture 7 | def handler(): 8 | return GameLogHandler() 9 | 10 | def test_log(handler): 11 | log = logging.getLogger() 12 | log.setLevel(logging.INFO) 13 | log.addHandler(handler) 14 | log.info('hello!') 15 | print(handler.history) 16 | print(log.getEffectiveLevel()) 17 | assert len(handler.history) > 0 18 | -------------------------------------------------------------------------------- /tests/test_room.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from game.models import (Room, 3 | Card, 4 | HEART, 5 | DIAMOND, 6 | CLUB, 7 | SPADE) 8 | 9 | 10 | @pytest.fixture 11 | def cards(): 12 | return [ 13 | Card(HEART, 5, 'Elixir'), 14 | Card(SPADE, 2), 15 | Card(CLUB, 9, ''), 16 | Card(DIAMOND, 6, 'Stalwart'), 17 | ] 18 | 19 | 20 | def test_room_escapable(cards): 21 | r = Room(cards, player_escaped_previous_room=False) 22 | assert r.escapable() == True 23 | 24 | 25 | def test_room_previous_escaped(cards): 26 | r = Room(cards, player_escaped_previous_room=True) 27 | assert r.escapable() == False 28 | r.select_card('k') 29 | r.select_card('l') 30 | assert r.escapable() == True 31 | 32 | 33 | def test_room_select_card(cards): 34 | r = Room(cards, player_escaped_previous_room=True) 35 | card = r.select_card('k') 36 | assert card.suit == SPADE 37 | assert card.value == 2 38 | 39 | 40 | def test_room_flee_failure(cards): 41 | r = Room(cards, player_escaped_previous_room=True) 42 | assert r.flee() == None 43 | 44 | 45 | def test_room_flee_success(cards): 46 | r = Room(cards) 47 | assert set(r.flee()) == set(cards) 48 | 49 | 50 | def test_room_completed(cards): 51 | r = Room(cards) 52 | r.select_card('j') 53 | r.select_card('k') 54 | r.select_card('l') 55 | r.select_card(';') 56 | assert r.completed() == True 57 | 58 | 59 | def test_room_not_completed(cards): 60 | r = Room(cards) 61 | r.select_card(';') 62 | assert r.completed() == False 63 | -------------------------------------------------------------------------------- /tests/test_shield.py: -------------------------------------------------------------------------------- 1 | from game.models import Shield 2 | 3 | 4 | def test_new_shield_handle_monster_under_value(): 5 | s = Shield(8) 6 | broken, damage = s.handle_monster(6) 7 | assert not broken 8 | assert damage == 0 9 | assert s.previous_value == 6 10 | 11 | 12 | def test_new_shield_handle_monster_over_value(): 13 | s = Shield(5) 14 | broken, damage = s.handle_monster(10) 15 | assert not broken 16 | assert damage == 5 17 | assert s.previous_value == 10 18 | 19 | 20 | def test_shield_breaking(): 21 | s = Shield(10) 22 | broken, damage = s.handle_monster(8) 23 | assert damage == 0 24 | assert not broken 25 | broken, damage = s.handle_monster(13) 26 | assert broken 27 | assert s.value == 0 28 | assert damage == 13 29 | --------------------------------------------------------------------------------