Skip to content

PlayerSpace

Represents the physical play space of card zones and accompanying metadata associated with a player.

Note

Despite most usually having only one card, all zones are treated as card lists so that certain effects may be tracked properly (like light heroes placing cards into their "soul").

The top card of any zone is considered the last element of that zone's list of cards.

Attributes:

Name Type Description
arms CardList

Any cards in the arms equipment zone.

arsenal CardList

Any cards currently in the player's arsenal.

banished CardList

Any cards currently in the player's banished zone.

chest CardList

Any cards in the chest equipment zone.

deck CardList

The "main" deck of the player.

graveyard CardList

Any cards currently in the player's graveyard.

hand CardList

Any cards currently in the player's hand.

head CardList

Any cards in the head equipment zone.

hero CardList

Any cards in the hero zone.

legs CardList

Any cards in the legs equipment zone.

permanent CardList

Any cards in the permanent zone (typically active tokens, items, or allies).

pitch CardList

Any cards currently in the player's pitch zone.

primary_weapon CardList

Any cards in the primary weapon zone.

secondary_weapon CardList

Any cards in the secondary weapon zone.

Source code in fab/arena.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
@dataclasses.dataclass
class PlayerSpace:
    '''
    Represents the physical play space of card zones and accompanying metadata
    associated with a player.

    Note:
      Despite most usually having only one card, all zones are treated as card
      lists so that certain effects may be tracked properly (like light heroes
      placing cards into their "soul").

      The top card of any zone is considered the last element of that zone's
      list of cards.

    Attributes:
      arms: Any cards in the arms equipment zone.
      arsenal: Any cards currently in the player's arsenal.
      banished: Any cards currently in the player's banished zone.
      chest: Any cards in the chest equipment zone.
      deck: The "main" deck of the player.
      graveyard: Any cards currently in the player's graveyard.
      hand: Any cards currently in the player's hand.
      head: Any cards in the head equipment zone.
      hero: Any cards in the hero zone.
      legs: Any cards in the legs equipment zone.
      permanent: Any cards in the permanent zone (typically active tokens, items, or allies).
      pitch: Any cards currently in the player's pitch zone.
      primary_weapon: Any cards in the primary weapon zone.
      secondary_weapon: Any cards in the secondary weapon zone.
    '''
    arms: CardList = CardList.empty()
    arsenal: CardList = CardList.empty()
    banished: CardList = CardList.empty()
    chest: CardList = CardList.empty()
    deck: CardList = CardList.empty()
    graveyard: CardList = CardList.empty()
    hand: CardList = CardList.empty()
    head: CardList = CardList.empty()
    hero: CardList = CardList.empty()
    legs: CardList = CardList.empty()
    permanent: CardList = CardList.empty()
    pitch: CardList = CardList.empty()
    primary_weapon: CardList = CardList.empty()
    secondary_weapon: CardList = CardList.empty()

    def clear_pitch(self, order: Optional[list[int]] = [], return_to_hand: bool = False) -> None:
        '''
        Clears the pitch zone of cards, placing them on the bottom of the deck.

        If the `order` argument is `None`, then the pitched cards will be placed
        on the bottom of the deck in a random order. Otherwise, a list of
        integers may be specified corresponding to the order of indices. An
        empty list indicates that the order should be preserved.

        Args:
          order: The order in which cards should be returned to the deck, or `None` to randomize the order.
          return_to_hand: Whether to return the pitched cards to the hand rather than placing them in the deck.
        '''
        if not order is None:
            cache = [self.pitch[i] for i in order] if order else copy.deepcopy(self.pitch.data)
        else:
            cache = copy.deepcopy(self.pitch)
            random.shuffle(cache)
        if return_to_hand:
            self.hand.extend(cache)
        else:
            self.deck[:0] = cache
        self.pitch = CardList.empty()

    def draw_hand(self, int_modifier: int = 0) -> None:
        '''
        Draws cards according to the hero card's intelligence.

        This method automatically moves cards from the `deck` field to the `hand`
        field.

        Args:
          int_modifier: An additional value to add to the effective hero intelligence.
        '''
        hero_int = self.hero_card().intelligence
        target_cards = (hero_int if not hero_int is None else 0) + int_modifier
        while len(self.hand) < target_cards:
            self.hand.append(self.deck.pop())

    @staticmethod
    def from_deck(
        deck: Deck,
        arms: Optional[Card | str] = None,
        chest: Optional[Card | str] = None,
        head: Optional[Card | str] = None,
        legs: Optional[Card | str] = None,
        primary_weapon: Optional[Card | str] = None,
        secondary_weapon: Optional[Card | str] = None
    ) -> PlayerSpace:
        '''
        Creates a new player space from the specified deck.

        Args:
          arms: An optional `Card` or full name of a card to place in the arms equipment zone.
          chest: An optional `Card` or full name of a card to place in the chest equipment zone.
          deck: The `Deck` object from which this player space should be constructed.
          head: An optional `Card` or full name of a card to place in the head equipment zone.
          legs: An optional `Card` or full name of a card to place in the legs equipment zone.
          primary_weapon: An optional `Card` or full name of a card to place in the primary weapon zone.
          secondary_weapon: An optional `Card` or full name of a card to place in the secondary weapon zone.

        Returns:
          A new player space from the specified deck.
        '''
        res = PlayerSpace(
            deck = copy.deepcopy(deck.cards),
            hero = CardList([copy.deepcopy(deck.hero)])
        )
        if isinstance(arms, str):
            res.arms = copy.deepcopy(deck.inventory.filter(full_name=arms))
        elif isinstance(arms, Card):
            res.arms.append(copy.deepcopy(arms))
        if isinstance(chest, str):
            res.chest = copy.deepcopy(deck.inventory.filter(full_name=chest))
        elif isinstance(chest, Card):
            res.chest.append(copy.deepcopy(chest))
        if isinstance(head, str):
            res.head = copy.deepcopy(deck.inventory.filter(full_name=head))
        elif isinstance(head, Card):
            res.head.append(copy.deepcopy(head))
        if isinstance(legs, str):
            res.legs = copy.deepcopy(deck.inventory.filter(full_name=legs))
        elif isinstance(legs, Card):
            res.legs.append(copy.deepcopy(legs))
        if isinstance(primary_weapon, str):
            res.primary_weapon = copy.deepcopy(deck.inventory.filter(full_name=primary_weapon))
        elif isinstance(primary_weapon, Card):
            res.primary_weapon.append(copy.deepcopy(primary_weapon))
        if isinstance(secondary_weapon, str):
            res.secondary_weapon = copy.deepcopy(deck.inventory.filter(full_name=secondary_weapon))
        elif isinstance(secondary_weapon, Card):
            res.secondary_weapon.append(copy.deepcopy(secondary_weapon))
        return res

    def hero_card(self) -> Card:
        '''
        Determines the _actual_ hero card within the player space's hero zone.

        Note:
          This is needed because multiple cards may exist in the hero zone.

        Returns:
          The hero card contained in the hero zone.
        '''
        return [card for card in self.hero if card.is_hero()][0]

    def primary_weapon_card(self) -> Card:
        '''
        Determines the _actual_ primary weapon card within the player space's
        primary weapon zone.

        Note:
          This is needed because multiple cards may exist in the primary weapon
          zone.

        Returns:
          The primary weapon card contained in the primary weapon zone.
        '''
        return self.primary_weapon.filter(types=['Weapon', 'Off-Hand'])[0]

    def redraw_hand(self, int_modifier: int = 0) -> None:
        '''
        Shuffles the current hand back into the deck, drawing a new one.

        Args:
          int_modifier: An additional modifier to add to the hero's intelligence value.
        '''
        self.deck.extend(self.hand)
        self.hand = CardList.empty()
        self.shuffle_deck()
        self.draw_hand(int_modifier=int_modifier)

    def reset_zones(self) -> None:
        '''
        Resets the zones of the player space.

        This method moves all cards from the pitch, graveyard and banished zones
        back into the deck (or other appropriate place), which is then shuffled.
        In addition:
          * Any cards in hand will be returned to the deck.
          * Equipment cards will be returned to their appropriate zone.
          * Token cards will deleted.
          * Non-token cards in the permanent zone will be returned to their appropriate zone.
        '''
        collected: CardList = copy.deepcopy(
            self.arms +
            self.arsenal +
            self.banished +
            self.chest +
            self.graveyard +
            self.hand +
            self.head +
            self.hero +
            self.legs +
            self.permanent +
            self.pitch +
            self.primary_weapon +
            self.secondary_weapon
        )
        self.arms = collected.filter(types='Arms')
        self.arsenal = CardList.empty()
        self.banished = CardList.empty()
        self.chest = collected.filter(types='Chest')
        self.graveyard = CardList.empty()
        self.hand = CardList.empty()
        self.head = collected.filter(types='Head')
        self.hero = collected.filter(types='Hero')
        self.legs = collected.filter(types='Legs')
        self.permanent = CardList.empty()
        self.pitch = CardList.empty()
        weapons = collected.filter(types=['Weapon', 'Off-Hand'])
        if len(weapons) == 0:
            self.primary_weapon = CardList.empty()
            self.secondary_weapon = CardList.empty()
        elif len(weapons) == 1:
            self.primary_weapon = weapons
            self.secondary_weapon = CardList.empty()
        elif len(weapons) == 2:
            self.primary_weapon = CardList([weapons[0]])
            self.secondary_weapon = CardList([weapons[1]])
        else:
            raise Exception('somehow the reset_zone method detected more than 2 weapon-zone cards')
        self.deck.extend(
            collected.filter(types=['Arms', 'Chest', 'Head', 'Hero', 'Legs', 'Off-Hand', 'Token', 'Weapon'], negate=True)
        )
        self.shuffle_deck()

    def secondary_weapon_card(self) -> Card:
        '''
        Determines the _actual_ secondary weapon card within the player space's
        secondary weapon zone.

        Note:
          This is needed because multiple cards may exist in the secondary weapon
          zone.

        Returns:
          The secondary weapon card contained in the secondary weapon zone.
        '''
        return self.secondary_weapon.filter(types=['Weapon', 'Off-Hand'])[0]

    def shuffle_banished(self) -> None:
        '''
        Shuffles the banished zone in-place.
        '''
        random.shuffle(self.banished)

    def shuffle_deck(self) -> None:
        '''
        Shuffles the deck in-place.
        '''
        random.shuffle(self.deck)

    def shuffle_graveyard(self) -> None:
        '''
        Shuffles the graveyard in-place.
        '''
        random.shuffle(self.graveyard)

    def shuffle_hand(self) -> None:
        '''
        Shuffles the hand in-place.
        '''
        random.shuffle(self.hand)

