Skip to content

Loads of fixes #88

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
db584df
Setup e2e test with 20 rounds
miketwo Oct 9, 2024
2f53820
Reset the bus at the start of a Game
miketwo Sep 28, 2024
d572b67
Add prioritized callbacks to Message Bus
miketwo Sep 30, 2024
e4ba377
Improve tests
miketwo Sep 28, 2024
7874570
Handle when choices are not possible
miketwo Dec 8, 2024
84531ea
Fix DistilledChaos logic
miketwo Dec 8, 2024
fd383fc
Fix a bunch of death-related errors
miketwo Sep 29, 2024
8f0e8a7
Fix the order of parameters
miketwo Dec 8, 2024
eed067f
Improvement: Automatically choose the only option in list_input
miketwo Dec 8, 2024
0dc0a9a
Fix Havoc
miketwo Dec 8, 2024
af14c58
Refactor Combat
miketwo Dec 8, 2024
c94182c
Add prioritized callbacks to Message Bus
miketwo Sep 30, 2024
3f0925b
Add (partial) Invulnerability
miketwo Oct 1, 2024
6c370ed
Update e2e tests to be smarter about ending turn when nothing is play…
miketwo Oct 8, 2024
d4bb436
Fix: Respect player's max energy
miketwo Dec 8, 2024
4c3ff43
Add ModeShift and SharpHide effects
miketwo Dec 8, 2024
c090161
Fix Havoc to modify the card to have 0 energy
miketwo Dec 8, 2024
836fdfb
Notes for later
miketwo Dec 8, 2024
be020a9
Be sure to set in_combat attribute
miketwo Dec 8, 2024
4a358c9
Big update to Guardian
miketwo Dec 8, 2024
6d7fe34
Performance: We don't need to create enemies if we're not using them
miketwo Dec 8, 2024
e6e0502
Fix TrueGrit
miketwo Oct 8, 2024
c79a430
Handle when no choices available in list_input
miketwo Oct 8, 2024
5e9da7b
Avoid recursion error by moving card before using it.
miketwo Oct 8, 2024
ec5c965
Move turn-taking code into methods
miketwo Oct 8, 2024
5496aca
Ability to Draw Cards without clearing hand
miketwo Oct 8, 2024
48c6677
A little better error checking in message bus tools
miketwo Oct 8, 2024
db413d7
Assertions to catch bad conditions
miketwo Oct 8, 2024
96dc170
Check if player is dead even when damage isn't taken
miketwo Oct 8, 2024
c8e3589
Typing
miketwo Oct 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 35 additions & 23 deletions card_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def pretty_print(self):
def upgrade_markers(self):
self.info += '<green>+</green>'
self.upgraded = True
return self

def modify_energy_cost(self, amount, modify_type='Adjust', one_turn=False):
if not (modify_type == 'Set' and amount != self.energy_cost) or not (modify_type == 'Adjust' and amount != 0):
Expand All @@ -60,6 +61,7 @@ def modify_energy_cost(self, amount, modify_type='Adjust', one_turn=False):
ansiprint(f"{self.name} got its energy set to {amount}.")
if one_turn:
self.reset_energy_next_turn = True
return self

def modify_damage(self, amount, context: str, permanent=False):
if permanent:
Expand All @@ -68,13 +70,15 @@ def modify_damage(self, amount, context: str, permanent=False):
self.damage += amount
self.damage_affected_by.append(context)
ansiprint(f"{self.name} had its damage modified by {amount} from {context}.")
return self

def modify_block(self, amount, context: str, permanent=False):
if permanent:
self.base_block += amount
else:
self.block += amount
self.block_affected_by.append(context)
return self

def is_upgradeable(self) -> bool:
return not self.upgraded and (self.name == "Burn" or self.type not in (CardType.STATUS, CardType.CURSE))
Expand Down Expand Up @@ -164,7 +168,8 @@ def apply(self, origin):
origin.blocking(card=self)
if not self.upgraded and len(origin.hand) > 0:
chosen_card = view.list_input("Choose a card to upgrade", origin.hand, view.view_piles, lambda card: card.is_upgradeable(), "That card is not upgradeable.")
origin.hand[chosen_card].upgrade()
if chosen_card is not None:
origin.hand[chosen_card].upgrade()
else:
for card in (card for card in origin.hand if card.is_upgradeable()):
card.upgrade()
Expand Down Expand Up @@ -264,14 +269,13 @@ def upgrade(self):
self.upgrade_markers()
self.energy_cost = 0

