《炉石传说》
IntroductionHearthstone is a strategy card game developed by Blizzard Entertainment. In this assignment, you will be implementing a text based game (very) loosely based on this title. Note that where the behaviour of the original game differs from this specification, this specification takes precedence, and you should implement the assignment as per it. In our version of Hearthstone, the player is a Hero tasked with defeating an enemy computer controlled Hero. Heroes battle by casting spells represented by cards which they draw from their personal card decks. These spells can summon minions to fight for their summoner, or directly attack (or defend) a Hero or minion.Getting StartedDownload a2.zip
from Blackboard. This archive contains the necessary files to start your assignment. Once extracted, the a2.zip
archive will provide the following files:* a2.py
: This is the only file you will submit and is where you write your code. Do not make changes to any other files.
support.py
: Do not modify or submit this file, it contains pre-defined constants to use in your assignment. In addition to these, you are encouraged to create your own constants ina2.py
where possible.display.py
: Do not modify or submit this file, it contains view classes to display your assignment in a visually appealing manner.gameplay/
: This folder contains a number of example outputs generated by playing the game using a fully-functional completed solution to this assignment. The purpose of the files in this folder is to help you understand how the game works, and how output should be formatted.levels/
: This folder contains a small collection of files used to initialize games of HearthStone. In addition to these, you are encouraged to create your own files to help test your implementation where possible.
GameplayThis section provides a high-level overview of gameplay, and is intended to provide you with an idea of the behaviour of a fully completed assignment. Where interactions are not explicitly mentioned in this section, please see the Implementation section.ImportantDo not simply implement the behaviour described here using your own structure; you must implement the individual functions, classes, and methods described in the Implementation section. Inputs and outputs must match exactly what is expected. Refer to the Implementation section, the given examples, and the provided Gradescope tests for clarification on required prompts and outputs.The game focusses on conflict between Two heroes, one controlled by the player, and one controlled by the computer. Heroes are a type of entity. All entities have a health and shield value. These values can be depleted by receiving damage. If an entity has a positive shield value, then its health cannot be depleted in this way. An entity is alive while its health value is above zero; it is defeated when its health drops to zero.Heroes also possess an energy level. Energy is a currency used to perform actions. A hero’s energy cannot exceed that hero’s energy capacity. Which is a value that gradually increases as the game progresses. Actions in Hearthstone are represented by cards. Heroes possess a deck of cards from which they draw into a hand. Cards can then be played from the hero’s hand in order to perform actions. In addition to the regular entity rules, a hero is also defeated if its deck contains zero cards. Heroes take turns playing cards, and at the start of a turn a hero draws from the top of their deck until their hand is full (that is contains the maximum number of cards). A hero can hold a maximum of 5 cards in their hand. Each hero can play any number of cards on their turn.In addition to a name and description, all cards have a cost and an effect. A cards cost is the energy that a hero must spend in order to play it. If a hero does not have enough energy to spend, it cannot play a given card. Instead of playing a card normally, a hero can choose to discard a given card. This moves the card from their hand to the bottom of their deck. A cards effect is what happens when that card is played. The types of effect are given in Table 1.Table 1: Types of effect. A card may have zero, one or multiple effect types.
Effect | Impact on target entity |
---|---|
Damage | For each point of damage, the opponent’s shield is reduced by 1. If the opponent’s shield is reduced to 0 and there is still damage remaining, the opponent’s health is reduced by the remaining amount. |
Shield | Increase entity’s shield by the given value |
Health | Increase entity’s health by the given value |
Usually, a cards effect requires selecting a target entity. The cards effect is then inflicted on the targeted entity, and the card is then removed from the game. However, some cards are permanent, and have special rules when played. When a permanent card is played, it is placed in a minion slot in front of the hero that played it. Each hero has set number of minion slots and they are filled from left to right. If a permanent card is to be placed but all minion slots are full, the card in that hero’s left most minion slot is removed from play, and all that hero’s minions are moved one slot left before the card is placed. A hero has 5 minion slots. Permanent cards in minion slots are Entities, and can be targeted by card effects. At the end of a hero’s turn, each permanent card in their minion slot (in order from left to right) independently selects a target and applies their effect to them. When a permanent card in a minion slot is defeated it is immediately removed from the game, and any permanent cards in slots to its right are moved left.The game ends when one of the following occurs:* The player wins, when their hero is not defeated, and their opponents hero is defeated.
- The player loses, when their hero is defeated.
After loading a game of Hearthstone from a given file, the player is welcomed and the following game loop occurs:1. The player is repeatedly prompted to enter a command until they enter a valid command. Valid commands are given in Table 2
2. Action is taken according to the command entered by the player. The appropriate actions are given in Table 2
3. The display is updated, informing the player of the results of their action.
4. If the game is over, gameplay advances immediately to Step 5. Otherwise, gameplay returns to Step 1.
5. The display is updated, informing the player of the game outcome (win or loss).
6. The program exits gracefully.
Table 2: Valid commands and the actions that should be taken. Commands are case insensitive. If the command entered by the user does not match one of the commands in this table, then no action should be taken, the display should be updated informing the user they entered an invalid command, and the program should return immediately to step 1. Constants for informing the player the result of their command can be found in support.py
.
Command | Action to take |
---|---|
Help | The display is updated, providing the player with a list of valid commands. |
Play X , where X is an integer between 1 and the number of cards in the player’s hand (inclusive) | The card at position X in the player’s hand is selected. If the card is not permanent, the player is repeatedly prompted to select an entity until they select a valid entity.Then, if the player’s current energy is below the cards cost, nothing happens. Otherwise, the player’s current energy is reduced by the cards cost and the card is removed from the player’s hand. Once the card is removed from the players hand, if it is permanent, it is placed in the player’s next available minion slot with 1 health and 0 shield. Otherwise, the card’s effects are then applied to the entity selected earlier. |
Discard X , where X is an integer between 1 and the number of cards in the player’s hand (inclusive) | The card at position X in the player’s hand is removed from the player’s hand, and added to the bottom of the player’s deck. |
Load F where F is the name of a file containing a game state. | If F is not an existing file, nothing happens. Otherwise, an attempt is made to load a new game state from F . If the attempt is successful, the game state is updated to that contained in F , otherwise nothing happens. |
End turn | Each of the player’s minions select a target and apply their effects to it. The enemy hero then (in order): Registers another turn on every Fireball card in their hand, draws from the top of their deck until their hand is full, Increases their energy capacity by 1, sets their energy level to their energy capacity. If the enemy hero is defeated, nothing more happens. Otherwise, the enemy hero then selects cards and plays them (The player will be informed what cards were played next time the display is updated). Each of the enemy’s minions then select a target and apply their effects to it. Finally, the player (in order): Registers another turn on each Fireball card in their hand, draws from the top of their deck until their hand is full, increases their energy capacity by 1, and sets their energy level to their energy capacity. Afterwards, If the game has not ended, the game state is saved to autosave.txt . |
ImplementaionImportantYou are not permitted to add any additional import statements to a2.py
. Doing so will result in a deduction of up to 100% of your mark. You must not modify or remove the import statements already provided to you in a2.py
. Removing or modifying these existing import statements may result in your code not functioning, and may result in a deduction of up to 100% of your mark.Required Classes and MethodsThis section outlines the classes and methods you are required to implement in a2.py
. Your program must operate exactly as specified. In particular, your program’s output must match exactly with the expected output. Your program will be marked automatically so minor differences in output (such as whitespace or casing) will cause tests to fail resulting in zero marks for that test.You will be following the Apple Model-View-Controller design pattern when implementing this assignment. You have been provided the view component, and are required to implement a number of classes in order to complete the Model and Controller components.The class diagram in Figure 1 provides an overview of all of the classes you must implement in your assignment, and the basic relationships between them. The details of these classes and their methods are described in depth later in this section. Within Figure 1😗 Orange classes are those provided to you in the supporting files.
- Green classes are abstract classes (superclasses). However, you are not required to enforce the abstract nature of the green classes in their implementation. The purpose of this distinction is to indicate to you that you should only ever instantiate the blue classes in your program (though you should instantiate the green classes to test them before beginning work on their subclasses).
- Blue classes are concrete classes (subclasses).
- Solid arrows indicate inheritance (i.e. the “is-a” relationship).
- Dotted arrows indicate composition (i.e. the “has-a” relationship). An arrow marked with 1-1 denotes that each instance of the class at the base of the arrow contains exactly one instance of the class at the head of the arrow. An arrow marked with 1-N denotes that each instance of the class at the base of the arrow may contain many instances of the class at the head of the arrow.
image.pngFigure 1: Basic UML diagram for the classes in assignment 2.NoteYou are awarded marks for the number of tests passed by your classes when they are tested independently of one another. Thus an incomplete assignment with some working classes may well be awarded more marks than a complete assignment with faulty classes.Each class is accompanied with some examples for usage to help you start your own testing. You should also test your methods with other values to ensure they operate according to the descriptions.The rest of this section describes what you must implement in a2.py
in detail. You should complete the model section before attempting the controller section, ensuring that everything you implement is tested thoroughly, operating correctly, and passes all relevant Gradescope tests. You will not be able to earn marks for the controller section until you have passed all Gradescope tests for the model section.
ModelThe following are the classes and methods you are required to implement as part of the model. You should develop the classes in the order in which they are described in this section and test each one (including on Gradescope) before moving on to the next class. Functionality marks are awarded for each class (and each method) that work correctly. You will likely do very poorly if you submit an attempt at every class, where no classes work according to the description. Some classes require significantly more time to implement than others. The marks allocated to each class are not necessarily an indication of their difficulty or the time required to complete them. You are allowed (and encouraged) to write additional helper methods for any class to help break up long methods, but these helper methods MUST be private (i.e. they must be named with a leading underscore).Task 1 Card()``Card
is an abstract class from which all instantiated types of card inherit. This class provides default card behavior, which can be inherited or overridden by specific types of cards. All cards have a name, description, cost and effect. A card’s effect is a dictionary mapping strings to integers, where the string represents the type of effect and the integer represents the strength of the effect. Cards can be considered permanent, but are not by default. Each type of card is represented by a symbol, typically a single character.Abstract cards have no effects, have 1 cost, and are represented by the symbol C
.Card
must implement the following methods:* __init__(self, **kwargs)
Instantiate a new card. The **kwargs
is required for multiple inheritance in a later task, you do not need to understand it yet and can ignore it for now.
__str__(self) -> str
Returns the name and description of this card__repr__(self) -> str
Returns a string which could be copied and pasted into a REPL to construct a new instance identical to self.get_symbol(self) -> str
Returns the symbol representing this card.get_name(self) -> str
Returns the name of this card.get_cost(self) -> int
Returns the cost of this card.get_effect(self) -> dict[str, int]
Returns this card’s effect.is_permanent(self) -> bool
Returns if this card is permanent or not.
Example:
>>> card = Card()
>>> card
Card()
>>> str(card)
'Card: A card.'
>>> card.get\_symbol()
'C'
>>> card.get\_name()
'Card'
>>> card.get\_cost()
1
>>> card.get\_effect()
{}
>>> card.is\_permanent()
False
Task 2 Shield(Card)``Shield
is a card that applies 5 shield to a target entity. Shield cards cost 1, and are represented by the symbol S
Example:
>>> card = Shield()
>>> card
Shield()
>>> str(card)
'Shield: Cast a protective shield that can absorb 5 damage.'
>>> card.get\_symbol()
'S'
>>> card.get\_name()
'Shield'
>>> card.get\_cost()
1
>>> card.get\_effect()
{'shield': 5}
>>> card.is\_permanent()
False
Task 3 Heal(Card)``Heal
is a card that applies 2 heal to a target entity. Heal cards cost 2, and are represented by the symbol H
Example:
>>> card = Heal()
>>> card
Heal()
>>> str(card)
'Heal: Cast an aura on target. It recovers 2 health.'
>>> card.get\_symbol()
'H'
>>> card.get\_name()
'Heal'
>>> card.get\_cost()
2
>>> card.get\_effect()
{'health': 2}
>>> card.is\_permanent()
False
Task 4 Fireball(Card)``Fireball
is a card that applies 3 damage to a target entity. Fireball cards apply 1 point of additional damage for each turn they have spent in a hero’s hand. Fireball cards cost 3. These cards are not represented by a single letter, but instead their symbol is the integer number of turns they have spent in hand.To support this extended functionality, The __init__
method of Fireball
takes in an additional argument. In addition to the Card
methods that must be supported, Fireball
must implement the following methods:* __init__(self, turns_in_hand: int)
Initialise a new Fireball
instance, with the given number of turns spent in hand.
increment_turn(self)
Register another turn spent in a hero’s hand.
Example:
>>> card = Fireball(7)
>>> card
Fireball(7)
>>> str(card)
'Fireball: FIREBALL! Deals 3 + [turns in hand] damage. Currently dealing 10 damage.'
>>> card.get\_symbol()
'7'
>>> card.get\_name()
'Fireball'
>>> card.get\_cost()
3
>>> card.get\_effect()
{'damage': 10}
>>> card.is\_permanent()
False
>>> card.increment\_turn()
>>> card
Fireball(8)
>>> card.get\_symbol()
'8'
>>> card.get\_effect()
{'damage': 11}
Task 5 CardDeck()``CardDeck
represents an ordered deck of cards. Cards are drawn from the top of a deck, and added to the bottom.CardDeck
must implement the following methods:* __init__(self, cards: list[Card])
Initialise a deck of cards containing the given cards. Cards are provided in order, with the first card in the list being the topmost card of the deck, and the last card in the list being the bottom most card of the deck.
__str__(self) -> str
Returns a comma separated list of the symbols representing each card in the deck. Symbols should appear in order, from top to bottom.__repr__(self) -> str
Returns a string which could be copied and pasted into a REPL to construct a new instance identical to self.is_empty(self) -> bool
Returns if thisCardDeck
is empty or not.remaining_count(self) -> int
Returns how many cards are currently in thisCardDeck
.draw_cards(self, num: int) -> list[Card]
Draws the specified number of cards from the top of the deck. Cards should be returned in the order they are drawn. If there are not enough cards remaining in the deck, as many cards as possible should be drawn.add_card(self, card: Card)
Adds the given card to the bottom of the deck.
Example:
>>> cards = [Card(), Card(), Shield(), Heal(), Fireball(6)]
>>> deck = CardDeck(cards)
>>> deck
CardDeck([Card(), Card(), Shield(), Heal(), Fireball(6)])
>>> str(deck)
'C,C,S,H,6'
>>> deck.remaining\_count()
5
>>> deck.is\_empty()
False
>>> deck.draw\_cards(3)
[Card(), Card(), Shield()]
>>> deck.remaining\_count()
2
>>> str(deck)
'H,6'
>>> deck.add\_card(Fireball(5))
>>> deck.remaining\_count()
3
>>> str(deck)
'H,6,5'
>>> deck.draw\_cards(1001)
[Heal(), Fireball(6), Fireball(5)]
>>> str(deck)
''
>>> deck.is\_empty()
True
Task 6 Entity()``Entity
is an abstract class from which all instantiated types of entity inherit. This class provides default entity behavior, which can be inherited or overridden by specific types of entities. Each entity has a health and shield value, and are alive if and only if their health value is above 0.Entity
must implement the following methods:* __init__(self, health: int, shield: int)
Initialise a new entity with the given health and shield value.
__repr__(self) -> str
Returns a string which could be copied and pasted into a REPL to construct a new instance identical to self.__str__(self) -> str
Returns this hero’s health and shield, comma separated.get_health(self) -> int
Returns this entity’s health.get_shield(self) -> int
Returns this entity’s shield.apply_shield(self, shield: int)
Applies the given amount of shield as per Table 1.apply_health(self, health: int)
Applies the given amount of health as per Table 1.apply_damage(self, damage: int)
Applies the given amount of damage as per Table 1. This function should not allow the entity’s health to drop below 0. Any damage exceeding the amount required to reduce the entity’s health to 0 should be discarded.is_alive(self) -> bool
Returns if this entity is alive or not.
Example:
>>> entity = Entity(5,3)
>>> entity
Entity(5, 3)
>>> str(entity)
'5,3'
>>> entity.get\_health()
5
>>> entity.get\_shield()
3
>>> entity.apply\_shield(1)
>>> entity
Entity(5, 4)
>>> entity.apply\_health(10)
>>> entity
Entity(15, 4)
>>> entity.apply\_damage(10)
>>> entity
Entity(9, 0)
>>> entity.is\_alive()
True
>>> entity.apply\_damage(9999999999)
>>> entity.get\_health()
0
>>> entity.is\_alive()
False
Task 7 Hero(Entity)
A Hero
is an entity with the agency to take actions in the game, possessing an energy level (and corresponding energy capacity), a deck of cards, and a hand of cards. When a hero is instantiated, its energy level is always at maximum capacity. A hero’s maximum hand size is 5. Unlike a base entity, a hero is only alive when both its health and the number of cards in its deck are greater than 0.In addition to the Entity
methods that must be supported, Hero
must implement the following methods:* __init__(self, health: int, shield: int, max_energy: int, deck: CardDeck, hand: list[Card])
Instantiate a new Hero
with the given health, shield, energy_capacity, deck, and hand.
__str__(self) -> str
Returns a string containing the following: This hero’s health, shield, and energy capacity, comma separated; followed by a semi-colon; followed by the string representation of this hero’s deck; followed by another semicolon; followed finally by the symbols of each card in this hero’s hand in order, comma separated.get_energy(self) -> int
Returns this hero’s current energy level.spend_energy(self, energy: int) -> bool
Attempts to spend the specified amount of this hero’s energy. If this hero does not have sufficient energy, then nothing happens. Returns whether the energy was spent or not.get_max_energy(self) -> int
Returns this hero’s energy capacity.get_deck(self) -> CardDeck
Returns this hero’s deck.get_hand(self) -> list[Card]
Returns this hero’s hand, in order.new_turn(self)
Registers a new turn of all fireball cards in this hero’s hand, draws from their deck into their hand, expands their energy capacity by 1, and refills their energy level, as per the Gameplay Section
Example:
>>> cards = [Card(), Card(), Shield(), Heal(), Fireball(6)]
>>> deck = CardDeck(cards)
>>> hand = [Heal(),Heal(),Fireball(2)]
>>> hero = Hero(4,5,3,deck,hand)
>>> hero
Hero(4, 5, 3, CardDeck([Card(), Card(), Shield(), Heal(), Fireball(6)]), [Heal(), Heal(), Fireball(2)])
>>> str(hero)
'4,5,3;C,C,S,H,6;H,H,2'
>>> hero.get\_energy()
3
>>> hero.get\_max\_energy()
3
>>> hero.spend\_energy(123456789)
False
>>> hero.get\_energy()
3
>>> hero.spend\_energy(2)
True
>>> hero.get\_energy()
1
>>> hero.get\_max\_energy()
3
>>> hero.get\_deck()
CardDeck([Card(), Card(), Shield(), Heal(), Fireball(6)])
>>> hero.get\_hand()
[Heal(), Heal(), Fireball(2)]
>>> hero.new\_turn()
>>> hero.get\_energy()
4
>>> hero.get\_max\_energy()
4
>>> hero.get\_deck()
CardDeck([Shield(), Heal(), Fireball(6)])
>>> hero.get\_hand()
[Heal(), Heal(), Fireball(3), Card(), Card()]
Task 8 Minion(Card, Entity)``Minion
is an abstract class from which all instantiated types of minion inherit. This class provides default minion behavior, which can be inherited or overridden by specific types of minions. Minions are a special type of Card
that also inherits from Entity
. Its __init__
method takes in the arguments of the Entity
class. You may need to modify your __init__
method in the Card
class to support multiple inheritance. The string representation of a Minion
should be that of the Card
class.All minions are permanent cards. Generic Minions have cost 2, no effect, and are represented by the symbol M
.A minion has the capacity to select its own target entity out of a given set. Generic minions ignore all given entities, and returns itself.In addition to the Entity
and Card
methods that must be supported, Minion
must implement the following methods:* __init__(self, health, shield)
Instantiate a new Minion
with the specified health and shield value.
__str__(self) -> str
Returns the name and description of this cardchoose_target(self, ally_hero: Entity, enemy_hero: Entity, ally_minions: list[Entity], enemy_minions: list[Entity]) -> Entity
Select this minion’s target out of the given entities. Note that here, the allied hero and minions will be those friendly to this minion, not necessarily to the player. This logic extends to the specified enemy hero and minions. minions should be provided in the order they appear in their respective minion slots, from left to right.
Example:
>>> minion = Minion(1,0)
>>> minion
Minion(1, 0)
>>> str(minion)
'Minion: Summon a minion.'
>>> minion.get\_symbol()
'M'
>>> minion.get\_name()
'Minion'
>>> minion.get\_cost()
2
>>> minion.get\_effect()
{}
>>> minion.is\_permanent()
True
>>> minion.get\_health()
1
>>> minion.get\_shield()
0
>>> minion.apply\_shield(1)
>>> minion
Minion(1, 1)
>>> minion.apply\_health(10)
>>> minion
Minion(11, 1)
>>> minion.apply\_damage(10)
>>> minion
Minion(2, 0)
>>> minion.is\_alive()
True
>>> hero\_friend = Hero(1,2,3,CardDeck([Fireball(8)]), [])
>>> hero\_foe = Hero(3,2,1,CardDeck([Card()]), [Shield()])
>>> minions\_friend = [Minion(1,0),Minion(2,0),Minion(1,2)]
>>> minions\_foe = [Minion(1,1),Minion(2,2)]
minion.choose\_target(hero\_friend, hero\_foe, minions\_friend, minions\_foe)
Minion(2, 0)
Task 9 Wyrm(Minion)
A Wyrm
is a minion that has 2 cost, is represented by the symbol W
, and whose effect is to apply 1 heal and 1 shield.When selecting a target entity, a Wyrm
will choose the allied entity with the lowest health.If multiple entities have the lowest health, if one of the tied entities is the allied hero, the allied hero should be selected. Otherwise, the leftmost tied minion should be selected.Example:
>>> minion = Wyrm(2,2)
>>> minion
Wyrm(2, 2)
>>> str(minion)
'Wyrm: Summon a Mana Wyrm to buff your minions.'
>>> minion.get\_symbol()
'W'
>>> minion.get\_name()
'Wyrm'
>>> minion.get\_cost()
2
>>> minion.get\_effect()
{'health': 1, 'shield': 1}
>>> minion.is\_permanent()
True
>>> hero\_friend = Hero(1,2,3,CardDeck([Fireball(8)]), [])
>>> hero\_foe = Hero(3,2,1,CardDeck([Card()]), [Shield()])
>>> minions\_friend = [Minion(1,0),Minion(2,0),Minion(1,2)]
>>> minions\_foe = [Minion(1,1),Minion(2,2)]
minion.choose\_target(hero\_friend, hero\_foe, minions\_friend, minions\_foe)
Hero(1, 2, 3, CardDeck([Fireball(8)]), [])
Task 10 Raptor(Minion)
A Raptor
is a minion that has 2 cost, is represented by the symbol R
, and whose effect is to apply damage equal to its health.When selecting a target entity, a Raptor
will choose the enemy minion with the highest health. If there is no such minion, it will select the enemy hero.If multiple minions have the highest health, the leftmost tied minion should be selected.Example:
>>> minion = Raptor(2,2)
>>> minion
Raptor(2, 2)
>>> str(minion)
'Raptor: Summon a Bloodfen Raptor to fight for you.'
>>> minion.get\_symbol()
'R'
>>> minion.get\_name()
'Raptor'
>>> minion.get\_cost()
2
>>> minion.get\_effect()
{'damage': 2}
>>> minion.apply\_health(2)
>>> minion.get\_effect()
{'damage': 4}
>>> minion.is\_permanent()
True
>>> hero\_friend = Hero(1,2,3,CardDeck([Fireball(8)]), [])
>>> hero\_foe = Hero(3,2,1,CardDeck([Card()]), [Shield()])
>>> minions\_friend = [Minion(1,0),Minion(2,0),Minion(1,2)]
>>> minions\_foe = [Minion(1,1),Minion(2,2)]
minion.choose\_target(hero\_friend, hero\_foe, minions\_friend, minions\_foe)
Minion(2, 2)
minion.choose\_target(hero\_friend, hero\_foe, minions\_friend, [])
Hero(3, 2, 1, CardDeck([Card()]), [Shield()])
Task 11 HearthModel()``HearthModel
models the logical state of a game of Hearthstone.Here, active minions are those minions currently existing within a minion slot. Both the player and their opponent have a maximum of 5 minion slots. Minion slots are filled out from left to right. If a minion is to be placed and all respective minion slots are full, the minion in the leftmost minion slot is removed from the game, and all minions in remaining slots are moved one slot left before the new minion is placed.Within this model, the enemy hero follows the following logic: When the enemy hero takes a turn, it attempts to play each card in its hand in order. Whenever it successfully plays a card, it begins trying cards from the beginning of its hand again. If the enemy plays a card that includes a damage effect, it always targets the player’s hero. Otherwise it targets itself.HearthModel
must implement the following methods:* __init__(self, player: Hero, active_player_minions: list[Minion], enemy: Hero, active_enemy_minions: list[Minion])
Instantiates a new HearthModel
using the given player, enemy, and active minions. Each respective list of minions is given in the order they appear in their corresponding minion slots, from left to right.
__str__(self) -> str
Return the following in order, separated by the pipe character (|
): The string representation of the player’s hero; a semicolon separated list of the players active minions (symbol, health, and shield, comma separated); the string representation of the enemy hero; and a semicolon separated list of the active enemy minions (symbol, health, and shield, comma separated).__repr__(self) -> str
Returns a string which could be copied and pasted into a REPL to construct a new instance identical to self.get_player(self) -> Hero
Return this model’s player hero instance.get_enemy(self) -> Hero
Return this model’s enemy hero instance.get_player_minions(self) -> list[Minion]
Return the player’s active minions. Minions should appear in order from leftmost minion slot to rightmost minion slot.get_enemy_minions(self) -> list[Minion]
Return the enemy’s active minions. Minions should appear in order from leftmost minion slot to rightmost minion slot.has_won(self) -> bool
Return true if and only if the player has won the game as per the Gameplay section.has_lost(self) -> bool
Return true if and only if the player has lost the game as per the Gameplay sectionplay_card(self, card: Card, target: Entity) -> bool
Attempts to play the specified card on the player’s behalf. Table 2 Specifies the actions that must occur when an attempt is made to play a card. Returns whether the card was successfully played or not. Thetarget
argument will be ignored if the specified card is permanent. If a minion is defeated, it should be removed from the game, and any remaining minions within the respective minion slots should be moved one slot left if able.discard_card(self, card: Card)
Discards the given card from the players hand. The discarded card should be added to the bottom of the player’s deck as per Table 2.end_turn(self) -> list[str]
Follows the instructions for theend turn
command in Table 2, excluding the last instruction (saving the game toautosave.txt
). Returns the names of the cards played by the enemy hero (in order). If a minion is defeated at any point, it should be removed from the game, and any remaining minions within the respective minion slots should be moved one slot left if able. If the enemy hero is not alive after it has drawn cards, it should not take a turn, and the player should not subsequently update its own status.
Example:
>>> deck1 = CardDeck([Shield(),Heal(),Fireball(3),Heal(),Raptor(1,0),Wyrm(1,0),Shield(),Heal(),Heal(),Raptor(1,0)])
>>> hand1 = [Raptor(2,2), Heal(), Shield(),Fireball(8)]
>>> player = Hero(5,0,2,deck1,hand1)
>>> deck2 = CardDeck([Heal(),Shield(),Heal(),Heal(),Raptor(1,2),Wyrm(1,3),Shield(),Heal(),Heal(),Raptor(2,2)])
>>> hand2 = [Wyrm(1,0),Fireball(0),Raptor(1,0),Shield()]
>>> enemy = Hero(10,0,3,deck2,hand2)
>>> player\_minions = [Raptor(1,0),Wyrm(1,1)]
>>> enemy\_minions = [Wyrm(1,2)]
>>> model = HearthModel(player,player\_minions,enemy,enemy\_minions)
>>> model
HearthModel(Hero(5, 0, 2, CardDeck([Shield(), Heal(), Fireball(3), Heal(), Raptor(1, 0), Wyrm(1, 0), Shield(), Heal(), Heal(), Raptor(1, 0)]), [Raptor(2, 2), Heal(), Shield(), Fireball(8)]), [Raptor(1, 0), Wyrm(1, 1)], Hero(10, 0, 3, CardDeck([Heal(), Shield(), Heal(), Heal(), Raptor(1, 2), Wyrm(1, 3), Shield(), Heal(), Heal(), Raptor(2, 2)]), [Wyrm(1, 0), Fireball(0), Raptor(1, 0), Shield()]), [Wyrm(1, 2)])
>>> str(model)
'5,0,2;S,H,3,H,R,W,S,H,H,R;R,H,S,8|R,1,0;W,1,1|10,0,3;H,S,H,H,R,W,S,H,H,R;W,0,R,S|W,1,2'
>>> model.get\_player()
Hero(5, 0, 2, CardDeck([Shield(), Heal(), Fireball(3), Heal(), Raptor(1, 0), Wyrm(1, 0), Shield(), Heal(), Heal(), Raptor(1, 0)]), [Raptor(2, 2), Heal(), Shield(), Fireball(8)])
>>> model.get\_enemy()
Hero(10, 0, 3, CardDeck([Heal(), Shield(), Heal(), Heal(), Raptor(1, 2), Wyrm(1, 3), Shield(), Heal(), Heal(), Raptor(2, 2)]), [Wyrm(1, 0), Fireball(0), Raptor(1, 0), Shield()])
>>> model.get\_player\_minions()
[Raptor(1, 0), Wyrm(1, 1)]
>>> model.get\_enemy\_minions()
[Wyrm(1, 2)]
>>> model.has\_won()
False
>>> model.has\_lost()
False
>>> card = model.get\_player().get\_hand()[3]
>>> card
Fireball(8)
>>> model.play\_card(card, enemy)
False
>>> model
HearthModel(Hero(5, 0, 2, CardDeck([Shield(), Heal(), Fireball(3), Heal(), Raptor(1, 0), Wyrm(1, 0), Shield(), Heal(), Heal(), Raptor(1, 0)]), [Raptor(2, 2), Heal(), Shield(), Fireball(8)]), [Raptor(1, 0), Wyrm(1, 1)], Hero(10, 0, 3, CardDeck([Heal(), Shield(), Heal(), Heal(), Raptor(1, 2), Wyrm(1, 3), Shield(), Heal(), Heal(), Raptor(2, 2)]), [Wyrm(1, 0), Fireball(0), Raptor(1, 0), Shield()]), [Wyrm(1, 2)])
>>> card = model.get\_player().get\_hand()[2]
>>> card
Shield()
>>> model.play\_card(card, player)
True
>>> model.get\_player().get\_energy() # Note that repr below depicts energy capacity (unchanged)
1
>>> model
HearthModel(Hero(5, 5, 2, CardDeck([Shield(), Heal(), Fireball(3), Heal(), Raptor(1, 0), Wyrm(1, 0), Shield(), Heal(), Heal(), Raptor(1, 0)]), [Raptor(2, 2), Heal(), Fireball(8)]), [Raptor(1, 0), Wyrm(1, 1)], Hero(10, 0, 3, CardDeck([Heal(), Shield(), Heal(), Heal(), Raptor(1, 2), Wyrm(1, 3), Shield(), Heal(), Heal(), Raptor(2, 2)]), [Wyrm(1, 0), Fireball(0), Raptor(1, 0), Shield()]), [Wyrm(1, 2)])
>>> card = model.get\_player().get\_hand()[0]
>>> card
Raptor(2, 2)
>>> model.discard\_card(card)
>>> model
HearthModel(Hero(5, 5, 2, CardDeck([Shield(), Heal(), Fireball(3), Heal(), Raptor(1, 0), Wyrm(1, 0), Shield(), Heal(), Heal(), Raptor(1, 0), Raptor(2, 2)]), [Heal(), Fireball(8)]), [Raptor(1, 0), Wyrm(1, 1)], Hero(10, 0, 3, CardDeck([Heal(), Shield(), Heal(), Heal(), Raptor(1, 2), Wyrm(1, 3), Shield(), Heal(), Heal(), Raptor(2, 2)]), [Wyrm(1, 0), Fireball(0), Raptor(1, 0), Shield()]), [Wyrm(1, 2)])
>>> model.end\_turn()
['Wyrm', 'Raptor']
>>> model
HearthModel(Hero(5, 5, 3, CardDeck([Heal(), Raptor(1, 0), Wyrm(1, 0), Shield(), Heal(), Heal(), Raptor(1, 0), Raptor(2, 2)]), [Heal(), Fireball(9), Shield(), Heal(), Fireball(3)]), [Raptor(2, 0), Wyrm(1, 1)], Hero(10, 0, 4, CardDeck([Shield(), Heal(), Heal(), Raptor(1, 2), Wyrm(1, 3), Shield(), Heal(), Heal(), Raptor(2, 2)]), [Fireball(1), Shield(), Heal()]), [Wyrm(2, 2), Wyrm(2, 1), Raptor(1, 0)])
>>> model.end\_turn()
['Fireball', 'Shield', 'Shield']
>>> model
HearthModel(Hero(5, 0, 4, CardDeck([Heal(), Raptor(1, 0), Wyrm(1, 0), Shield(), Heal(), Heal(), Raptor(1, 0), Raptor(2, 2)]), [Heal(), Fireball(10), Shield(), Heal(), Fireball(4)]), [Wyrm(2, 2)], Hero(10, 10, 5, CardDeck([Heal(), Raptor(1, 2), Wyrm(1, 3), Shield(), Heal(), Heal(), Raptor(2, 2)]), [Heal(), Heal()]), [Wyrm(3, 1), Wyrm(2, 1), Raptor(2, 1)])
>>> model.end\_turn()
['Heal', 'Heal', 'Heal']
>>> model
HearthModel(Hero(5, 0, 5, CardDeck([Heal(), Raptor(1, 0), Wyrm(1, 0), Shield(), Heal(), Heal(), Raptor(1, 0), Raptor(2, 2)]), [Heal(), Fireball(11), Shield(), Heal(), Fireball(5)]), [Wyrm(3, 0)], Hero(16, 10, 6, CardDeck([Shield(), Heal(), Heal(), Raptor(2, 2)]), [Raptor(1, 2), Wyrm(1, 3)]), [Wyrm(3, 1), Wyrm(3, 2), Raptor(3, 2)])
>>> model.end\_turn()
['Raptor', 'Wyrm', 'Shield', 'Heal']
>>> model
HearthModel(Hero(5, 0, 6, CardDeck([Heal(), Raptor(1, 0), Wyrm(1, 0), Shield(), Heal(), Heal(), Raptor(1, 0), Raptor(2, 2)]), [Heal(), Fireball(12), Shield(), Heal(), Fireball(6)]), [], Hero(18, 15, 7, CardDeck([Raptor(2, 2)]), [Heal()]), [Wyrm(3, 1), Wyrm(3, 2), Raptor(3, 2), Raptor(3, 4), Wyrm(2, 4)])
>>> model.end\_turn() # Enemy draws last of their cards
[]
>>> model.has\_won()
True
>>> model
HearthModel(Hero(5, 0, 6, CardDeck([Heal(), Raptor(1, 0), Wyrm(1, 0), Shield(), Heal(), Heal(), Raptor(1, 0), Raptor(2, 2)]), [Heal(), Fireball(12), Shield(), Heal(), Fireball(6)]), [], Hero(18, 15, 8, CardDeck([]), [Heal(), Raptor(2, 2)]), [Wyrm(3, 1), Wyrm(3, 2), Raptor(3, 2), Raptor(3, 4), Wyrm(2, 4)])
ControllerThe controller is a single class, Hearthstone()
, which you must implement according to this section. You will also implement the play_game
function to construct the controller within this section. Unlike with the model, there is no recommended task completion order in this section. It will likely be helpful to work on certain tasks in parallel to implement features. Please refer to the gameplay/
folder provided with this assignment for examples of intended controller behavior. Note that for all examples, the file initially loaded is levels/deck1.txt
. Note that examples may appear to have alignment issues if your text editor does not enforce uniform character widths, this has no functional impact on your assignment (you are not implementing the view class).Task 12 Hearthstone()``Hearthstone()
is the controller class for the overall game. The controller is responsible for creating and maintaining instances of the model and view classes, handling player input, and facilitating communication between the model and view classes.When loading a game state from a file, it is expected that the first line of the file contains the string representation of a HearthModel
. Any content after the first line of a file should be ignored when loading a game state from it.NoteOne of the most important skills in programming is the ability to understand and use someone elses code. You will be expected to understand and correctly use the HearthView
class provided in display.py
.Hearthstone()
should implement the following methods:* __init__(self, file: str)
Instantiates the controller. Creates view and model instances. The model should be instantiated with the game state specified by the data within the file with the given name. Minions that are not currently in a minion slot should be instantiated with 1 health and 0 shield. You should not handle the case where the file with the specified name does not exist, nor should you handle the case where the file does not contain a valid game state. That is to say, you should not check for an invalid file/game state, and you should not handle any errors that may occur because of one. You should make use of the provided HearthView
class.
__str__(self) -> str
Returns a human readable string stating that this is a game of Hearthstone using the current file path.__repr__(self) -> str
Returns a string which could be copied and pasted into a REPL to construct a new instance identical to self. PLEASE READ: We have been made aware that Gradescope is expecting therepr
not to contain quotations around the argument (i.e.Hearthstone(file.txt)
instead ofHearthstone("file.txt")
). While this is not correct, due to the number of submissions made to Gradescope at this point, and the fact it is a minor difference that does not impact the rest of the assignment, we have elected not to change what Gradescope is expecting. Please write yourrepr
method to satisfy Gradescope.update_display(self, messages: list[str])
Update the display by printing out the current game state. The display should contain (from top to bottom): A banner containing the game name; A depiction of the enemy hero, with (from left to right) their health (HP), shield, number of remaining cards, and energy level; The enemy’s minion slots (with minion depiction including health (HP) and shield); The player’s minion slots (with minion depiction including health (HP) and shield); The player’s current hand; A depiction of the player hero, with (from left to right) their health (HP), shield, number of remaining cards, and energy level; A list of messages to the player. Minions and heros are labelled with the character/number that should be entered to target them. Messages are arranges such that earlier messages in the provided list appear above later ones. You should make use of the providedHearthView
class to accomplish this.get_command(self) -> str
Repeatedly prompts the user until they enter a valid command. Returns the first valid command entered by the user. The possible valid commands are given in Table 2. Whenever the user enters an invalid command, the display should be updated with theINVALID_COMMAND
message fromsupport.py
before the user is prompted again. The player’s command will be case insensitive, but the returned command should be lower case. Note also that card positions will be entered one-indexed (that is, starting at 1, not 0).get_target_entity(self) -> str
Repeatedly prompts the user until they enter a valid entity identifier. A valid entity identifier is one of the following:PLAYER_SELECT
orENEMY_SELECT
fromsupport.py
, to select the player or enemy hero respectively; an integer between 1 and 5 inclusive, to select the minion in the enemy’s minion slot at the respective position; or an integer between 6 and 10 inclusive, to select the minion in the player’s minion slot at the position given by the subtracting 5 from the integer. If a minion does not currently exist at a specified position, then the identifier is treated as invalid. If a hero is selected, the identifier should be returned directly. If an enemy’s minion is selected, the (zero-indexed) index of the minion in the enemy’s minion slots should be returned prepended byENEMY_SELECT
fromsupport.py
. If an player’s minion is selected, the (zero-indexed) index of the minion in the player’s minion slots should be returned prepended byPLAYER_SELECT
fromsupport.py
. Input should be case-insensitive, but the returned identifier should be upper-case. Whenever the user enters an invalid identifier, the display should be updated with theINVALID_ENTITY
message fromsupport.py
before the user is prompted again.save_game(self)
Writes the string representation of this controllersHearthModel
instance toautosave.txt
. Ifautosave.txt
does not exist, it should be created. Ifautosave.txt
already has content in it, it should be overwritten.load_game(self, file: str)
Replaces the current model instance with a new one loaded from the data within the file with the given name. Minions that are not currently in a minion slot should be instantiated with 1 health and 0 shield. You should not handle the case where the file with the specified name does not exist, nor should you handle the case where the file does not contain a valid game state. That is to say, you should not check for an invalid file/game state, and you should not handle any errors that may occur because of one.play(self)
Conducts a game of Hearthstone from start to finish, following the Gameplay section
Task 13 play_game(file: str)
The play game function should be fairly short and do exactly two things:1. Construct a controller instance with the given game file. You cannot assume no errors will occur when loading the file. If a ValueError
or FileNotFound
error is raised while creating the controller, you should attempt to recreate the controller using autosave.txt
. You should not handle any errors raised when constructing this backup controller.
2. Play a single game of Hearthstone from beginning to end (using .play()
)
Task 14 main()
The purpose of the main function is to allow you to test your own code. Like the play game function, the main function should be short (A single line) and do exactly one thing: call play_game
with a file name of your choice.Assessment and Marking CriteriaThis assignment assesses following course learning objectives:1. Apply program constructs such as variables, selection, iteration and sub-routines,
2. Apply basic object-oriented concepts such as classes, instances and methods,
3. Read and analyse code written by others,
4. Analyse a problem and design an algorithmic solution to the problem,
5. Read and analyse a design and be able to translate the design into a working program, and
6. Apply techniques for testing and debugging.
There are a total of 100 marks for this assessment item.FunctionalityYour program’s functionality will be marked out of a total of 50 marks.The breakdown of marks for each implementation section is as follows:* Model: 35 Marks
- Controller: 15 Marks
Your assignment will be put through a series of tests and your functionality mark will be proportional to the number of tests you pass. You will be given a subset of the functionality tests before the due date for the assignment.You may receive partial marks within each section for partially working functions, or for implementing only a few functions.WarningYou need to perform your own testing of your program to make sure that it meets all specifications given in the assignment. Only relying on the provided tests is likely to result in your program failing in some cases and you losing some functionality marks.NoteFunctionality tests are automated, so string outputs need to match exactly what is expected.Your program must run in Gradescope, which uses Python 3.12. Partial solutions will be marked but if there are errors in your code that cause the interpreter to fail to execute your program, you will get zero for functionality marks. If there is a part of your code that causes the interpreter to fail, comment out the code so that the remainder can run. Your program must run using the Python 3.12 interpreter. If it runs in another environment (e.g. Python 3.8 or PyCharm) but not in the Python 3.12 interpreter, you will get zero for the functionality mark.Code StyleThe style of your assignment will be assessed by a tutor. Style will be marked according to the style rubric provided with the assignment. The style mark will be out of 50.NoteStyle accounts for half the marks available on this assignmentYou are expected to follow the PEP-8 style guidelines discussed in lectures. The key consideration in marking your code style is whether the code is easy to understand. There are several aspects of code style that contribute to how easy it is to understand code. In this assignment, your code style will be assessed against the following criteria:* Readability
+ Program Structure: Layout of code makes it easy to read and follow its logic. This includes using whitespace to highlight blocks of logic, and ensuring all lines are below 80 characters.
+ Descriptive Identifier Names: Variable, constant, and function names clearly describe what they represent in the program’s logic. Do not use Hungarian Notation for identifiers. In short, this means do not include the identifier’s type in its name, rather make the name meaningful (e.g. employee identifier).
+ Named Constants: Any non-trivial fixed value (literal constant) in the code is represented by a descriptive named constant (identifier).
- Algorithmic Logic
- Single Instance of Logic: Blocks of code should not be duplicated in your program. Any code that needs to be used multiple times should be implemented as a function.
- Variable Scope: Variables should be declared locally in the function in which they are needed. Global variables should not be used.
- Control Structures: Logic is structured simply and clearly through good use of control structures (e.g. loops and conditional statements).
- Object-Oriented Program Structure
- Classes & Instances: Objects are used as entities to which messages are sent, demonstrating understanding of the differences between classes and instances.
- Encapsulation: Classes are designed as independent modules with state and behaviour. Methods only directly access the state of the object on which they were invoked. Methods never update the state of another object.
- Abstraction: Public interfaces of classes are simple and reusable. Enabling modular and reusable components which abstract GUI details.
- Inheritance & Polymorphism: Subclasses are designed as specialised versions of their superclasses. Subclasses extend the behaviour of their superclass without re-implementing behaviour, or breaking the superclass behaviour or design. Subclasses redefine behaviour of appropriate methods to extend the superclasses’ type. Subclasses do not break their superclass’ interface.
- Model View Controller: Your program adheres to the Model-View-Controller design pattern. The GUI’s view and control logic is clearly separated from the model. Model information stored in the controller and passed to the view when required.
- Documentation:
- Comment Clarity: Comments provide meaningful descriptions of the code. They should not repeat what is already obvious by reading the code (e.g.
# Setting variable to 0
). Comments should not be verbose or excessive, as this can make it difficult to follow the code. - Informative Docstrings: Every function should have a docstring that summarises its purpose. This includes describing parameters and return values (including type information) so that others can understand how to use the function correctly.
- Description of Logic: All significant blocks of code should have a comment to explain how the logic works. For a small function, this would usually be the docstring. For long or complex functions, there may be different blocks of code in the function. Each of these should have an in-line comment describing the logic.
- Comment Clarity: Comments provide meaningful descriptions of the code. They should not repeat what is already obvious by reading the code (e.g.
Assignment SubmissionYou must submit your assignment electronically via Gradescope. You must use your UQ email address which is based on your student number (e.g. [s4123456@student.uq.edu.au](mailto:s4123456@student.uq.edu.au)
) as your Gradescope submission account.When you login to Gradescope you may be presented with a list of courses. Select CSSE1001
. You will see a list of assignments. Choose Assignment 2
. You will be prompted to choose a file to upload. Do not select the option to submit using a git repository if it is presented to you. The prompt may say that you can upload any files, including zip files. You must submit your assignment as a single Python file called a2.py
(use this name – all lower case), and nothing else. Your submission will be automatically run to determine the functionality mark. If you submit a file with a different name, the tests will fail and you will get zero for functionality. Do not submit any sort of archive file (e.g. zip, rar, 7z, etc.).Upload an initial version of your assignment at least one week before the due date. Do this even if it is just the initial code provided with the assignment. If you are unable access Gradescope, make a post on Edstem immediately. Excuses, such as you were not able to login or were unable to upload a file will not be accepted as reasons for granting an extension.When you upload your assignment it will run a subset of the functionality autograder tests on your submission. It will show you the results of these tests. It is your responsibility to ensure that your uploaded assignment file runs and that it passes the tests you expect it to pass.Late submission of the assignment will result in a deduction of 100% of the total possible mark. A one-hour grace period will be applied to the due time, after which time (16:00) your submission will be considered officially late and will receive a mark of 0. Do not wait until the last minute to submit your assignment, as the time to upload it may make it late. Multiple submissions are allowed and encouraged, so ensure that you have submitted an almost complete version of the assignment well before the submission deadline of 15:00. Your latest submission will be marked. Do not submit after the deadline, as this will result in a late penalty of 100% of the maximum possible mark being applied to your submission.In the event of exceptional personal or medical circumstances that prevent you from handing in the assignment on time, you may submit a request for an extension. See the course profile for details of how to apply for an extension.Requests for extensions must be made before the submission deadline. The application and supporting documentation (e.g. medical certificate) must be submitted via myUQ. You must retain the original documentation for a minimum period of six months to provide as verification, should you be requested to do so.PlagiarismThis assignment must be your own individual work. By submitting the assignment, you are claiming it is entirely your own work. You may discuss general ideas about the solution approach with other students. Describing details of how you implement a function or sharing part of your code with another student is considered to be collusion and will be counted as plagiarism. You must not copy fragments of code that you find on the Internet to use in your assignment. You must not use any artificial intelligence programs to assist you in writing your assignment.Please read the section in the course profile about plagiarism. You are encouraged to complete both parts A and B of the academic integrity modules before starting this assignment. Submitted assignments will be electronically checked for potential cases of plagiarism.On Wed, 28 May 2025 at 20:35, Lisa Zeng <zenglisa540@gmail.com> wrote:
代码
# DO NOT modify or add any import statements
from support import *
from display import HearthView
# Name: Lingchen Zeng
# Student Number: 48745910
# Favorite Building:
# -----------------------------------------------------------------------------
# Define your classes and functions here
#Task 1
class Card():
"""
Abstract class representing card. This class provides default card
behaviour, which can be inherited or overridden by specific types of cards.
"""
def __init__(self, **kwargs):
self._name = CARD_NAME #using variables from support document
self._description = CARD_DESC
self._symbol = CARD_SYMBOL
self._cost = 1
self._effect = {}
self._permanent = False
def __str__(self) -> str: #dont write docstring because they inherit from object
return f"{self._name}: {self._description}"
def __repr__(self) -> str:
return "Card()"
def get_symbol(self) -> str:
"""
Returns the symbol representing this card
Parameters:
param 1: description of param 1
param 2: description
position: a tuple of two integers that represents the
position on board
Returns:
str: the symbol associated with this card
"""
return self._symbol
def get_name(self) -> str:
"""
Returns the name of this card.
Returns:
str: the name of this card.
"""
return self._name
def get_cost(self) -> int:
"""
Returns the energy cost required to play this card.
Returns:
int: the cost of this card.
"""
return self._cost
def get_effect(self) -> dict:
"""
Returns the effect information of this card.
Returns:
dict: a dictionary describing the effects of this card.
"""
return self._effect
def is_permanent(self) -> bool:
"""
Indicates whether this card is permanent or not.
Returns:
bool: True if it is permanent, false otherwise.
"""
return self._permanent
#Task 2
class Shield(Card):
"""
Shield is a card that applies 5 shield to a target entity.
Inherits from Card.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._name = SHIELD_NAME
self._description = SHIELD_DESC
self._symbol = SHIELD_SYMBOL
self._cost = 1
self._effect = {'shield': 5}
self._permanent = False
def __str__(self) -> str:
return f"{self._name}: {self._description}"
def __repr__(self) -> str:
return "Shield()"
#Task 3
class Heal(Card):
"""
Heal is a card that applies 2 heal to a target entity.
Inherits from card.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._name = HEAL_NAME
self._description = HEAL_DESC
self._symbol = HEAL_SYMBOL
self._cost = 2
self._effect = {'health': 2}
self._permanent = False
def __str__(self) -> str:
return f"{self._name}: {self._description}"
def __repr__(self) -> str:
return "Heal()"
#Task 4
class Fireball(Card):
"""
Fireball is a card that applies 3 damage to a target entity.
Inherits from card.
"""
def __init__(self, turns_in_hand: int, **kwargs):
super().__init__(**kwargs)
self._name = FIREBALL_NAME
self._description = FIREBALL_DESC
self._base_damage = 3
self._cost = 3
self._permanent = False
self._turns_in_hand = turns_in_hand
def __str__(self) -> str:
total_damage = self._base_damage + self._turns_in_hand
return (f"{self._name}: {self._description} Currently dealing "
f"{total_damage} damage.")
def __repr__(self) -> str:
return f"Fireball({self._turns_in_hand})"
def increment_turn(self) -> None:
"""
Registers another turn spent in the hero's hand.
Returns:
None
"""
self._turns_in_hand += 1
def get_symbol(self) -> str:
"""
Returns a string representation of the numebr of turns the card has
been in the hero's hand.
Returns:
str: The number of turns.
"""
return str(self._turns_in_hand)
def get_effect(self) -> dict:
"""
Returns a dictionary with the current effect of fireball.
Returns:
dict: A dictionary with the total damage of fireball.
"""
return {'damage': self._base_damage + self._turns_in_hand}
#Task 5
class CardDeck():
"""
Represents an ordered deck of cards.
Cards are drawn from the top of a deck, and added to the bottom.
"""
def __init__(self, cards: list[Card]):
self._cards = list(cards)
def __repr__(self) -> str:
return f"CardDeck({self._cards})"
def __str__(self) -> str:
"""
Returns a string showing the symbols of all cards in the deck
seperated by commas.
Returns:
str: Comma-seperated list of card symbols.
"""
#string seperating symbol of each card using a comma
return ",".join(card.get_symbol() for card in self._cards)
def is_empty(self) -> bool:
"""
Checks if the deck is empty.
Returns:
bool: True if the deck has no cards, False otherwise.
"""
return len(self._cards) == 0
def remaining_count(self) -> int:
"""
Returns the number of remaining cards in the deck.
Returns:
int: Count of cards left in the deck.
"""
return len(self._cards)
def draw_cards(self, num: int) -> list[Card]:
"""
Draws a specific number of cards from the top of the deck.
Parameters:
num (int): Number of cards to draw.
Returns:
list[Card]: List of drawn carsd from the top of the deck.
"""
drawn = self._cards[:num] #get cards up to some number
self._cards = self._cards[num:] #remove the drawn cards from deck
return drawn
def add_card(self, card: Card):
"""
Adds a card to the bottom of the deck.
Paramaters:
card(Card): The card to add to the deck.
Returns:
None
"""
self._cards.append(card)
#Task 6
class Entity():
"""
Entity is an abstract class from which all instantiated
types of entity inherit.
"""
def __init__(self, health: int, shield: int):
self._health = health
self._shield = shield
def __repr__(self) -> str:
return f"Entity({self._health}, {self._shield})"
def __str__(self) -> str:
return f"{self._health},{self._shield}"
def get_health(self) -> int:
"""
Returns the current health value of the entity.
Returns:
int: The amount of health remaining.
"""
return self._health
def get_shield(self) -> int:
"""
Returns the current shield value of the entity.
Returns:
int: The amount of shield value remaining.
"""
return self._shield
def apply_shield(self, shield: int):
"""
Increases the entity's shield value by a specific amount.
Parameter:
shield(int): The amount of shield points to add.
Returns:
None
"""
self._shield += shield
def apply_health(self, health: int):
"""
Increases the entity's health by a specific amount.
Parameter:
health(int): The amount of health to add.
Return:
None
"""
self._health += health
def apply_damage(self, damage: int):
"""
Applies a specific amount of damage to entity.
Parameter:
damage(int): The amount of damage to apply.
Return:
None
"""
#damage absorbed by shield
if self._shield >= damage:
self._shield -= damage
else:
#if shield is not enough, remaining damage is applied to health
extra_damage = damage - self._shield
self._shield = 0
self._health -= extra_damage
if self._health < 0: #health cannot drop below 0
self._health = 0
def is_alive(self) -> bool:
"""
Checks if the entity is still alive.
Returns:
bool: True if still alive, False otherwise.
"""
return self._health > 0
#Task 7
class Hero(Entity):
"""
A Hero is an entity with the agency to take actions in the game,
possessing an energy level (and corresponding energy capacity),
a deck of cards, and a hand of cards.
"""
def __init__(self, health: int, shield: int, max_energy: int,
deck: CardDeck, hand: list[Card]):
super().__init__(health, shield)
self._max_energy = max_energy
self._energy = max_energy
self._deck = deck
self._hand = list(hand)
def __repr__(self) -> str:
return (f"Hero({self._health}, {self._shield}, {self._max_energy}, "
f"{repr(self._deck)}, {repr(self._hand)})")
def __str__(self) -> str:
stats = f"{self._health},{self._shield},{self._max_energy}"
deck_symbols = str(self._deck)
hands_symbols = ",".join(card.get_symbol() for card in self._hand)
return f"{stats};{deck_symbols};{hands_symbols}"
def get_energy(self) -> int:
"""
Returns the hero's current energy.
Returns:
int: The current available energy.
"""
return self._energy
def get_max_energy(self) -> int:
"""
Returns the hero's current maximum energy.
Returns:
int: the maximum energy capacity.
"""
return self._max_energy
def spend_energy(self, energy: int) -> bool:
"""
Attempts to spend the specified amount of this hero’s energy.
If this hero does not have sufficient energy, then nothing happens.
Returns whether the energy was spent or not.
Parameters:
energy(int): The amount of energy to spend.
Return:
bool: True if possible, false otherwise.
"""
if self._energy >= energy:
self._energy -= energy
return True
return False
def get_deck(self) -> "CardDeck":
"""
Returns the hero's deck.
Returns:
CardDeck: The CardDeck object.
"""
return self._deck
def get_hand(self) -> list["Card"]:
"""
Returns the current hero's hand, in order.
Returns:
List[Card]: The hand as a list of Card objects.
"""
return self._hand
def is_alive(self) -> bool:
"""
Checks if the hero is still alive.
Returns:
bool: True if hero is still alive, False otherwise.
"""
return self._health > 0 and self._deck.remaining_count() > 0
def new_turn(self):
"""
Registers a new turn of all fireball cards in this hero’s hand,
draws from their deck into their hand, expands their energy capacity
by 1, and refills their energy level.
Returns:
None
"""
#increment turns for Fireball cards
for card in self._hand:
if type(card) is Fireball:
card.increment_turn()
#Draw cards to fill hand with five cards if hand was not full
if len(self._hand) < MAX_HAND:
diff = MAX_HAND - len(self._hand)
#put drawn cards in hand
self._hand.extend(self._deck.draw_cards(diff))
#increase max energy each turn, and refill energy
self._max_energy += 1
self._energy = self._max_energy
#Task 8
class Minion(Card, Entity):
"""
Minion is an abstract class from which all instantiated
types of minion inherit. This class provides default minion behavior,
which can be inherited or overridden by specific types of minions.
Minions are a special type of Card that also inherits from Entity.
"""
def __init__(self, health: int, shield: int):
Card.__init__(self)
self._name = MINION_NAME
self._description = MINION_DESC
self._symbol = MINION_SYMBOL
self._cost = 2
self._permanent = True
Entity.__init__(self, health, shield)
def __str__(self) -> str:
return f"{self._name}: {self._description}"
def __repr__(self):
return f"Minion({self._health}, {self._shield})"
def get_symbol(self) -> str:
"""
Returns the symbol representing the minion.
Returns:
str: The symbol string for the minion.
"""
return self._symbol
def get_name(self) -> str:
"""
Returns the minion name.
Returns:
str: The minion's name.
"""
return self._name
def get_cost(self) -> int:
"""
Returns the energy cost to play the minion.
Returns:
int: The minion's cost
"""
return self._cost
def get_effect(self) -> dict:
"""
Returns the minion's effect.
Returns:
dict: Description of the minion's effect.
"""
return self._effect
def is_permanent(self) -> bool:
"""
Indicates whether the minion is permanent.
Returns:
bool: True for all minions.
"""
return self._permanent
def choose_target(self, ally_hero: Entity, enemy_hero: Entity,
ally_minions: list[Entity],
enemy_minions: list[Entity]) -> Entity:
"""
Select a target for the minion's effect.
Parameters:
ally_hero (Entity): The ally hero entity.
enemy_hero (Entity): The enemy hero entity.
ally_minion (list[Entity]): List of all allied minions.
enemy_minion (list[Entity]): List of all enemy minions.
Returns:
Entity: The chosen target entity.
"""
return self
#Task 9
class Wyrm(Minion):
"""
A Wyrm is a minion that has 2 cost, is represented by the symbol W,
and whose effect is to apply 1 heal and 1 shield.
"""
def __init__(self, health, shield):
Minion.__init__(self, health, shield) #how to know when to use super
self._name = WYRM_NAME
self._description = WYRM_DESC
self._symbol = WYRM_SYMBOL
self._cost = 2
self._effect = {"health": 1, "shield" : 1}
self._permanent = True
def __str__(self) -> str:
return f"{self._name}: {self._description}"
def __repr__(self) -> str:
return f"{self._name}({self._health}, {self._shield})"
def choose_target(self, ally_hero: Entity, enemy_hero: Entity,
ally_minions: list[Entity],
enemy_minions: list[Entity]) -> Entity:
"""
Chooses the allied entity with lowest health to target its effects.
Parameters:
ally_hero (Entity): The ally hero entity.
enemy_hero (Entity): The enemy hero entity.
ally_minion (list[Entity]): List of all allied minions.
enemy_minion (list[Entity]): List of all enemy minions.
Returns:
Entity: the allied entity with the lowest health.
"""
lowest_health = ally_hero.get_health()
lowest_health_target = ally_hero
for minion in ally_minions: #iterate through ally minions
#set lowest health target to lowest health minion
if int(minion.get_health()) < lowest_health:
lowest_health = minion.get_health()
lowest_health_target = minion
#set lowest health target to ally hero if it has a lower health
#than lowest health minion
if int(ally_hero.get_health()) < lowest_health:
lowest_health_target = ally_hero
return lowest_health_target
#Task 10
class Raptor(Minion):
"""
A Raptor is a minion that has 2 cost, is represented by the symbol R,
and whose effect is to apply damage equal to its health.
"""
def __init__(self, health, shield):
super().__init__(health, shield)
self._name = RAPTOR_NAME
self._description = RAPTOR_DESC
self._symbol = RAPTOR_SYMBOL
self._cost = 2
self._effect = {"damage": self._health}
self._permanent = True
def __str__(self) -> str:
return f"{self._name}: {self._description}"
def __repr__(self) -> str:
return f"{self._name}({self._health}, {self._shield})"
def choose_target(self, ally_hero: Entity, enemy_hero: Entity,
ally_minions: list[Entity],
enemy_minions: list[Entity]) -> Entity:
"""
Chooses the enemy minion with the highest health to target its effects.
If there are no enemy minions, targets the enemy hero.
Parameters:
ally_hero (Entity): The ally hero entity.
enemy_hero (Entity): The enemy hero entity.
ally_minion (list[Entity]): List of all allied minions.
enemy_minion (list[Entity]): List of all enemy minions.
Returns:
Entity: The chosen target enemy entity.
"""
#no minion with the highest health, so enemy hero is targeted
if len(enemy_minions) == 0:
return enemy_hero
#choose the first minion in the list as initial highest health
highest_health = enemy_minions[0].get_health()
highest_health_minion = enemy_minions[0]
#iterate through the list of minions to find the highest health minion
for minion in enemy_minions:
if int(minion.get_health()) > highest_health:
highest_health_minion = minion
highest_health = int(minion.get_health())
return highest_health_minion
#Task 11
class HearthModel():
"""
Models the logical state of a game of Hearthstone.
"""
def __init__(self, player: Hero, active_player_minions: list[Minion], enemy: Hero, active_enemy_minions: list[Minion]):
self._player = player
self._enemy = enemy # # 敌方英雄对象
self._player_minions = list(active_player_minions)
self._enemy_minions = list(active_enemy_minions) # 敌方场上随从(从左到右)
def __str__(self) -> str:
player = str(self._player)
player_minions = ";".join(
f"{m.get_symbol()},{m.get_health()},{m.get_shield()}"
for m in self._player_minions
)
enemy = str(self._enemy)
enemy_minions = ";".join(
f"{m.get_symbol()},{m.get_health()},{m.get_shield()}"
for m in self._enemy_minions
)
return f"{player}|{player_minions}|{enemy}|{enemy_minions}"
def __repr__(self) -> str:
return (
f"HearthModel({repr(self._player)}, {repr(self._player_minions)}, "
f"{repr(self._enemy)}, {repr(self._enemy_minions)})"
)
def get_player(self) -> Hero:
"""
Returns the player's Hero.
Returns:
Hero: The player's hero.
"""
return self._player
def get_enemy(self) -> Hero:
"""
Returns the enemy's hero.
Returns:
Hero: The enemy hero.
"""
return self._enemy
def get_player_minions(self) -> list[Minion]:
"""
returns a copy of the player's current minion list.
Returns:
list[Minions]: List of the player's minions.
"""
return list(self._player_minions)
def get_enemy_minions(self) -> list[Minion]:
"""
returns a copy of the enemy's current minion list.
Returns:
list[Minions]: List of the enemy's minions.
"""
return list(self._enemy_minions)
def has_won(self) -> bool:
"""
Determines if the player has won.
Returns:
bool: True is player has won, False otherwise.
"""
return (
self._player.is_alive()
and (
not self._enemy.is_alive()
or self._player.get_deck().remaining_count() == 0
)
)
def has_lost(self) -> bool:
"""
Determines if the player has lost.
Returns:
bool: True is player has lost, False otherwise.
"""
return not self._player.is_alive()
def play_card(self, card: Card, target: Entity) -> bool:
cost = card.get_cost()
if not self._player.spend_energy(cost):
return False
self._player._hand.remove(card)
if card.is_permanent():
if len(self._player_minions) >= 5:
self._player_minions.pop(0)
self._player_minions.append(card)
else:
effect = card.get_effect()
self._apply_effect(target, effect)
# if 'damage' in effect:
# target.apply_damage(effect['damage'])
# if 'health' in effect:
# target.apply_health(effect['health'])
# if 'shield' in effect:
# target.apply_shield(effect['shield'])
self._player_minions = [m for m in self._player_minions if m.is_alive()]
self._enemy_minions = [m for m in self._enemy_minions if m.is_alive()]
return True
def discard_card(self, card: Card):
self._player._hand.remove(card)
self._player.get_deck().add_card(card)
# def _apply_effect(self, card, tgt, eff) -> None:
# if (card in eff) and (card == "damage"):
# tgt.apply_damage(eff[card])
# if (card in eff) and (card == "health"):
# tgt.apply_health(eff[card])
# if (card in eff) and (card == "shield"):
# tgt.apply_shield(eff[card])
def _apply_effect(self, tgt, eff) -> None:
cards = ['damage', 'health', 'shield']
for card in cards:
if (card in eff) and (card == "damage"):
tgt.apply_damage(eff[card])
if (card in eff) and (card == "health"):
tgt.apply_health(eff[card])
if (card in eff) and (card == "shield"):
tgt.apply_shield(eff[card])
def end_turn(self) -> list[str]:
"""
结束玩家回合
1. 玩家随从依次触发
2. 敌方英雄手牌火球回合数+1
3. 敌方抽卡、能量更新
4. 敌方出牌:始终尝试打出手牌中的每张牌,能打就打,然后重新遍历;
5. 敌方随从依次触发
6. 玩家的新回合:手牌火球更新、抽卡、能量的更新
返回:敌方出牌的牌名列表,用于终端 UI 现实
"""
played_by_enemy: list[str] = [] # 存储数据
# --- 步骤 1:玩家随从触发----
for m in list(self._player_minions):
tgt = m.choose_target(
self._player, self._enemy,
self._player_minions, self._enemy_minions
)
eff = m.get_effect()
self._apply_effect(tgt, eff)
# cards = ['damage', 'health', 'shield']
# for c in cards:
# self._apply_effect(c, tgt, eff)
# if 'damage' in eff:
# tgt.apply_damage(eff['damage'])
# if 'health' in eff:
# tgt.apply_health(eff['health'])
# if 'shield' in eff:
# tgt.apply_shield(eff['shield'])
# 清理阵亡随从
# self._player_minions = [m for m in self._player_minions if m.is_alive()]
self._player_minions = []
for m in self._player_minions:
if m.is_alive():
self._player_minions.append(m)
# self._enemy_minions = [m for m in self._enemy_minions if m.is_alive()]
self._enemy_minions = []
for m in self._enemy_minions:
if m.is_alive():
self._enemy_minions.append(m)
# --- 步骤 2:
for card in self._enemy.get_hand():
# if isinstance(card, Fireball):
if type(card) == Fireball:
card.increment_turn()
while len(self._enemy.get_hand()) < 5:
drawn = self._enemy.get_deck().draw_cards(1)
if not drawn:
break
self._enemy._hand.extend(drawn)
self._enemy._max_energy += 1
self._enemy._energy = self._enemy._max_energy
if not self._enemy.is_alive():
return played_by_enemy
# 敌方出牌逻辑
made_play = True
while made_play:
made_play = False
for card in list(self._enemy.get_hand()):
cost = card.get_cost() # 取出该牌的法力消耗
if self._enemy.get_energy() >= cost: # 若敌方当前能力 >= 消耗,才有资格出牌
# 选择目标:有伤害则打玩家,加持自己
eff = card.get_effect()
if 'damage' in eff:
tgt = self._player
else:
tgt = self._enemy
# tgt = self._player if 'damage' in eff else self._enemy # 若效果里面含有 damage 则攻击玩家,否则给自己施法。
# 支付能量并出牌
self._enemy.spend_energy(cost) # 扣除能量(若不足,这步返回 False,但这里已保证足够)
self._enemy._hand.remove(card) # 从手牌移除这张牌
if card.is_permanent(): # 若是随从类型(永久牌)——放入随从槽
if len(self._enemy_minions) >= 5: # 若槽已满,先挤掉最左边的老随从
# self._enemy_minions.pop(0)
self._enemy_minions = self._enemy_minions[1:]
self._enemy_minions.append(card) # 将新随从插到最右侧槽位
else:
self._apply_effect(tgt, eff)
# cards = ['damage', 'health', 'shield']
# for c in cards:
# self._apply_effect(c, tgt, eff)
# if 'damage' in eff:
# tgt.apply_damage(eff['damage'])
# if 'health' in eff:
# tgt.apply_health(eff['health'])
# if 'shield' in eff:
# tgt.apply_shield(eff['shield'])
played_by_enemy.append(card.get_name())
made_play = True
break
for m in list(self._enemy_minions):
tgt = m.choose_target(
self._enemy, self._player,
self._enemy_minions, self._player_minions
)
eff = m.get_effect()
self._apply_effect(tgt, eff)
# cards = ['damage', 'health', 'shield']
# for c in cards:
# self._apply_effect(c, tgt, eff)
# if 'damage' in eff:
# tgt.apply_damage(eff['damage'])
# if 'health' in eff:
# tgt.apply_health(eff['health'])
# if 'shield' in eff:
# tgt.apply_shield(eff['shield'])
self._player_minions = [m for m in self._player_minions if m.is_alive()]
self._enemy_minions = [m for m in self._enemy_minions if m.is_alive()]
for card in self._player.get_hand():
# if isinstance(card, Fireball):
if type(card) == Fireball:
card.increment_turn()
while len(self._player.get_hand()) < 5: # 抽牌直到手牌满 5 张或牌库耗尽
drawn = self._player.get_deck().draw_cards(1)
if not drawn: # 若已没有牌可抽则停止
break
self._player._hand.extend(drawn)
self._player._max_energy += 1 # 能量上限 +1
self._player._energy = self._player._max_energy # 将当前能量补满到新上限
return played_by_enemy # 返回本回合敌方曾打出的牌名列表
def main() -> None:
"""
"""
pass
if __name__ == "__main__":
main()
公众号:AI悦创【二维码】

AI悦创·编程一对一
AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发、Web、Linux」,招收学员面向国内外,国外占 80%。全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh
C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh
方法一:QQ
方法二:微信:Jiabcdefh

更新日志
471be
-于ac069
-于e7af2
-于