clear_pitch(order=[], return_to_hand=False)

Clears the pitch zone of cards, placing them on the bottom of the deck.

If the order argument is None, then the pitched cards will be placed on the bottom of the deck in a random order. Otherwise, a list of integers may be specified corresponding to the order of indices. An empty list indicates that the order should be preserved.

Parameters:

Name Type Description Default
order Optional[list[int]]

The order in which cards should be returned to the deck, or None to randomize the order.

[]
return_to_hand bool

Whether to return the pitched cards to the hand rather than placing them in the deck.

False
Source code in fab/arena.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
def clear_pitch(self, order: Optional[list[int]] = [], return_to_hand: bool = False) -> None:
    '''
    Clears the pitch zone of cards, placing them on the bottom of the deck.

    If the `order` argument is `None`, then the pitched cards will be placed
    on the bottom of the deck in a random order. Otherwise, a list of
    integers may be specified corresponding to the order of indices. An
    empty list indicates that the order should be preserved.

    Args:
      order: The order in which cards should be returned to the deck, or `None` to randomize the order.
      return_to_hand: Whether to return the pitched cards to the hand rather than placing them in the deck.
    '''
    if not order is None:
        cache = [self.pitch[i] for i in order] if order else copy.deepcopy(self.pitch.data)
    else:
        cache = copy.deepcopy(self.pitch)
        random.shuffle(cache)
    if return_to_hand:
        self.hand.extend(cache)
    else:
        self.deck[:0] = cache
    self.pitch = CardList.empty()