def apply(self, origin, enemies):
def apply(self, origin: Player, enemies):
if len(origin.draw_pile) == 0:
print("You have no cards in your draw pile.")
return
top_card = origin.draw_pile[-1]
if top_card.target in (TargetType.SINGLE, TargetType.YOURSELF):
origin.use_card(top_card, True, origin.draw_pile, random.choice(enemies))
elif top_card.target in (TargetType.AREA, TargetType.ANY):
origin.use_card(top_card, True, origin.draw_pile, enemies)
else:
origin.use_card(top_card, True, origin.draw_pile, random.choice(enemies))
top_card.modify_energy_cost(0, 'Set', True)
origin.use_card(card=top_card, exhaust=True, pile=origin.draw_pile, enemies=enemies, target=random.choice(enemies))

class Headbutt(Card):
def __init__(self):
Expand All @@ -289,7 +293,8 @@ def upgrade(self):
def apply(self, origin, target):
origin.attack(target, self)
chosen_card = view.list_input("Choose a card to put on top of your draw pile", origin.discard_pile, view.view_piles)
origin.draw_pile.append(origin.discard_pile.pop(chosen_card))
if chosen_card is not None:
origin.draw_pile.append(origin.discard_pile.pop(chosen_card))

class HeavyBlade(Card):
def __init__(self):
Expand Down Expand Up @@ -376,7 +381,7 @@ def upgrade(self):

def apply(self, origin, target):
origin.attack(target, self)
origin.draw_cards(self.cards)
origin.draw_cards(self.cards, clear_hand=False)

class ShrugItOff(Card):
def __init__(self):
Expand All @@ -393,7 +398,7 @@ def upgrade(self):

def apply(self, origin):
origin.blocking(card=self)
origin.draw_cards(1)
origin.draw_cards(1, clear_hand=False)

class SwordBoomerang(Card):
def __init__(self):
Expand Down Expand Up @@ -443,14 +448,19 @@ def upgrade(self):
self.base_block, self.block = 9, 9
self.info = "Gain 9 <keyword>Block</keyword>. <keyword>Exhaust</keyword> a card in your hand."

def apply(self, origin):
def apply(self, origin: Player):
origin.blocking(card=self)
if self.upgraded is True:
chosen_card = view.list_input("Choose a card to <keyword>Exhaust</keyword>", origin.hand, view.view_piles, lambda card: card.upgradeable is True and card.upgraded is False, "That card is either not upgradeable or is already upgraded.")
origin.move_card(origin.hand[chosen_card], origin.exhaust_pile, origin.hand, False)
if chosen_card is not None:
origin.move_card(origin.hand[chosen_card], origin.exhaust_pile, origin.hand, False)
else:
random_card = random.choice([card for card in origin.hand if card.upgradeable is True and card.upgraded is False])
origin.move_card(random_card, origin.exhaust_pile, origin.hand, False)
cards_to_choose = [card for card in origin.hand if card.upgradeable is True and card.upgraded is False]
if len(cards_to_choose) > 0:
random_card = random.choice(cards_to_choose)
origin.move_card(random_card, origin.exhaust_pile, origin.hand, False)
else:
print("You have no upgradeable cards to exhaust.")

class TwinStrike(Card):
def __init__(self):
Expand Down Expand Up @@ -481,9 +491,10 @@ def upgrade(self):
self.info = "Draw 2 cards. Put a card from your hand on top of your draw pile. <keyword>Exhaust</keyword>."

def apply(self, origin):
origin.draw_cards(self.cards)
origin.draw_cards(self.cards, clear_hand=False)
chosen_card = view.list_input("Choose a card to put on top of your draw pile", origin.hand, view.view_piles)
origin.move_card(origin.hand[chosen_card], origin.draw_pile, origin.hand, False)
if chosen_card is not None:
origin.move_card(origin.hand[chosen_card], origin.draw_pile, origin.hand, False)

