Skip to content

Deck

Card

Bases: object

Card class

Example use
from automation.simulation.deck import Card
Card("SA") == Card("S","A")
> True

Attributes:

Name Type Description
suit str

one of [D,C,H,S,R,B] for Diamond, Club, Heart, Spade, Red, Black

val str

one of [A, K, Q, J, T] for Ace to Ten. Or 2 to 9.

val_number int

integer corresponding to above value

color str

either R or B for red or black

Source code in automation/simulator/deck.py
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 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
class Card(object):
    """Card class

    Example use:
        ```python
        from automation.simulation.deck import Card
        Card("SA") == Card("S","A")
        > True
        ```

    Attributes:
        suit (str): one of [D,C,H,S,R,B] for Diamond, Club, Heart, Spade, Red, Black
        val (str): one of [A, K, Q, J, T] for Ace to Ten. Or 2 to 9.
        val_number (int): integer corresponding to above value
        color (str): either R or B for red or black
    """

    def __init__(self, suit, val=" "):
        if len(suit) == 2:
            self.suit = suit[0].upper()
            self.val = suit[1].upper()
        elif val.lower() == "joker":
            self.suit = suit[0].upper()
            self.val = "Joker"
        else:
            self.suit = suit[0].upper()
            self.val = val[0].upper()

        if self.val != "Joker" and (
            self.suit not in all_suits or self.val not in all_vals
        ):
            if suit.lower() != "random":
                logger.warning(f"Couldn't make card value from input: {suit}, {val}")
            self.suit = random.choice(all_suits)
            self.val = random.choice(all_vals)

        self._val_to_num = {  # A:1, 2:2, ... T:10
            "A": 1,
            "2": 2,
            "3": 3,
            "4": 4,
            "5": 5,
            "6": 6,
            "7": 7,
            "8": 8,
            "9": 9,
            "T": 10,
            "J": 11,
            "Q": 12,
            "K": 0,
            "Joker": "Joker",
        }
        self._num_to_val = {v: k for k, v in self._val_to_num.items()}

    @property
    def suit_symbol(self):
        """Symbol for each suit, including red and black for jokers"""
        return {
            "D": "♦️",
            "C": "♠️",
            "H": "♥️",
            "S": "♠️",
            "R": "🟥",  # Red joker
            "B": "⬛",  # Black joker
        }[self.suit]

    @property
    def val_number(self):
        """Integer value for number, to permit calculations"""
        return self._val_to_num[self.val]

    @property
    def color(self):
        """R or B"""
        return "B" if self.suit in ["C", "S", "B"] else "R"

    def __repr__(self):
        """Result of print(card)"""
        return f"{self.suit_symbol} {self.val}"

    def __add__(self, x: int):
        """Result of card + integer. Loops around"""
        assert self.val != "Joker", "Cannot add/subtract Joker"
        return self._num_to_val[(self.val_number + x) % 13]

    def __sub__(self, x: int):
        """Result of card - integer. Loops around"""
        assert self.val != "Joker", "Cannot add/subtract Joker"
        return self._num_to_val[(self.val_number - x) % 13]

    def __eq__(self, x):
        """Returns true if two cards are the same"""
        return (self.suit == x.suit) & (self.val == x.val)

    def __hash__(self):
        """Returns unique value to represent card"""
        return hash((self.suit, self.val))

    def range(self, DR: int):
        """Returns list of cards in a DR"""
        DR = abs(DR)
        return [self + diff for diff in range(-DR, DR + 1)]

suit_symbol property

Symbol for each suit, including red and black for jokers

val_number property

Integer value for number, to permit calculations

color property

R or B

__repr__()

Result of print(card)

Source code in automation/simulator/deck.py
87
88
89
def __repr__(self):
    """Result of print(card)"""
    return f"{self.suit_symbol} {self.val}"

__add__(x)

Result of card + integer. Loops around

Source code in automation/simulator/deck.py
91
92
93
94
def __add__(self, x: int):
    """Result of card + integer. Loops around"""
    assert self.val != "Joker", "Cannot add/subtract Joker"
    return self._num_to_val[(self.val_number + x) % 13]

__sub__(x)

Result of card - integer. Loops around