draw_hand(int_modifier=0)

Draws cards according to the hero card's intelligence.

This method automatically moves cards from the deck field to the hand field.

Parameters:

Name Type Description Default
int_modifier int

An additional value to add to the effective hero intelligence.

0
Source code in fab/arena.py
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def draw_hand(self, int_modifier: int = 0) -> None:
    '''
    Draws cards according to the hero card's intelligence.

    This method automatically moves cards from the `deck` field to the `hand`
    field.

    Args:
      int_modifier: An additional value to add to the effective hero intelligence.
    '''
    hero_int = self.hero_card().intelligence
    target_cards = (hero_int if not hero_int is None else 0) + int_modifier
    while len(self.hand) < target_cards:
        self.hand.append(self.deck.pop())

from_deck(deck, arms=None, chest=None, head=None, legs=None, primary_weapon=None, secondary_weapon=None) staticmethod

Creates a new player space from the specified deck.

Parameters:

Name Type Description Default
arms Optional[Card | str]

An optional Card or full name of a card to place in the arms equipment zone.

None
chest Optional[Card | str]

An optional Card or full name of a card to place in the chest equipment zone.

None
deck Deck

The Deck object from which this player space should be constructed.

required
head Optional[Card | str]

An optional Card or full name of a card to place in the head equipment zone.

