← Back to writeups

m0leCon Teaser CTF 2025 — Precipice

Have you ever played bala...precipice???

nc precipice.challs.m0lecon.it 14615

We're given a lengthy Python interface that looks like this:

py

1#!/usr/bin/env python3
2
3from numpy import float32
4
5from game_state import GameState
6from scoring import base_scores
7
8
9def play_round(state):
10    assert not state.round_over
11    last_cmd = ""
12    try:
13        while not state.round_over:
14            ts = input(">")
15            if len(ts) == 0:
16                ts = last_cmd
17            for t in ts.split(" "):
18                if len(t) == 0:
19                    continue
20                elif t in {"?", "h", "help", }:
21                    print(
22                        " \"?\" or \"h\" or \"help\" to show this message",
23                        " \"v\" or \"verbose\" to toggle verbose ui",
24                        " \"s\" or \"sort\" to toggle sorting by rank or by suit",
25                        " <a number> to select card by number",
26                        " \"p\" or \"play\" to play hand",
27                        " \"d\" or \"discard\" to discard hand",
28                        " \"a\" or \"ability\" to see the jokers abilities",
29                        " \"l\" or \"level\" to see hand level and scoring",
30                        sep="\n"
31                    )
32                elif t in {"v", "verbose", }:
33                    state.toggle_verbose_ui()
34                elif t in {"s", "sort", }:
35                    state.toggle_sorting_order()
36                elif t in {"p", "play", }:
37                    state.play()
38                elif t in {"d", "discard", }:
39                    state.discard()
40                elif t.isdecimal():
41                    state.select(int(t)-1)
42                elif t in {"a", "ability", }:
43                    for i, j in enumerate(state.jokers, start=1):
44                        print(
45                            f" -{i}: {j.name}: {j.description} {j.currently(state)}")
46                elif t in {"l", "level", }:
47                    for hand_type in base_scores.keys():
48                        hand_level = state.hand_levels[hand_type]
49                        chips, mult = [
50                            b * hand_level for b in base_scores[hand_type]]
51                        print(f" {hand_type}: {hand_level}: ${chips} X{mult}")
52                else:
53                    print(f"command not supported: {t!r}")
54            last_cmd = ts
55    except KeyboardInterrupt:
56        pass
57
58
59def play_game(state):
60    state.in_shop()
61    last_cmd = ""
62    try:
63        while not state.game_over:
64            t = input(">").strip()
65            if len(t) == 0:
66                t = last_cmd
67            if t in {"?", "h", "help", }:
68                print(
69                    " \"?\" or \"h\" or \"help\" to show this message",
70                    " \"v\" or \"verbose\" to toggle verbose ui",
71                    " \"p\" or \"play\" to buy in and play the next ante",
72                    " \"f\" or \"forfit\" to forfit (Loser)",
73                    f" \"r\" or \"reroll\" to reroll the shop (${state.reroll_shop_price})",
74                    " <a number> to select shop joker by number",
75                    " \"b\" or \"buy\" to buy select jokers",
76                    " -<a number> to select owned joker by number",
77                    " \"s\" or \"sell\" to sell select jokers",
78                    sep="\n"
79                )
80            elif t in {"v", "verbose", }:
81                state.toggle_verbose_ui()
82            elif t in {"p", "play", }:
83                state.out_shop()
84                state.buy_in()
85                play_round(state)
86                state.round_ended()
87                state.in_shop()
88            elif t in {"f", "forfit", }:
89                state.quit()
90                break
91            elif t in {"r", "reroll", }:
92                state.reroll_shop()
93            elif t.isdecimal():
94                state.select_shop_joker(int(t)-1)
95            elif t[0] == "-" and t[1:].isdecimal():
96                state.select_joker(int(t[1:])-1)
97            elif t in {"b", "buy", }:
98                state.buy_jokers()
99            elif t in {"s", "sell", }:
100                state.sell_jokers()
101            else:
102                print(f"command not supported: {t!r}")
103            last_cmd = t
104    except KeyboardInterrupt:
105        pass
106
107
108if __name__ == "__main__":
109    state = GameState(
110        initial_score=0,
111        initial_hands=5,
112        initial_discards=5,
113    )
114    play_game(state)

Reading through the source code, it looks like the server implements a game similar to Balatro, and the goal is to beat the last ante for the flag:

py

1   @renders
2    def round_ended(self):
3        assert not self.game_over
4        assert self.round_over
5        assert self.hand is not None
6        if self.current_ante == len(self.antes) - 1:
7            import os
8
9            # import sys
10            self.header += [
11                CardClass.joker_art,
12                choice([
13                    "You Aced it!",
14                    "You dealt with that pretty well!",
15                    "Looks like you weren't bluffing!",
16                    "Too bad these chips are all virtual...",
17                    "How the turn tables.",
18                    "Looks like I've taught you well!",
19                    "You made some heads up plays!",
20                    "Good thing I didn't bet against you!",
21                ]),
22            ]
23            self.message.append("GG")
24            with open("/flag", "r") as f:
25                flag = f.read()
26            self.message.append(flag)
27            import sys
28            print("FLAGGED", file=sys.stderr)
29            print("ante:", self.current_ante, file=sys.stderr)
30            print("score:", self.score, file=sys.stderr)
31            print("deck_len:", len(self.game_deck), file=sys.stderr)
32            list(map(lambda j: print(j.name, j.description, j.currently(self), file=sys.stderr), self.jokers))
33            self.game_over = True
34            return
35        self.game_deck += self.hand
36        self.game_deck += self.discard_pile
37        self.discard_pile.clear()
38        # ...

py