class WildStrike(Card):
def __init__(self):
Expand Down Expand Up @@ -514,7 +525,7 @@ def upgrade(self):
self.info = "Draw 4 cards. You can't draw additional cards this turn."

def apply(self, origin):
origin.draw_cards(cards=self.cards)
origin.draw_cards(cards=self.cards, clear_hand=False)
ei.apply_effect(origin, None, effect_catalog.NoDraw)

class BloodForBlood(Card):
Expand Down Expand Up @@ -552,7 +563,7 @@ def upgrade(self):

def apply(self, origin):
origin.take_sourceless_dmg(3)
origin.draw_cards(cards=self.cards)
origin.draw_cards(cards=self.cards, clear_hand=False)

class BurningPact(Card):
def __init__(self):
Expand All @@ -569,7 +580,7 @@ def apply(self, origin):
chosen_card = view.list_input("Choose a card to <keyword>Exhaust</keyword>", origin.hand, view.view_piles)
if chosen_card is not None:
origin.move_card(origin.hand[chosen_card], origin.exhaust_pile, origin.hand, False)
origin.draw_cards(cards=self.cards)
origin.draw_cards(cards=self.cards, clear_hand=False)

class Carnage(Card):
def __init__(self):
Expand Down Expand Up @@ -648,7 +659,7 @@ def apply(self, origin: Player, target: Enemy):
origin.attack(target, self)
if effect_catalog.effect_amount(effect_catalog.Vulnerable, target.debuffs) >= 1:
origin.energy += 1
origin.draw_cards(cards=1)
origin.draw_cards(cards=1, clear_hand=False)

def callback(self, message, data):
# Unnecessary. We can modify the energy and draw a card directly in the apply method
Expand All @@ -673,8 +684,9 @@ def apply(self, origin):
view.view_piles,
validator=lambda card: card.type in (CardType.ATTACK, CardType.POWER),
message_when_invalid="That card is neither an Attack or a Power.")
for _ in range(self.copies):
origin.hand.insert(chosen_card, origin.hand[chosen_card])
if chosen_card is not None:
for _ in range(self.copies):
origin.hand.insert(chosen_card, origin.hand[chosen_card])