None
legs Optional[Card | str]

An optional Card or full name of a card to place in the legs equipment zone.

None
primary_weapon Optional[Card | str]

An optional Card or full name of a card to place in the primary weapon zone.

None
secondary_weapon Optional[Card | str]

An optional Card or full name of a card to place in the secondary weapon zone.

None

Returns:

Type Description
PlayerSpace

A new player space from the specified deck.

Source code in fab/arena.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
@staticmethod
def from_deck(
    deck: Deck,
    arms: Optional[Card | str] = None,
    chest: Optional[Card | str] = None,
    head: Optional[Card | str] = None,
    legs: Optional[Card | str] = None,
    primary_weapon: Optional[Card | str] = None,
    secondary_weapon: Optional[Card | str] = None
) -> PlayerSpace:
    '''
    Creates a new player space from the specified deck.

    Args:
      arms: An optional `Card` or full name of a card to place in the arms equipment zone.
      chest: An optional `Card` or full name of a card to place in the chest equipment zone.
      deck: The `Deck` object from which this player space should be constructed.
      head: An optional `Card` or full name of a card to place in the head equipment zone.
      legs: An optional `Card` or full name of a card to place in the legs equipment zone.
      primary_weapon: An optional `Card` or full name of a card to place in the primary weapon zone.
      secondary_weapon: An optional `Card` or full name of a card to place in the secondary weapon zone.

    Returns:
      A new player space from the specified deck.
    '''
    res = PlayerSpace(
        deck = copy.deepcopy(deck.cards),
        hero = CardList([copy.deepcopy(deck.hero)])
    )
    if isinstance(arms, str):
        res.arms = copy.deepcopy(deck.inventory.filter(full_name=arms))
    elif isinstance(arms, Card):
        res.arms.append(copy.deepcopy(arms))
    if isinstance(chest, str):
        res.chest = copy.deepcopy(deck.inventory.filter(full_name=chest))
    elif isinstance(chest, Card):
        res.chest.append(copy.deepcopy(chest))
    if isinstance(head, str):
        res.head = copy.deepcopy(deck.inventory.filter(full_name=head))
    elif isinstance(head, Card):
        res.head.append(copy.deepcopy(head))
    if isinstance(legs, str):
        res.legs = copy.deepcopy(deck.inventory.filter(full_name=legs))
    elif isinstance(legs, Card):
        res.legs.append(copy.deepcopy(legs))
    if isinstance(primary_weapon, str):
        res.primary_weapon = copy.deepcopy(deck.inventory.filter(full_name=primary_weapon))
    elif isinstance(primary_weapon, Card):
        res.primary_weapon.append(copy.deepcopy(primary_weapon))
    if isinstance(secondary_weapon, str):
        res.secondary_weapon = copy.deepcopy(deck.inventory.filter(full_name=secondary_weapon))
    elif isinstance(secondary_weapon, Card):
        res.secondary_weapon.append(copy.deepcopy(secondary_weapon))
    return res

hero_card()

Determines the actual hero card within the player space's hero zone.

Note

This is needed because multiple cards may exist in the hero zone.

Returns:

Type Description
Card

The hero card contained in the hero zone.

Source code in fab/arena.py
222
223
224
225
226
227
228
229
230
231
232
def hero_card(self) -> Card:
    '''
    Determines the _actual_ hero card within the player space's hero zone.

    Note:
      This is needed because multiple cards may exist in the hero zone.

    Returns:
      The hero card contained in the hero zone.
    '''
    return [card for card in self.hero if card.is_hero()][0]

primary_weapon_card()

Determines the actual primary weapon card within the player space's primary weapon zone.

Note

This is needed because multiple cards may exist in the primary weapon zone.

Returns:

Type Description
Card

The primary weapon card contained in the primary weapon zone.

Source code in fab/arena.py
234
235
236
237
238
239
240
241
242
243
244
245
246
def primary_weapon_card(self) -> Card:
    '''
    Determines the _actual_ primary weapon card within the player space's
    primary weapon zone.

    Note:
      This is needed because multiple cards may exist in the primary weapon
      zone.

    Returns:
      The primary weapon card contained in the primary weapon zone.
    '''
    return self.primary_weapon.filter(types=['Weapon', 'Off-Hand'])[0]

redraw_hand(int_modifier=0)

Shuffles the current hand back into the deck, drawing a new one.

Parameters:

Name Type Description Default
int_modifier int

An additional modifier to add to the hero's intelligence value.