Source code in automation/simulator/deck.py
96
97
98
99
def __sub__(self, x: int):
    """Result of card - integer. Loops around"""
    assert self.val != "Joker", "Cannot add/subtract Joker"
    return self._num_to_val[(self.val_number - x) % 13]

__eq__(x)

Returns true if two cards are the same

Source code in automation/simulator/deck.py
101
102
103
def __eq__(self, x):
    """Returns true if two cards are the same"""
    return (self.suit == x.suit) & (self.val == x.val)

__hash__()

Returns unique value to represent card

Source code in automation/simulator/deck.py
105
106
107
def __hash__(self):
    """Returns unique value to represent card"""
    return hash((self.suit, self.val))

range(DR)

Returns list of cards in a DR

Source code in automation/simulator/deck.py
109
110
111
112
def range(self, DR: int):
    """Returns list of cards in a DR"""
    DR = abs(DR)
    return [self + diff for diff in range(-DR, DR + 1)]

Deck

Bases: object

Full deck of cards

Example use
from automation.simulation.deck import Deck
d=Deck()
d.draw()
> ♦️ A
d.check(TC=Card("S","A"),DR=3)
> [INFO]: Drew ♦️ 8 vs ♠️ A with DR 3: Miss

Attributes:

Name Type Description
suits tuple

all suits in deck

vals tuple

all values in deck

cards list

source deck

discards list

discard pile

hand list

jokers and fate cards in hand