class Entrench(Card):
def __init__(self):
Expand Down
100 changes: 57 additions & 43 deletions combat.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,60 +33,72 @@ def __init__(self, tier: CombatTier, player: Player, game_map: game_map.GameMap,
def active_enemies(self):
return [enemy for enemy in self.all_enemies if enemy.state == State.ALIVE]

def combat(self) -> None:
"""There's too much to say here."""
self.start_combat()
# Combat automatically ends when all enemies are dead.
while len(self.active_enemies) > 0:
bus.publish(Message.START_OF_TURN, (self.turn, self.player))
while True:
self.on_player_move()
if all((enemy.state == State.DEAD for enemy in self.all_enemies)):
self.end_combat(killed_enemies=True)
break

print(f"Turn {self.turn}: ")
# Shows the player's potions, cards(in hand), amount of cards in discard and draw pile, and shows the status for you and the enemies.
view.display_ui(self.player, self.active_enemies)
print("1-0: Play card, P: Play Potion, M: View Map, D: View Deck, A: View Draw Pile, S: View Discard Pile, X: View Exhaust Pile, E: End Turn, F: View Debuffs and Buffs")
action = input("> ").lower()
other_options = {
"d": lambda: view.view_piles(self.player.deck, end=True),
"a": lambda: view.view_piles(
self.player.draw_pile, shuffle=True, end=True
),
"s": lambda: view.view_piles(self.player.discard_pile, end=True),
"x": lambda: view.view_piles(self.player.exhaust_pile, end=True),
"p": self.play_potion,
"f": lambda: ei.full_view(self.player, self.active_enemies),
"m": lambda: view.view_map(self.game_map),
}
if action.isdigit():
option = int(action) - 1
if option + 1 in range(len(self.player.hand) + 1):
self.play_new_card(self.player.hand[option])
else:
view.clear()
continue
elif action in other_options:
other_options[action]()
elif action == "e":
view.clear()
break
def end_conditions(self) -> bool:
'''Returns True if the combat should end, False otherwise.'''
return len(self.active_enemies) <= 0 or \
self.player.state == State.DEAD

def take_turn(self):
killed, escaped, robbed = False, False, False
while True:
self.on_player_move()
killed = all((enemy.state == State.DEAD for enemy in self.all_enemies))
escaped = self.player.state == State.ESCAPED
if any([killed, escaped, robbed]):
return killed, escaped, robbed
print(f"Turn {self.turn}: ")
# Shows the player's potions, cards(in hand), amount of cards in discard and draw pile, and shows the status for you and the enemies.
view.display_ui(self.player, self.active_enemies)
print("1-0: Play card, P: Play Potion, M: View Map, D: View Deck, A: View Draw Pile, S: View Discard Pile, X: View Exhaust Pile, E: End Turn, F: View Debuffs and Buffs")
action = input("> ").lower()
other_options = {
"d": lambda: view.view_piles(self.player.deck, end=True),
"a": lambda: view.view_piles(
self.player.draw_pile, shuffle=True, end=True
),
"s": lambda: view.view_piles(self.player.discard_pile, end=True),
"x": lambda: view.view_piles(self.player.exhaust_pile, end=True),
"p": self.play_potion,
"f": lambda: ei.full_view(self.player, self.active_enemies),
"m": lambda: view.view_map(self.game_map),
}
if action.isdigit():
option = int(action) - 1
if option + 1 in range(len(self.player.hand) + 1):
self.play_new_card(self.player.hand[option])
else:
view.clear()
continue
sleep(1)
elif action in other_options:
other_options[action]()
elif action == "e":
view.clear()
break
else:
view.clear()
if self.player.state == State.ESCAPED:
self.end_combat(self, escaped=True)
continue
sleep(1)
view.clear()
return killed, escaped, robbed

def combat(self) -> None:
"""There's too much to say here."""
self.start_combat()
while not self.end_conditions():
assert self.player.health > 0, "Player's death undetected."
for enemy in self.active_enemies:
assert enemy.health > 0, f"Enemy {enemy.name}'s death undetected."
bus.publish(Message.START_OF_TURN, (self.turn, self.player))
killed, escaped, robbed = self.take_turn()
bus.publish(Message.END_OF_TURN, data=(self.player, self.all_enemies))
self.turn += 1
self.end_combat(killed_enemies=killed, escaped=escaped, robbed=robbed)

def end_combat(self, killed_enemies=False, escaped=False, robbed=False):
if killed_enemies is True:
potion_roll = random.random()
ansiprint("<green>Combat finished!</green>")
self.player.in_combat = False
self.player.gain_gold(random.randint(10, 20))
if (potion_roll < self.player.potion_dropchance):
gen.claim_potions(True, 1, self.player, potion_catalog.create_all_potions())
Expand Down Expand Up @@ -198,6 +210,8 @@ def play_potion(self):
ansiprint("<red>You have no potions.</red>")
return
chosen_potion = view.list_input("Choose a potion to play", self.player.potions, view.view_potions, lambda potion: potion.playable, "That potion is not playable.")
if chosen_potion is None:
return
potion = self.player.potions.pop(chosen_potion)
if potion.target == TargetType.YOURSELF:
potion.apply(self.player)
Expand Down
10 changes: 10 additions & 0 deletions displayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def view_map(game_map):
input("Press enter to leave > ")

def display_ui(entity, enemies, combat=True):
assert all(x is not None for x in entity.hand)
# Repeats for every card in the entity's hand
ansiprint("<bold>Relics: </bold>")
view_relics(entity.relics)
Expand All @@ -94,6 +95,15 @@ def list_input(input_string: str, choices: list, displayer: Callable,
"""Allows the player to choose from a certain list of options. Includes validation."""
if extra_allowables is None:
extra_allowables = []
valid_choices = [choice for choice in choices if validator(choice)] + extra_allowables
if len(valid_choices) == 0:
ansiprint("<red>There are no valid choices.</red>")
return None
# Automatically choose the only option if there is only one
if len(choices) + len(extra_allowables) == 1:
if len(choices) == 1:
return 0
return extra_allowables[0]
while True:
try:
displayer(choices, validator=validator)
Expand Down
Loading
Loading