0
Source code in fab/arena.py
248
249
250
251
252
253
254
255
256
257
258
def redraw_hand(self, int_modifier: int = 0) -> None:
    '''
    Shuffles the current hand back into the deck, drawing a new one.

    Args:
      int_modifier: An additional modifier to add to the hero's intelligence value.
    '''
    self.deck.extend(self.hand)
    self.hand = CardList.empty()
    self.shuffle_deck()
    self.draw_hand(int_modifier=int_modifier)

reset_zones()

Resets the zones of the player space.

This method moves all cards from the pitch, graveyard and banished zones back into the deck (or other appropriate place), which is then shuffled.

In addition
  • Any cards in hand will be returned to the deck.
  • Equipment cards will be returned to their appropriate zone.
  • Token cards will deleted.
  • Non-token cards in the permanent zone will be returned to their appropriate zone.
Source code in fab/arena.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
def reset_zones(self) -> None:
    '''
    Resets the zones of the player space.

    This method moves all cards from the pitch, graveyard and banished zones
    back into the deck (or other appropriate place), which is then shuffled.
    In addition:
      * Any cards in hand will be returned to the deck.
      * Equipment cards will be returned to their appropriate zone.
      * Token cards will deleted.
      * Non-token cards in the permanent zone will be returned to their appropriate zone.
    '''
    collected: CardList = copy.deepcopy(
        self.arms +
        self.arsenal +
        self.banished +
        self.chest +
        self.graveyard +
        self.hand +
        self.head +
        self.hero +
        self.legs +
        self.permanent +
        self.pitch +
        self.primary_weapon +
        self.secondary_weapon
    )
    self.arms = collected.filter(types='Arms')
    self.arsenal = CardList.empty()
    self.banished = CardList.empty()
    self.chest = collected.filter(types='Chest')
    self.graveyard = CardList.empty()
    self.hand = CardList.empty()
    self.head = collected.filter(types='Head')
    self.hero = collected.filter(types='Hero')
    self.legs = collected.filter(types='Legs')
    self.permanent = CardList.empty()
    self.pitch = CardList.empty()
    weapons = collected.filter(types=['Weapon', 'Off-Hand'])
    if len(weapons) == 0:
        self.primary_weapon = CardList.empty()
        self.secondary_weapon = CardList.empty()
    elif len(weapons) == 1:
        self.primary_weapon = weapons
        self.secondary_weapon = CardList.empty()
    elif len(weapons) == 2:
        self.primary_weapon = CardList([weapons[0]])
        self.secondary_weapon = CardList([weapons[1]])
    else:
        raise Exception('somehow the reset_zone method detected more than 2 weapon-zone cards')
    self.deck.extend(
        collected.filter(types=['Arms', 'Chest', 'Head', 'Hero', 'Legs', 'Off-Hand', 'Token', 'Weapon'], negate=True)
    )
    self.shuffle_deck()

secondary_weapon_card()

Determines the actual secondary weapon card within the player space's secondary weapon zone.

Note

This is needed because multiple cards may exist in the secondary weapon zone.

Returns:

Type Description
Card

The secondary weapon card contained in the secondary weapon zone.

Source code in fab/arena.py
315
316
317
318
319
320
321
322
323
324
325
326
327
def secondary_weapon_card(self) -> Card:
    '''
    Determines the _actual_ secondary weapon card within the player space's
    secondary weapon zone.

    Note:
      This is needed because multiple cards may exist in the secondary weapon
      zone.

    Returns:
      The secondary weapon card contained in the secondary weapon zone.
    '''
    return self.secondary_weapon.filter(types=['Weapon', 'Off-Hand'])[0]

shuffle_banished()

Shuffles the banished zone in-place.

Source code in fab/arena.py
329
330
331
332
333
def shuffle_banished(self) -> None:
    '''
    Shuffles the banished zone in-place.
    '''
    random.shuffle(self.banished)

shuffle_deck()

Shuffles the deck in-place.

Source code in fab/arena.py
335
336
337
338
339
def shuffle_deck(self) -> None:
    '''
    Shuffles the deck in-place.
    '''
    random.shuffle(self.deck)

shuffle_graveyard()

Shuffles the graveyard in-place.

Source code in fab/arena.py
341
342
343
344
345
def shuffle_graveyard(self) -> None:
    '''
    Shuffles the graveyard in-place.
    '''
    random.shuffle(self.graveyard)

shuffle_hand()

Shuffles the hand in-place.

Source code in fab/arena.py
347
348
349
350
351
def shuffle_hand(self) -> None:
    '''
    Shuffles the hand in-place.
    '''
    random.shuffle(self.hand)