Source code in automation/simulator/deck.py
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
352
353
354
355
356
357
358
359
360
361
362
363
364
class Deck(object):
    """Full deck of cards

    Example use:
        ```python
        from automation.simulation.deck import Deck
        d=Deck()
        d.draw()
        > ♦️ A
        d.check(TC=Card("S","A"),DR=3)
        > [INFO]: Drew ♦️ 8 vs ♠️ A with DR 3: Miss
        ```

    Attributes:
        suits (tuple): all suits in deck
        vals (tuple): all values in deck
        cards (list): source deck
        discards (list): discard pile
        hand (list): jokers and fate cards in hand
    """

    def __init__(self, use_TC=True):
        self.cards, self.hand, self.discards = [], [], []
        self._jokers = [Card("B", "Joker"), Card("R", "Joker")]
        # "Start" with all discarded. Shuffle assumes only shuffle from discard to cards
        self.discards.extend([Card(s, v) for s in all_suits for v in all_vals])
        self.hand.extend(self._jokers)
        self._use_TC = use_TC
        self.Name = "GM" if not self._use_TC else "Deck"
        self._TC = None
        self.shuffle()
        self.result_types = {
            "Critical Success": 5,
            "Major Success": 4,
            "Suited Hit": 3,
            "Color Hit": 2,
            "Hit": 1,
            "No result": 0,
            "Suited Miss": -1,
            "Color Miss": -2,
            "Miss": -3,
        }
        self.result_types.update(dict([reversed(i) for i in self.result_types.items()]))

    def __repr__(self):
        """Result of print(Deck)"""
        output = ""
        output += f"TC      : {self.TC}\n"
        output += f"Hand    :  {len(self.hand):02d}\n"
        output += f"Deck    :  {len(self.cards):02d}\n"
        output += f"Discards:  {len(self.discards):02d}\n"
        return output

    def shuffle(self, limit: int = None):
        """Shuffle N from discard to deck

        If limit provided, only shuffle those from discard. If no limit, reshuffle all
        discarded. Add jokers back to hand.

        Args:
            limit (int, optional): Number of cards to shuffle back into deck. Defaults
                to None.
        """
        random.shuffle(self.discards)
        if not limit:
            limit = len(self.discards)
            self.hand = [*set((*self.hand, *self._jokers))]  # Set removes duplicates
        self.cards.extend(self.discards[:limit])
        self.discards = self.discards[limit:]
        random.shuffle(self.cards)
        if self._use_TC:
            self.draw_TC()

    @property
    def TC(self) -> Card:
        """Target card

        Returns:
            Card: Current Target Card from deck. Not applicable to GM decks.
        """
        if not self._TC and self._use_TC:
            self.draw_TC
        return self._TC

    def draw_TC(self):
        """Draw a new target card"""
        self._TC = self.draw()

    def draw(self) -> Card | None:
        """Draw a card, if available. Otherwise returns None.

        Returns:
            Card: _description_
        """
        """Draw a card. If any available, return card"""
        if len(self.cards) == 0 and self._use_TC:
            logger.warning("No cards available in deck.")
            return None
        elif len(self.cards) == 0 and not self._use_TC:
            self.shuffle()  # for GMs, just shuffle
        card = self.cards.pop()
        if card.val == "A":
            self.hand.append(card)
        else:
            self.discards.append(card)
        return card

    def check_by_skill(self, **kwargs):
        if self._use_TC:
            raise TypeError("check_by_skill method invoked on a non-GM deck")
        kwargs.pop("skill", None)
        return self.check(**kwargs)

    def discard(self, n: int, return_string=False, **_) -> str | None:
        """Draw n cards, return none. Discard/hand as normal

        Args:
            n (int): Number of cards to discard. If "all", uses discards all remaining.
            return_string (bool, optional): Return a string reflecting result. Defaults
                to False.

        Returns:
            str | None: If return_string, report "Drew X"
        """ """"""
        if n == "all":
            n = len(self.cards)

        draws = []
        for _ in range(n):
            draws.append(self.draw())

        if return_string:
            return f"Drew {draws}"

    def exchange_fate(self, return_string=False) -> str | None:
        """Move fate card from hand. If Ace, add to discard

        Args:
            return_string (bool, optional): Default to False"""
        if len(self.hand) == 0:
            result = "No cards available to exchange"
        else:
            card = self.hand.pop()
            if card.val == "A":
                self.discards.append(card)
            result = f"Exchanged Fate Card: {card}"

        if return_string:
            return result

        logger.info(result)

    def _basic_check(self, TC: Card, DR: int) -> None | int:
        """Return string corresponding to check 'Hit/Miss/Color/Suit' etc

        Args:
            TC (Card): Target card
            DR: (int): Difficulty Range
            mod (int): DR modifier
        """
        DR = abs(DR)
        draw = self.draw()
        result = ""
        if draw is None:
            result += "No result"
        elif draw == TC:
            result += "Critical Success"
        elif draw.val == TC.val:
            result += "Major Success"
        else:
            if draw.suit == TC.suit:
                result += "Suited "
            elif draw.color == TC.color:
                result += "Color "
            result += "Hit" if draw.val in TC.range(DR) else "Miss"
        return (draw, self.result_types[result])  # Return (draw, int)

    def check(
        self,
        TC: Card,
        DR: int,
        mod: int = 0,
        upper_lower: str = "none",
        draw_n: int = 1,
        upper_lower_int: int = 0,
        draw_all: bool = False,
        return_val: bool = False,
        return_string: bool = False,
        verbose=True,
    ) -> tuple[int, str] | str:
        """Log string corresponding to check 'Hit/Miss/Color/Suit' etc

        Args:
            TC (Card): Target card
            DR: (int): Difficulty Range
            mod (int): DR modifier
            upper_lower (str): 'upper' or 'lower' Hand ('u' or 'l'). Default neither.
            draw_n (int): How many to draw. If upper/lower, default 2. Otherwise 1.
            upper_lower_int (int): Instead of passing upper_lower and draw_n, use
                positive/negative for upper/lower with int of draw_n -1.
                for example, -1 for draw 2 lower
            draw_all (bool): If upper hand, draw all before stopping. Default false.
            return_val (bool): Return the integer of the result. Default False.
            return_string (bool): Return the string describing what happened. Default
                False.
            verbose (bool): Log the result string as a debug item

        Returns:
            tuple | str: If return_string, returns a tuple containing the result as a
                string, followed by the result value. If return_val, only integer result.
        """

        DR = max(0, abs(DR) + mod)  # Apply mod to non-negative TR
        if upper_lower_int:
            upper_lower = (
                "U" if upper_lower_int > 0 else "L" if upper_lower_int < 0 else "N"
            )
            draw_n = 1 if upper_lower == "N" else abs(upper_lower_int) + 1
        else:
            upper_lower = upper_lower[0].upper()
            draw_n = 1 if upper_lower == "N" else 2 if abs(draw_n) == 1 else abs(draw_n)

        results = []
        draws = []

        for _ in range(draw_n):
            draw, this_result = self._basic_check(TC, DR)
            draws.append(draw)
            results.append(this_result)
            if results[-1] > 0 and not draw_all and upper_lower == "U":
                break  # If success (>0) and not draw-all with upper, stop drawing

        ul_str = ""
        if upper_lower == "U":
            ul_str = f" at Upper Hand {draw_n}"
        elif upper_lower == "L":
            ul_str = f" at Lower Hand {draw_n}"

        result = max(results) if upper_lower == "U" else min(results)
        result_string = (
            f"Drew {draws} vs {TC} with DR {DR}{ul_str}: {self.result_types[result]}"
        )

        if verbose:
            logger.debug(result_string)

        if return_string:
            return result_string, result
        elif return_val:
            return result