1antes = [
2    0,
3    100,
4    300,
5    800,
6    2000,
7    5000,
8    11000,
9    20000,
10    35000,
11    50000,
12    110000,
13    1560000,
14    17200000,
15    1300000000,
16    147000000000,
17    12.9e13,
18    17.7e16,
19    18.6e20,
20    14.2e25,
21    19.2e30,
22    19.2e36,
23    24.3e43,
24    29.7e50,
25    21.0e59,
26    25.8e67,
27    21.6e77,
28    22.4e87,
29    21.9e98,
30    28.4e109,
31    22.0e122,
32    22.7e135,
33    32.1e149,
34    39.9e163,
35    32.7e179,
36    34.4e195,
37    34.4e212,
38    32.8e230,
39    31.1e249,
40    32.7e268,
41    34.5e288,
42    34.8e309,
43]

But how do we score 34.8e309? In regular Balatro, scoring close to inf is only possible by stacking retriggers and xMult via for e.g. Perkeo + Observatory, Baron + Mime + steel cards + red seals, etc. Looking in cards.py, however, it's clear that many of these strategies are simply not implemented in this limited recreation of Balatro:

py

1from enum import Enum
2from itertools import chain
3
4cards_mode = "txt"
5cards_mode = "utf"
6
7
8class CardEnhancement(Enum):
9    NONE = 0
10    GOLD = 1
11    STONE = 2
12    STEEL = 3
13
14
15# ANSI escape codes for colors
16COLORS = {
17    "reset": "\033[0m",
18    "black": "\033[90m",
19    "red": "\033[91m",
20    "green": "\033[92m",
21    "yellow": "\033[93m",
22    "blue": "\033[94m",
23    "purple": "\033[95m",
24    "white_background": "\033[107m",
25}
26
27SUIT_COLORS = {
28    None: COLORS["reset"],
29    0: COLORS["red"],
30    1: COLORS["purple"],
31    2: COLORS["blue"],
32    3: COLORS["black"],
33}
34
35
36class Card:
37    suits = None
38    figures = None
39    ranks = None
40    ranks_sorting_map = {r: i for i, r in enumerate(
41        chain(range(2, 13+1), range(1, 1+1)),
42        start=2
43    )} | {None: 0}
44    is_joker = False
45
46    @classmethod
47    def clean_art(cls, art):
48        # Remove all ANSI color codes for rank/suit extraction
49        for color in COLORS.values():
50            art = art.replace(color, "")
51        return art
52
53    @classmethod
54    def color_art(cls, art, suit=None, rank=None, enhancement=CardEnhancement.NONE):
55        if enhancement == CardEnhancement.GOLD:
56            return f"{COLORS['yellow']}{art}{COLORS['reset']}"
57        elif enhancement == CardEnhancement.STONE:
58            return f"{COLORS['white_background']}{COLORS['black']}{art}🪨{COLORS['reset']}"
59        assert enhancement == CardEnhancement.NONE
60        return f"{SUIT_COLORS[suit]}{art}{COLORS['reset']}"
61
62    @classmethod
63    def art_factory(cls, suit, rank, enhancement=CardEnhancement.NONE):
64        raise NotImplementedError()
65
66    @classmethod
67    def rank_from_art(cls, art):
68        raise NotImplementedError()
69
70    @classmethod
71    def suit_from_art(cls, art):
72        raise NotImplementedError()
73
74    def __init__(self, suit, rank, chips_bonus=0, mult_bonus=1, enhancement=CardEnhancement.NONE):
75        self.rank = rank
76        self.sorting_rank = self.ranks_sorting_map[self.rank]
77        self.suit = suit
78        self.chips_bonus = chips_bonus
79        self.mult_bonus = mult_bonus
80        self.enhancement = enhancement
81        self.art = self.art_factory(suit, rank, enhancement)
82
83    def __str__(self) -> str:
84        return self.art
85
86    def __repr__(self) -> str:
87        return self.__str__()
88
89    def is_face_card(self):
90        return self.rank in [11, 12, 13]
91
92# ...
93
94if cards_mode == "utf":
95    CardClass = UTFCard
96elif cards_mode == "txt":
97    CardClass = TXTCard
98else:
99    raise NotImplementedError(f"rendering mode {cards_mode!r} not supported")
100
101cards = [CardClass(s, r) for s in range(4) for r in range(1, 13+1)]
102
103assert len(CardClass.suits) == 4, [CardClass.suits, len(CardClass.suits)]
104assert len(CardClass.figures) == 3, [CardClass.figures, len(CardClass.figures)]
105assert len(CardClass.ranks) == 13, [CardClass.ranks, len(CardClass.ranks)]
106assert len(cards) == len(CardClass.ranks) * \
107    len(CardClass.suits), [cards, len(cards)]
108
109
110def sort_hand(hand, reverse=False, suit_first=False, ace_after_king=True):
111    if suit_first:
112        return sorted(hand, reverse=reverse, key=lambda c: (c.suit, c.sorting_rank if ace_after_king else c.rank))
113    else:
114        return sorted(hand, reverse=reverse, key=lambda c: (c.sorting_rank if ace_after_king else c.rank, c.suit))

Indeed, we only have two "real" card enhancements: Gold and Stone (steel cards are unimplemented, and if one somehow gets created the server simply crashes). Furthermore, tarot cards, spectral cards, planets, and seals are all missing.