__repr__()

Result of print(Deck)

Source code in automation/simulator/deck.py
159
160
161
162
163
164
165
166
def __repr__(self):
    """Result of print(Deck)"""
    output = ""
    output += f"TC      : {self.TC}\n"
    output += f"Hand    :  {len(self.hand):02d}\n"
    output += f"Deck    :  {len(self.cards):02d}\n"
    output += f"Discards:  {len(self.discards):02d}\n"
    return output

shuffle(limit=None)

Shuffle N from discard to deck

If limit provided, only shuffle those from discard. If no limit, reshuffle all discarded. Add jokers back to hand.

Parameters:

Name Type Description Default
limit int

Number of cards to shuffle back into deck. Defaults to None.

None
Source code in automation/simulator/deck.py
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def shuffle(self, limit: int = None):
    """Shuffle N from discard to deck

    If limit provided, only shuffle those from discard. If no limit, reshuffle all
    discarded. Add jokers back to hand.

    Args:
        limit (int, optional): Number of cards to shuffle back into deck. Defaults
            to None.
    """
    random.shuffle(self.discards)
    if not limit:
        limit = len(self.discards)
        self.hand = [*set((*self.hand, *self._jokers))]  # Set removes duplicates
    self.cards.extend(self.discards[:limit])
    self.discards = self.discards[limit:]
    random.shuffle(self.cards)
    if self._use_TC:
        self.draw_TC()

TC: Card property

Target card

Returns:

Name Type Description
Card Card

Current Target Card from deck. Not applicable to GM decks.

draw_TC()

Draw a new target card

Source code in automation/simulator/deck.py
199
200
201
def draw_TC(self):
    """Draw a new target card"""
    self._TC = self.draw()

draw()

Draw a card, if available. Otherwise returns None.

Returns:

Name Type Description
Card Card | None

description

Source code in automation/simulator/deck.py
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
def draw(self) -> Card | None:
    """Draw a card, if available. Otherwise returns None.

    Returns:
        Card: _description_
    """
    """Draw a card. If any available, return card"""
    if len(self.cards) == 0 and self._use_TC:
        logger.warning("No cards available in deck.")
        return None
    elif len(self.cards) == 0 and not self._use_TC:
        self.shuffle()  # for GMs, just shuffle
    card = self.cards.pop()
    if card.val == "A":
        self.hand.append(card)
    else:
        self.discards.append(card)
    return card

discard(n, return_string=False, **_)

Draw n cards, return none. Discard/hand as normal

Parameters:

Name Type Description Default
n int

Number of cards to discard. If "all", uses discards all remaining.

required
return_string bool

Return a string reflecting result. Defaults to False.

False

Returns:

Type Description
str | None

str | None: If return_string, report "Drew X"

Source code in automation/simulator/deck.py
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
def discard(self, n: int, return_string=False, **_) -> str | None:
    """Draw n cards, return none. Discard/hand as normal

    Args:
        n (int): Number of cards to discard. If "all", uses discards all remaining.
        return_string (bool, optional): Return a string reflecting result. Defaults
            to False.

    Returns:
        str | None: If return_string, report "Drew X"
    """ """"""
    if n == "all":
        n = len(self.cards)

    draws = []
    for _ in range(n):
        draws.append(self.draw())

    if return_string:
        return f"Drew {draws}"

exchange_fate(return_string=False)

Move fate card from hand. If Ace, add to discard

Parameters:

Name Type Description Default
return_string bool

Default to False