(As a side note, steel cards ended up being quite a bit of a red herring, as the server also defines a custom non-Balatro joker meant to create steel cards which also crashes the game if it was ever triggered; luckily, this joker was removed from the joker map and couldn't appear in a shop, unlike some other perpetrators...)

py

1from cards import CardEnhancement
2
3from .joker_base import JokerClass, JokerRarity
4
5
6class SteelMask(JokerClass):
7    description = "All face cards become Steel cards when played"
8    rarity = JokerRarity.UNCOMMON
9    price = 7
10
11    def on_card_scoring(self, state, i, card, ignore_jokers=None):
12        if state.is_face_card(card):
13            raise NotImplementedError()  # TODO

Note also that scoring is tied to your money (or more precisely, buying from the shop simply subtracts from your score), and score carries over between rounds. As opposed to normal Balatro, this allows us to pretty much buy anything we want from the shop, as hand scores are in the O(100) even on the first round, and rerolls cost a static $5 with jokers costing < $10.

py

1    @renders
2    def buy_jokers(self):
3        assert self.jokers_in_shop is not None
4        assert self.jokers_selected_in_shop is not None
5        if len(self.jokers_selected_in_shop) > self.max_joker_slots - len(self.jokers):
6            self.message.append(f"You can't have more than {self.max_joker_slots} jokers.")
7            return
8        price = 0
9        for joker_class in self.jokers_selected_in_shop:
10            price += joker_class.price
11        if price > self.score:
12            self.message.append(f"You don't have enough money to buy the selected jokers (${price!r}).")
13            return
14        while len(self.jokers_selected_in_shop) > 0:
15            joker = self.jokers_selected_in_shop.pop(0)
16            self.score -= joker.price
17            self.jokers.append(joker)
18            self.jokers_in_shop.remove(joker)
19            for joker in self.jokers:
20                joker.on_joker_buy(self, joker)
21        self.check_game_over()

But even so, how do we even score close to e309? The key idea is this: jokers that provide retriggers are implemented in the game by re-calling the method on state that runs a specific phase of scoring.

py

1from .joker_base import JokerClass, JokerRarity
2
3
4class HangingChad(JokerClass):
5    description = "Retrigger first played card used in scoring 2 additional times"
6    rarity = JokerRarity.COMMON
7    price = 4
8
9    def on_card_scoring(self, state, i, card, ignore_jokers=None):
10        if i == 0:
11            # Retrigger 2 additional times
12            for _ in range(2):
13                state.message.append(f"{self.name}: retrigger card")
14                if ignore_jokers is None:
15                    ignore_jokers = set()
16                ignore_jokers.add(self)
17                state.compute_card_score(i, card, ignore_jokers=ignore_jokers)
18                ignore_jokers.discard(self)

(here, the Hanging Chad calls state.compute_card_score() again, which as a side effect re-triggers all jokers' on_card_scoring() callbacks). To avoid infinitely looping with themselves, jokers like the Hanging Chad add themselves to an ignore_jokers list, for which all jokers on the list are skipped in the rerun scoring phase.

Enter the Blueprint:

py

1from copy import deepcopy
2
3from .joker_base import JokerClass, JokerRarity
4
5
6class Blueprint(JokerClass):
7    description = "Copies ability of Joker to the right"
8    rarity = JokerRarity.RARE
9    price = 10
10
11    def currently(self, state):
12        if self.copied_joker is None:
13            return "(none)"
14        return f"(currently: {self.copied_joker.name})"
15
16    def __init__(self):
17        super().__init__()
18        self.copied_joker = None
19
20    def find_joker_to_copy(self, state):
21        self.copied_joker = None
22        found = False
23        for joker in state.jokers:
24            if found:
25                self.copied_joker = deepcopy(joker)
26                break
27            if joker is self:
28                found = True
29                continue
30
31    def on_joker_buy(self, state, joker):
32        self.find_joker_to_copy(state)
33        if self.copied_joker is not None:
34            self.copied_joker.on_joker_buy(state, joker)
35
36    def on_joker_sell(self, state, joker):
37        self.find_joker_to_copy(state)
38        if self.copied_joker is not None:
39            self.copied_joker.on_joker_sell(state, joker)
40
41    def on_card_scoring(self, state, i, card, ignore_jokers=None):
42        if self.copied_joker is not None:
43            self.copied_joker.on_card_scoring(
44                state, i, card, ignore_jokers=ignore_jokers)
45
46    def on_hand_scoring(self, state, ignore_jokers=None):
47        if self.copied_joker is not None:
48            self.copied_joker.on_hand_scoring(
49                state, ignore_jokers=ignore_jokers)
50
51    def on_in_hand_card_scoring(self, state, i, card, ignore_jokers=None):
52        if self.copied_joker is not None:
53            self.copied_joker.on_in_hand_card_scoring(
54                state, i, card, ignore_jokers=ignore_jokers)
55
56    def on_in_hand_scoring(self, state, ignore_jokers=None):
57        if self.copied_joker is not None:
58            self.copied_joker.on_in_hand_scoring(
59                state, ignore_jokers=ignore_jokers)
60
61    def on_round_start(self, state):
62        if self.copied_joker is not None:
63            self.copied_joker.on_round_start(state)
64
65    def on_round_end(self, state):
66        if self.copied_joker is not None:
67            self.copied_joker.on_round_end(state)
68
69    def on_discard(self, state):
70        if self.copied_joker is not None:
71            self.copied_joker.on_discard(state)
72
73    def on_play(self, state):
74        if self.copied_joker is not None:
75            self.copied_joker.on_play(state)
76
77    def on_hand_updated(self, state):
78        if self.copied_joker is not None:
79            self.copied_joker.on_hand_updated(state)
80
81    def on_card_added_to_deck(self, state, card):
82        if self.copied_joker is not None:
83            self.copied_joker.on_card_added_to_deck(state, card)
84
85    def on_card_removed_from_deck(self, state, card):
86        if self.copied_joker is not None:
87            self.copied_joker.on_card_removed_from_deck(state, card)
88
89    def overrides_is_face_card(self, state, card):
90        if self.copied_joker is not None:
91            return self.copied_joker.overrides_is_face_card(state, card)
92        return super().overrides_is_face_card(state, card)

Namely, Blueprint "copies" all the callbacks of self.copied_joker by simply passing down all arguments to self.copied_joker's callbacks. But in the ignore_joker case, if we e.g. have Blueprint copying Hanging Chad,

  • On card score, we will call blueprint.on_card_scoring()
  • Blueprint calls blueprint.copied_joker.on_card_scoring() (where copied joker is Hanging Chad)
  • The copied callback adds blueprint.copied_joker to the ignore_jokers list and re-triggers state.compute_card_score()
  • Since Blueprint is not in the ignore_jokers list, we call blueprint.on_card_scoring()
  • Blueprint calls blueprint.copied_joker.on_card_scoring()
  • ...

for infinite recursion. As a remark, note that the joker order matters: any jokers to the left of the Blueprint will have their on_card_scoring() callbacks execute before Blueprint's, allowing them to actually happen before Blueprint triggers another recursion. Since there is no mechanism for reordering jokers, we need to be careful about the order we buy jokers in.

So how do we leverage this for high scoring? In the Blueprint + Hanging Chad case, we can essentially infinitely trigger any joker's .on_card_scoring() callback (until a stack overflow exception is thrown, which the game simply quietly catches and moves on to the next round):

py

1    def compute_score(self):
2        self.hand_chips, self.hand_mult = 0, 1
3        # print("initial scoring:    ", self.hand_chips, self.hand_mult)
4        try:  # fail safe on unimplemented jokers
5            self.compute_hand_score()
6            # print("hand_value scoring: ", self.hand_chips, self.hand_mult)
7            self.compute_cards_score()
8            # print("cards_value scoring:", self.hand_chips, self.hand_mult)
9            self.compute_in_hand_cards_score()
10            # print("in_hand_cards_value scoring:", self.hand_chips, self.hand_mult)
11        except Exception as e:
12            print(repr(e), file=stderr)

For .on_card_scoring(), the best joker to retrigger is Photograph, giving us x2 mult per loop:

py

1from .joker_base import JokerClass, JokerRarity
2
3
4class Photograph(JokerClass):
5    description = "First played face card gives X2 Mult when scored"
6    rarity = JokerRarity.COMMON
7    price = 5
8
9    def __init__(self):
10        super().__init__()
11        self.is_first_face = False
12
13    def on_hand_scoring(self, state, ignore_jokers=None):
14        self.is_first_face = True
15
16    def on_card_scoring(self, state, i, card, ignore_jokers=None):
17        if state.is_face_card(card):
18            state.message.append(f"{self.name}: X2 Mult for first face card")
19            state.hand_mult *= 2
20            self.is_first_face = False
21
22    def on_play(self, state):
23        self.is_first_face = False

(since their Photograph implementation is bugged, and never checks self.is_first_face for whether to apply the x2 mult on subsequent face card scorings).

However, even with 3 Photographs (their shop deduplication is broken) + Blueprint + Hanging Chad, due to the maximum recursion depth of 1000 we can only get a score of ~e301.

Instead, we can pivot to another ignore_jokers joker: Mime, which retriggers state.compute_in_hand_cards_score().

py

1from .joker_base import JokerClass, JokerRarity
2
3
4class Mime(JokerClass):
5    description = "Retrigger all card held in hand abilities"
6    rarity = JokerRarity.UNCOMMON
7    price = 5
8
9    def on_in_hand_scoring(self, state, ignore_jokers=None):
10        state.message.append(f"{self.name}: retrigger hand cards")
11        if ignore_jokers is None:
12            ignore_jokers = set()
13        ignore_jokers.add(self)
14        state.compute_in_hand_cards_score(ignore_jokers=ignore_jokers)
15        ignore_jokers.discard(self)

Notably, .compute_in_hands_score() retriggers each card in the hand before recursing by triggering jokers, allowing us to get more than x2 mult per recursion:

py

1    def compute_in_hand_cards_score(self, ignore_jokers=None):
2        assert self.hand is not None
3        assert self.selected is not None
4        assert self.hand_chips is not None
5        assert self.hand_mult is not None
6        if ignore_jokers is None:
7            ignore_jokers = set()
8        for i, card in enumerate(self.hand):
9            if card not in self.selected:
10                self.compute_in_hand_card_score(
11                    i, card, ignore_jokers=ignore_jokers)
12        for joker in self.jokers:
13            if joker not in ignore_jokers:
14                joker.on_in_hand_scoring(self, ignore_jokers=ignore_jokers)

Thus, all we need to do is pair this with a Baron. We can see that with just 2 barons, we get up to ~x11 mult per loop with just 3 kings in hand:

py

1from .joker_base import JokerClass, JokerRarity
2
3
4class Baron(JokerClass):
5    description = "Each King held in hand gives X1.5 Mult"
6    rarity = JokerRarity.RARE
7    price = 8
8
9    def on_in_hand_card_scoring(self, state, i, card, ignore_jokers=None):
10        # King has rank 13
11        if card.rank == 13:
12            state.message.append(f"{self.name}: X1.5 Mult for King")
13            state.hand_mult *= 1.5

Code

11.5^2     = 2.25       (K=1)
2(1.5^2)^2 = 5.0625     (K=2)
3(1.5^2)^3 = 11.390625  (K=3)

So the plan of attack is as follows:

  1. Play the first round randomly. (if we play 5 cards at a time, we're almost guaranteed to have enough money by the end of round)
  2. Reroll until we get the following jokers, in order: [Baron, Baron, Blueprint, Mime]
  3. On the next round, discard for 3 kings and play any hand; the infinite recursion should trigger, giving you inf score.
  4. Play out the rest of the antes until you win and get the flag.

and all thats left is scripting. For this challenge, though, even scripting is non-trivial and mildly annoying due to their game interface and PoW. Even funnier, due to other bugs in their implementation, the moment you try to select an Erosion in the shop the server immediately crashes; since you can't tell which jokers are which before attempting to select them, this is a completely random element that you just need to get lucky enough to avoid.

After some lengthy trial and error, we can create a script like so:

py

1import subprocess
2
3import pwn
4
5# queue = ['Photograph', 'Photograph', 'Photograph', 'Blueprint', 'Hanging Chad']
6queue = ['Baron', 'Baron', 'Blueprint', 'Mime']
7
8
9def skip_shop():
10    conn.recvuntil(b'\n $')
11    money = conn.recvline()
12    print(f'shop: ${money.decode().strip()}')
13    conn.sendline(b'p')
14
15
16def handle_shop():
17    conn.recvuntil(b'\n $')
18    money = conn.recvline()
19    print(f'shop: ${money.decode().strip()}')
20
21    # Query both jokers, reroll until we get all the jokers we want
22    while True:
23        b = query_shop_joker('1')
24        query_shop_joker('2' if not b else '1')
25
26        if len(queue) == 0:
27            break
28        conn.sendline(b'r')
29
30        conn.recvuntil(b'\n $')
31        money = int(conn.recvline())
32        print(f'reroll: ${money}')
33
34        if money < 100:
35            break
36
37    conn.recvuntil(b'\n>')
38    conn.sendline(b'p')
39
40
41def query_shop_joker(index: str):
42    if len(queue) == 0:
43        return False
44
45    conn.recvuntil(b'\n>')
46    conn.sendline(index.encode())
47    conn.recvuntil(f'{index}: '.encode())
48
49    line = conn.recvline().decode()
50    name = line.partition(':')[0]
51
52    print('->', name)
53
54    want = queue[0]
55
56    if want == name:
57        queue.pop(0)
58        conn.sendline(b'b')
59        print(f'purchased {name}')
60        return True
61    else:
62        # Deselect the joker
63        conn.sendline(index.encode())
64        return False
65
66
67def play_game():
68    for _ in range(5):
69        conn.recvuntil(b'\n>')
70        conn.sendline(b'a 1 2 3 4 5')
71        conn.recvuntil(b'\n>')
72        conn.sendline(b'p')
73
74
75def play_game_kings():
76    for _ in range(5):
77        hand = conn.recvuntil(b'\n 1').decode().split('\n')[-2]
78        kings = hand.count('🂮') + hand.count('🂾') + hand.count('🃎') + hand.count('🃞')
79        aces = hand.count('🂡') + hand.count('🂱') + hand.count('🃁') + hand.count('🃑')
80        conn.recvuntil(b'\n>')
81
82        print(hand, kings, aces)
83        if kings >= 3:
84            break
85
86        # Find the indices to discard; we probably want to discard on the right first
87        # since we play cards from the left.
88        remove = list(range(1, 9))[-aces:] + list(range(1, 9))[:8 - kings - aces]
89        remove = remove[:5]
90
91        conn.sendline(f'a {" ".join(map(str, remove))}'.encode())
92        conn.recvuntil(b'\n>')
93        conn.sendline(b'd')
94    else:
95        print('not able to discard :(')
96
97    for _ in range(5):
98        conn.sendline(b'a 1')
99        conn.recvuntil(b'\n>').decode()
100        conn.sendline(b'p')
101
102
103conn = pwn.remote('precipice.challs.m0lecon.it', 14615)
104
105# Do PoW :)
106cmd = conn.recvuntil(b'Result: ').decode().split('\n')[3]
107res = subprocess.run(cmd, capture_output=True, shell=True).stdout.decode()
108conn.sendline(res.encode())
109
110skip_shop()
111
112while len(queue) > 0:
113    play_game()
114    handle_shop()
115
116while True:
117    play_game_kings()
118
119    if conn.recvuntil(b'GG', timeout=0.5) != b'':
120        print(conn.recvall().decode())
121        break
122
123    skip_shop()

which we can run repeatedly (praying it never hits an Erosion)

bash

1reroll: $226
2-> Riff Raff
3-> Gros Michel
4reroll: $221
5-> Gros Michel
6-> Baron
7reroll: $216
8-> Stone Joker
9Traceback (most recent call last):
10  File "/mnt/c/Users/kevin/Downloads/ctf/balatro.py", line 112, in <module>
11    handle_shop()
12  File "/mnt/c/Users/kevin/Downloads/ctf/balatro.py", line 23, in handle_shop
13    query_shop_joker('2' if not b else '1')
14  File "/mnt/c/Users/kevin/Downloads/ctf/balatro.py", line 46, in query_shop_joker
15    conn.recvuntil(f'{index}: '.encode())
16  File "/home/ky28059/.local/lib/python3.12/site-packages/pwnlib/tubes/tube.py", line 381, in recvuntil
17    res = self.recv(timeout=self.timeout)
18          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
19  File "/home/ky28059/.local/lib/python3.12/site-packages/pwnlib/tubes/tube.py", line 146, in recv
20    return self._recv(numb, timeout) or b''
21           ^^^^^^^^^^^^^^^^^^^^^^^^^
22  File "/home/ky28059/.local/lib/python3.12/site-packages/pwnlib/tubes/tube.py", line 216, in _recv
23    if not self.buffer and not self._fillbuffer(timeout):
24                               ^^^^^^^^^^^^^^^^^^^^^^^^^
25  File "/home/ky28059/.local/lib/python3.12/site-packages/pwnlib/tubes/tube.py", line 195, in _fillbuffer
26    data = self.recv_raw(self.buffer.get_fill_size())
27           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28  File "/home/ky28059/.local/lib/python3.12/site-packages/pwnlib/tubes/sock.py", line 56, in recv_raw
29    raise EOFError
30EOFError
31[*] Closed connection to precipice.challs.m0lecon.it port 14615

until finally, we win:

bash

1[+] Opening connection to precipice.challs.m0lecon.it on port 14615: Done
2shop: $0.0
3shop: $366
4-> Brainstorm
5-> Burnt Joker
6reroll: $366
7-> Dusk
8-> Hanging Chad
9reroll: $361
10-> Cavendish
11-> Blueprint
12reroll: $356
13-> Flower Pot
14-> Acrobat
15reroll: $351
16-> Hiker
17-> Midas Mask
18reroll: $346
19-> Certificate
20-> Ancient Joker
21reroll: $341
22-> Banner
23-> Sock And Buskin
24reroll: $336
25-> Ramen
26-> Brainstorm
27reroll: $331
28-> Misprint
29-> Banner
30reroll: $326
31-> Scary Face
32-> Sock And Buskin
33reroll: $321
34-> Vampire
35-> Joker
36reroll: $316
37-> Abstract Joker
38-> Midas Mask
39reroll: $311
40-> Abstract Joker
41-> Ramen
42reroll: $306
43-> Mime
44-> Smiley Face
45reroll: $301
46-> Baron
47purchased Baron
48-> Turtle Bean
49reroll: $288
50-> Drunkard
51-> Hologram
52reroll: $283
53-> Baron
54purchased Baron
55-> Burnt Joker
56reroll: $270
57-> Certificate
58-> Sock And Buskin
59reroll: $265
60-> Brainstorm
61-> Joker
62reroll: $260
63-> Stone Joker
64-> Joker Stencil
65reroll: $255
66-> Hiker
67-> Hiker
68reroll: $250
69-> Joker Stencil
70-> Hiker
71reroll: $245
72-> Loyalty Card
73-> Golden Ticket
74reroll: $240
75-> Gros Michel
76-> Certificate
77reroll: $235
78-> Blueprint
79purchased Blueprint
80-> Midas Mask
81reroll: $220
82-> Drunkard
83-> Gros Michel
84reroll: $215
85-> Blue Joker
86-> Ramen
87reroll: $210
88-> Hologram
89-> Dusk
90reroll: $205
91-> Shoot The Moon
92-> Gros Michel
93reroll: $200
94-> Blue Joker
95-> Abstract Joker
96reroll: $195
97-> Raised Fist
98-> Loyalty Card
99reroll: $190
100-> Baron
101-> Dusk
102reroll: $185
103-> Sock And Buskin
104-> Mime
105purchased Mime
106 🂢 🃈 🂹 🃋 🂾 🃁 🃑 🂡 1 3
107 🂴 🂵 🂷 🂧 🂹 🃋 🂽 🂾 1 0
108 🃄 🂤 🃗 🂩 🃋 🂽 🂾 🃞 2 0
109 🂳 🃕 🃖 🃇 🂻 🂽 🂾 🃞 2 0
110 🃂 🃔 🃉 🂪 🂽 🂾 🃞 🂱 2 1
111not able to discard :(
112shop: $inf
113 🃏 🃏 0 0
114   🂹 🃉 🂩 🂻 🂽 🃞 🂱 1 1
115 🃅 🃗 🂫 🂽 🂭 🃞 🂱   1 1
116 🂳 🃄 🃇 🃙 🃊 🂭 🃞   1 0
117   🃔 🃕 🃊 🃚 🃋 🂭 🃞 1 0
118not able to discard :(
119shop: $inf
120 🃏 🃏 0 0
121   🃕 🂨 🂹 🃉 🂻 🃋 🃝 0 0
122   🃖 🂻 🃋 🃛 🃝 🂾 🃁 1 1
123 🃔 🂶 🃇 🃈 🃝 🂾 🃁   1 1
124 🃂 🂥 🃗 🃘 🃝 🂾 🃁   1 1
125not able to discard :(
126shop: $inf
127 🃏 🃏 0 0
128   🂢 🂣 🃄 🃕 🂶 🃙 🃑 0 1
129 🂴 🂶 🃆 🃇 🃙 🃛 🃑   0 1
130 🃓 🂵 🃙 🃋 🃛 🃝 🃞   1 0
131   🂧 🃛 🂫 🃝 🂭 🂾 🃞 2 0
132not able to discard :(
133shop: $inf
134 🃏 🃏 0 0
135   🂴 🂷 🃈 🂪 🃝 🂾 🃑 1 1
136 🂵 🂺 🂫 🃝 🂾 🂮 🂱   2 1
137 🂢 🃘 🃍 🂭 🂾 🂮 🂱   2 1
138 🂤 🃖 🂧 🃙 🂻 🂾 🂮   2 0
139not able to discard :(
140shop: $inf
141 🃏 🃏 0 0
142   🃇 🂨 🃊 🃝 🂭 🃎 🃞 2 0
143   🂦 🂸 🂻 🂭 🃎 🃞 🃑 2 1
144 🃂 🂥 🃆 🂧 🂹 🃎 🃞   2 0
145   🂶 🂷 🂹 🃚 🃎 🃞 🂡 2 1
146not able to discard :(
147shop: $inf
148 🃏 🃏 0 0
149   🂣 🃕 🂥 🃆 🂦 🃚 🂮 1 0
150   🂧 🃚 🂪 🂻 🃞 🂮 🂡 2 1
151 🂷 🃈 🃘 🃛 🂽 🃞 🂮   2 0
152   🂽 🃍 🂭 🃎 🃞 🂮 🃑 3 1
153shop: $inf
154 🃏 🃏 0 0
155   🃂 🂢 🃃 🂵 🂥 🃈 🂩 0 0
156   🂥 🃈 🂩 🃚 🃝 🃞 🃑 1 1
157 🂴 🃔 🂦 🃗 🃘 🃝 🃞   1 0
158   🃆 🃘 🃋 🃛 🃝 🂭 🃞 1 0
159not able to discard :(
160shop: $inf
161 🃏 🃏 0 0
162   🂢 🃅 🃆 🃈 🂹 🂩 🂽 0 0
163   🃄 🃔 🂦 🂹 🂩 🂽 🃍 0 0
164   🃇 🂩 🂪 🂽 🃍 🂾 🃑 1 1
165 🃒 🂥 🂧 🃊 🃍 🂾 🂮   2 0
166not able to discard :(
167shop: $inf
168 🃏 🃏 0 0
169   🂳 🃄 🂨 🃊 🃚 🂪 🃎 1 0
170   🃇 🃗 🃚 🂪 🃛 🂫 🃎 1 0
171   🃔 🂧 🃈 🃛 🂫 🂽 🃎 1 0
172   🃕 🂥 🂫 🂽 🃍 🂭 🃎 1 0
173not able to discard :(
174shop: $inf
175 🃏 🃏 0 0
176   🃗 🂸 🃉 🃝 🃎 🂮 🃁 2 1
177 🃔 🂦 🂫 🃎 🃞 🂮 🃁   3 1
178shop: $inf
179 🃏 🃏 0 0
180   🂦 🃇 🃉 🂻 🃋 🂱 🃑 0 2
181 🃒 🃃 🃋 🃛 🂽 🂾   🃑 1 1
182 🂲 🂳 🂷 🂨 🂺 🃛 🂽   0 0
183   🃔 🃗 🂧 🂺 🂪 🃛 🂽 0 0
184not able to discard :(
185shop: $inf
186 🃏 🃏 0 0
187   🃇 🂸 🃚 🂪 🃍 🃞 🃁 1 1
188 🃅 🂧 🃊 🃛 🃍 🃞 🂱   1 1
189 🂲 🂣 🃍 🃝 🂭 🃞 🂱   1 1
190 🃒 🂥 🂶 🃖 🂭 🃞 🂱   1 1
191not able to discard :(
192shop: $inf
193 🃏 🃏 0 0
194   🂥 🃆 🂷 🃇 🂪 🃛 🃑 0 1
195 🂴 🃖 🃘 🃙 🂪 🃛 🂽   0 0
196   🂧 🂺 🂪 🃛 🂽 🂱 🂡 0 2
197 🃃 🃄 🃕 🃋 🂽 🃞   🂡 1 1
198not able to discard :(
199shop: $inf
200 🃏 🃏 0 0
201   🂣 🃄 🃔 🃆 🃇 🂧 🃝 0 0
202   🃇 🂧 🂸 🂨 🂺 🃋 🃝 0 0
203   🂵 🂦 🂺 🃋 🃝 🂾 🃞 2 0
204   🃙 🂽 🃍 🃝 🂾 🃞 🂮 3 0
205shop: $inf
206 🃏 🃏 0 0
207   🃗 🂸 🃈 🃉 🃋 🂭 🃎 1 0
208   🃓 🃕 🂨 🃋 🂭 🂾 🃎 2 0
209   🂶 🂹 🃍 🂭 🂾 🃎 🃁 2 1
210 🂳 🃅 🃚 🃝 🂾 🃎 🃁   2 1
211not able to discard :(
212shop: $inf
213 🃏 🃏 0 0
214   🃒 🂣 🃅 🃕 🂦 🃉 🂱 0 1
215 🂴 🂶 🂦 🃉 🃊 🃍 🃝   0 0
216   🃇 🂸 🂺 🃊 🃍 🃝 🂡 0 1
217 🂢 🃖 🃋 🂽 🃍 🃝 🂮   1 0
218not able to discard :(
219shop: $inf
220 🃏 🃏 0 0
221   🃓 🂵 🂷 🃈 🂻 🃋 🃎 1 0
222   🃆 🂨 🂻 🃋 🃛 🃝 🃎 1 0
223   🂺 🃚 🃛 🂽 🃝 🂭 🃎 1 0
224   🃔 🃗 🂫 🃝 🂭 🃎 🃁 1 1
225not able to discard :(
226shop: $inf
227 🃏 🃏 0 0
228   🂳 🃔 🃕 🃗 🂹 🂮 🂡 1 1
229 🃓 🃅 🃖 🂷 🂹 🂮 🃑   1 1
230 🃆 🃇 🂧 🂹 🂫 🂮 🂱   1 1
231 🃉 🃚 🂻 🃛 🂫 🃎 🂮   2 0
232not able to discard :(
233shop: $inf
234 🃏 🃏 0 0
235   🂦 🃇 🂧 🃘 🃝 🃞 🂡 1 1
236 🃓 🃕 🂸 🃈 🃝 🃎 🃞   2 0
237   🃉 🃋 🃝 🃎 🃞 🂱 🃑 2 2
238 🂲 🂢 🂹 🂪 🂭 🃞   🃑 1 1
239not able to discard :(
240shop: $inf
241 🃏 🃏 0 0
242   🂨 🃉 🃊 🂪 🂾 🃞 🂱 2 1
243 🃄 🃔 🂤 🂧 🂸 🂾 🃞   2 0
244   🃕 🂷 🂸 🃛 🂭 🂾 🃞 2 0
245   🂢 🃃 🂣 🃙 🂭 🂾 🃞 2 0
246not able to discard :(
247shop: $inf
248 🃏 🃏 0 0
249   🃒 🃓 🂴 🃇 🂩 🂪 🃝 0 0
250   🂷 🂩 🂪 🂻 🃝 🃎 🂱 1 1
251 🂥 🂨 🂫 🂽 🃝 🃎 🂱   1 1
252 🃖 🂧 🃍 🃝 🂾 🃎 🂱   2 1
253not able to discard :(
254shop: $inf
255 🃏 🃏 0 0
256   🂣 🃔 🂤 🂶 🂸 🃋 🃎 1 0
257   🃅 🂸 🂩 🃋 🂫 🂾 🃎 2 0
258   🂢 🂹 🃊 🂫 🂾 🃎 🂮 3 0
259shop: $inf
260 🃏 🃏 0 0
261   🂢 🂤 🂶 🃆 🂷 🃚 🂭 0 0
262   🂷 🂸 🃘 🃚 🂭 🃑 🂡 0 2
263 🂵 🂦 🂧 🂽 🂭 🃁   🂡 0 2
264 🂣 🃉 🂽 🃍 🃝 🂭   🃁 0 1
265not able to discard :(
266shop: $inf
267 🃏 🃏 0 0
268   🂣 🃄 🂵 🃆 🂸 🂪 🂭 0 0
269   🂳 🂤 🃅 🂸 🂪 🃛 🂭 0 0
270   🃇 🃗 🃈 🂪 🃛 🂭 🃎 1 0
271   🂶 🂧 🃋 🃛 🂭 🃎 🂡 1 1
272not able to discard :(
273shop: $inf
274 🃏 🃏 0 0
275   🂴 🃅 🂸 🃘 🃙 🃚 🃛 0 0
276   🂧 🃙 🃚 🃛 🂽 🂾 🃞 2 0
277   🂹 🂽 🂾 🃎 🃞 🂮 🂱 4 1
278shop: $inf
279 🃏 🃏 0 0
280   🃔 🂵 🂻 🃛 🃝 🂾 🃁 1 1
281 🃒 🂢 🂶 🂧 🃋 🃝 🂾   1 0
282   🃉 🃙 🂺 🃋 🃝 🂭 🂾 1 0
283   🃄 🃖 🂦 🂨 🃝 🂭 🂾 1 0
284not able to discard :(
285shop: $inf
286 🃏 🃏 0 0
287   🂴 🂵 🃅 🂸 🂩 🂪 🃋 0 0
288   🃓 🂣 🃈 🂩 🃊 🂪 🃋 0 0
289   🃕 🃆 🃊 🂪 🃋 🂭 🂾 1 0
290   🂶 🂷 🃙 🃋 🂭 🂾 🃎 2 0
291not able to discard :(
292shop: $inf
293 🃏 🃏 0 0
294   🂢 🃔 🃕 🂧 🃈 🂩 🂫 0 0
295   🃈 🂩 🂫 🂽 🂾 🃞 🂮 3 0
296shop: $inf
297 🃏 🃏 0 0
298   🂴 🃅 🂥 🂦 🂷 🂨 🃍 0 0
299   🂷 🂨 🃉 🃙 🃍 🃝 🃎 1 0
300   🃍 🃝 🂭 🂾 🃎 🃑 🂡 2 2
301 🃔 🂵 🂶 🃖 🃎 🂮   🂡 2 1
302not able to discard :(
303shop: $inf
304 🃏 🃏 0 0
305   🂤 🂥 🂶 🂷 🃇 🃉 🃁 0 1
306 🃇 🃗 🂸 🂹 🃉 🂩 🃚   0 0
307   🃔 🃅 🃖 🃉 🂩 🃚 🃎 1 0
308   🃈 🂩 🃚 🂪 🃋 🃝 🃎 1 0
309not able to discard :(
310shop: $inf
311 🃏 🃏 0 0
312   🃅 🃆 🃖 🃈 🃋 🃍 🂮 1 0
313   🂤 🃗 🂧 🂪 🃋 🃍 🂮 1 0
314   🃘 🃙 🂻 🃋 🃍 🃞 🂮 2 0
315   🂷 🃇 🃍 🂭 🃎 🃞 🂮 3 0
316shop: $inf
317 🃏 🃏 0 0
318   🃔 🃕 🃗 🃈 🂹 🂫 🃝 0 0
319   🂹 🃊 🂻 🂫 🃍 🃝 🃎 1 0
320   🂢 🃓 🃖 🃇 🃍 🃝 🃎 1 0
321   🂸 🂽 🃍 🃝 🂭 🃎 🃞 2 0
322not able to discard :(
323shop: $inf
324 🃏 🃏 0 0
325   🂴 🂷 🃘 🂽 🂭 🂾 🂱 1 1
326 🃅 🃇 🃗 🃉 🂭 🂾 🃞   2 0
327   🂵 🂥 🂩 🂪 🂭 🂾 🃞 2 0
328   🃕 🂨 🃛 🂭 🂾 🃞 🂡 2 1
329not able to discard :(
330shop: $inf
331 🃏 🃏 0 0
332   🃔 🃙 🃚 🃍 🂾 🂮 🃁 2 1
333 🂧 🃘 🂹 🃊 🂾 🂮 🂱   2 1
334 🃕 🃆 🂽 🂾 🃎 🂮 🂱   3 1
335shop: $inf
336 🃏 🃏 0 0
337   🃖 🃘 🂪 🂻 🃛 🂫 🃎 1 0
338   🃆 🂸 🃈 🂩 🃛 🂫 🃎 1 0
339   🃓 🂺 🃛 🂫 🂾 🃎 🂮 3 0
340shop: $inf
341 🃏 🃏 0 0
342   🃄 🃔 🃘 🂪 🂻 🂫 🂮 1 0
343   🂣 🂷 🂻 🂫 🃝 🂮 🂱 1 1
344 🃓 🃖 🂸 🃊 🃝 🂮 🂱   1 1
345 🂲 🃗 🂨 🃙 🃝 🃞 🂮   2 0
346not able to discard :(
347shop: $inf
348 🃏 🃏 0 0
349   🂴 🃔 🂸 🃈 🃙 🂭 🃞 1 0
350   🂤 🂥 🃙 🂺 🂻 🂭 🃞 1 0
351   🂨 🃚 🂻 🃝 🂭 🃞 🂱 1 1
352 🂲 🃂 🃓 🃖 🂹 🂭 🃞   1 0
353not able to discard :(
354shop: $inf
355 🃏 🃏 0 0
356   🃖 🃇 🃈 🂩 🂪 🂱 🃑 0 2
357 🃒 🃃 🂣 🂪 🂻 🂭   🃑 0 1
358 🂲 🂵 🂥 🃗 🂪 🂻 🂭   0 0
359   🂶 🃉 🂺 🃊 🂪 🂻 🂭 0 0
360not able to discard :(
361shop: $inf
362 🃏 🃏 0 0
363   🃃 🂴 🂶 🃙 🃚 🂽 🂾 1 0
364   🃔 🃉 🃚 🃋 🂽 🂾 🂱 1 1
365 🃒 🃅 🂦 🃛 🂽 🃝 🂾   1 0
366   🂵 🂸 🂨 🂺 🂽 🃝 🂾 1 0
367not able to discard :(
368[+] Receiving all data: Done (116B)
369[*] Closed connection to precipice.challs.m0lecon.it port 14615
370
371 ptm{7urns_0u7_7h3r3_w@s_p@p3r0nis_m0n3y_p00l_@7_7h3_b0770m_0f_7h3_pr3cipic3}
372\x1b[2J
373 ~$inf
374 $3.308591879916669e+233