False
Source code in automation/simulator/deck.py
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
def exchange_fate(self, return_string=False) -> str | None:
    """Move fate card from hand. If Ace, add to discard

    Args:
        return_string (bool, optional): Default to False"""
    if len(self.hand) == 0:
        result = "No cards available to exchange"
    else:
        card = self.hand.pop()
        if card.val == "A":
            self.discards.append(card)
        result = f"Exchanged Fate Card: {card}"

    if return_string:
        return result

    logger.info(result)

check(TC, DR, mod=0, upper_lower='none', draw_n=1, upper_lower_int=0, draw_all=False, return_val=False, return_string=False, verbose=True)

Log string corresponding to check 'Hit/Miss/Color/Suit' etc

Parameters:

Name Type Description Default
TC Card

Target card

required
DR int

(int): Difficulty Range

required
mod int

DR modifier

0
upper_lower str

'upper' or 'lower' Hand ('u' or 'l'). Default neither.

'none'
draw_n int

How many to draw. If upper/lower, default 2. Otherwise 1.

1
upper_lower_int int

Instead of passing upper_lower and draw_n, use positive/negative for upper/lower with int of draw_n -1. for example, -1 for draw 2 lower

0
draw_all bool

If upper hand, draw all before stopping. Default false.

False
return_val bool

Return the integer of the result. Default False.

False
return_string bool

Return the string describing what happened. Default False.

False
verbose bool

Log the result string as a debug item

True

Returns:

Type Description
tuple[int, str] | str

tuple | str: If return_string, returns a tuple containing the result as a string, followed by the result value. If return_val, only integer result.

Source code in automation/simulator/deck.py
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
352
353
354
355
356
357
358
359
360
361
362
363
364
def check(
    self,
    TC: Card,
    DR: int,
    mod: int = 0,
    upper_lower: str = "none",
    draw_n: int = 1,
    upper_lower_int: int = 0,
    draw_all: bool = False,
    return_val: bool = False,
    return_string: bool = False,
    verbose=True,
) -> tuple[int, str] | str:
    """Log string corresponding to check 'Hit/Miss/Color/Suit' etc

    Args:
        TC (Card): Target card
        DR: (int): Difficulty Range
        mod (int): DR modifier
        upper_lower (str): 'upper' or 'lower' Hand ('u' or 'l'). Default neither.
        draw_n (int): How many to draw. If upper/lower, default 2. Otherwise 1.
        upper_lower_int (int): Instead of passing upper_lower and draw_n, use
            positive/negative for upper/lower with int of draw_n -1.
            for example, -1 for draw 2 lower
        draw_all (bool): If upper hand, draw all before stopping. Default false.
        return_val (bool): Return the integer of the result. Default False.
        return_string (bool): Return the string describing what happened. Default
            False.
        verbose (bool): Log the result string as a debug item

    Returns:
        tuple | str: If return_string, returns a tuple containing the result as a
            string, followed by the result value. If return_val, only integer result.
    """

    DR = max(0, abs(DR) + mod)  # Apply mod to non-negative TR
    if upper_lower_int:
        upper_lower = (
            "U" if upper_lower_int > 0 else "L" if upper_lower_int < 0 else "N"
        )
        draw_n = 1 if upper_lower == "N" else abs(upper_lower_int) + 1
    else:
        upper_lower = upper_lower[0].upper()
        draw_n = 1 if upper_lower == "N" else 2 if abs(draw_n) == 1 else abs(draw_n)

    results = []
    draws = []

    for _ in range(draw_n):
        draw, this_result = self._basic_check(TC, DR)
        draws.append(draw)
        results.append(this_result)
        if results[-1] > 0 and not draw_all and upper_lower == "U":
            break  # If success (>0) and not draw-all with upper, stop drawing

    ul_str = ""
    if upper_lower == "U":
        ul_str = f" at Upper Hand {draw_n}"
    elif upper_lower == "L":
        ul_str = f" at Lower Hand {draw_n}"

    result = max(results) if upper_lower == "U" else min(results)
    result_string = (
        f"Drew {draws} vs {TC} with DR {DR}{ul_str}: {self.result_types[result]}"
    )

    if verbose:
        logger.debug(result_string)

    if return_string:
        return result_string, result
    elif return_val:
        return result