Messo di fronte alla scelta tra ripetere un’operazione banale (e perciò noiosa) e l’automatizzarla, non ho mai avuto dubbi: si deve automatizzare.
Non importa se, tirando le somme, il tempo che avrei impiegato a svolgere l’attività manualmente è di gran lunga inferiore a quello che ho sprecato per creare lo script che la svolga al posto mio.
Questa abitudine, che spesso si manifesta in maniera piuttosto innocua con la scrittura di regex per modificare rapidamente il contenuto di un file, si è fatta strata, piano piano, anche nel mio approccio con i videogiochi.
Il primo sentore è stata l’automatizzazione di alcuni puzzle in Zero Escape: Virtue’s Last Reward, risolti tramite SMT solver.
L’ultima impresa, che mi sono deciso a condividere in quanto potrebbe essere utile a qualcun altro, è stata la creazione di uno script per completare un achievement in Balatro.
Giusto per dare un minimo di contesto: uno degli achievements del gioco, Completionist, si ottiene sbloccando almeno una volta tutti i joker, i coupon e, in generale, tutti gli oggetti che si possono trovare durante le partite.
Il numero di questi ultimi, per quanto superi le centinaia, non è così elevato da rendere l’impresa impossibile, se non fosse per il fatto che, come ogni buon roguelike che si rispetti, ottenere ciò che si cerca è fortemente legata alla fortuna.
Dopo aver passato ore ed ore ad inseguire questo obiettivo, mi sono ridotto ad aver completato l’intera collezione con l’eccezione di un solo joker leggendario, Yorick.
Nonostante la mia strategia eccellente, che si riduceva semplicemente a giocare il deck Anaglyph e bruciare tutti i double tag accumulati fra le ante sugli skip che permettono di aprire un pacchetto arcano, il joker in questione non si è mai fatto vedere.
Essendomi stancato di tentare la sorte, su suggerimento del buon Corrado ho provato una strada alternativa: continuare a riavviare la partita sperando di trovare lo skip del pacchetto arcano in una delle prime due blind, controllare se tale pacchetto contenesse la carta spettrale tanto ambita e resettare immediatamente nel caso non avessi avuto fortuna.
E di fortuna, in effetti, ne sarebbe servita parecchia.
Script Python
Dopo circa 5 minuti di tentativi manuali fallimentari e senza nessuna speranza all’orizzonte, ho deciso di scrivere uno script per automatizzare il tutto.
Il risultato è stato un piccolo programmino in Python che, sfruttando le librerie pynput
e pyautogui
, si occupa di controllare lo schermo, cliccare nei punti giusti e ripete queste operazioni all’infinito.
Questo è il risultato:
import time
from pyautogui import pixel
from pynput.mouse import Button, Controller
# Position of the skip icons
skip_1_icon_pos = (480, 564)
skip_2_icon_pos = (680, 564)
skip_2_icon_under_pos = (680, 600)
# Position of the skip buttons
skip_1_btn_pos = (550, 550)
skip_2_btn_pos = (750, 550)
# Position of the skip in the arcane pack
skip_arcane_pack_pos = (890, 654)
# Position of the new game button
new_game_btn_pos = (650, 270)
play_btn_pos = (650, 550)
options_btn_pos = (226, 617)
# Positions of the cards
card_1_pos = (520, 550)
card_2_pos = (620, 550)
card_3_pos = (720, 550)
card_4_pos = (820, 550)
card_5_pos = (920, 550)
card_1_icon_pos = (560, 420)
card_2_icon_pos = (660, 420)
card_3_icon_pos = (760, 420)
card_4_icon_pos = (860, 420)
card_5_icon_pos = (960, 420)
card_1_use_pos = (530, 565)
card_2_use_pos = (630, 565)
card_3_use_pos = (730, 565)
card_4_use_pos = (830, 565)
card_5_use_pos = (930, 565)
cards = tuple(
zip(
[card_1_pos, card_2_pos, card_3_pos, card_4_pos, card_5_pos],
[card_1_icon_pos, card_2_icon_pos, card_3_icon_pos, card_4_icon_pos, card_5_icon_pos],
[card_1_use_pos, card_2_use_pos, card_3_use_pos, card_4_use_pos, card_5_use_pos],
)
)
# Color of the tarot card
tarot_color = (158, 116, 206)
# Color of the tarot skip 1
arcane_pack_color = (125, 96, 224)
# Color of the tarot skip 2
arcane_pack_color_dark = (93, 89, 155)
mouse = Controller()
# Distance between two colors
def dist(p0: "tuple[int, int, int]", p1: "tuple[int, int, int]") -> int:
return abs(p0[0] - p1[0]) + abs(p0[1] - p1[1]) + abs(p0[2] - p1[2])
# After opening an arcane pack, look for the legendary card
def select_arcane_card():
# Hover over card all five cards
for card_pos, icon_pos, use_pos in cards:
time.sleep(0.5)
mouse.position = card_pos
time.sleep(0.1)
color = pixel(icon_pos[0], icon_pos[1])
# print(color)
if dist(color, tarot_color) > 10:
print("Legendary card found!")
mouse.click(Button.left)
time.sleep(1)
mouse.position = use_pos
time.sleep(0.1)
mouse.click(Button.left)
time.sleep(10)
return
print("No legendary card found")
def reset_game():
# Reset the game
mouse.position = options_btn_pos
time.sleep(0.1)
mouse.click(Button.left)
time.sleep(0.5)
mouse.position = new_game_btn_pos
time.sleep(0.1)
mouse.click(Button.left)
time.sleep(0.5)
mouse.position = play_btn_pos
time.sleep(0.1)
mouse.click(Button.left)
time.sleep(2)
print("New game")
def click_arcane_pack():
time.sleep(1)
skip_1_pixel = pixel(skip_1_icon_pos[0], skip_1_icon_pos[1])
skip_2_pixel = pixel(skip_2_icon_under_pos[0], skip_2_icon_under_pos[1])
print("Skip 1 and 2 pixels")
# print(skip_1_pixel, skip_2_pixel)
has_first = dist(skip_1_pixel, arcane_pack_color) < 10
has_second = dist(skip_2_pixel, arcane_pack_color_dark) < 10
if has_first or has_second:
# Click of the first skip
mouse.position = skip_1_btn_pos
time.sleep(0.1)
mouse.click(Button.left)
print("Skip 1")
time.sleep(2.5)
if has_first:
print("Skip 1 is a booster pack")
select_arcane_card()
time.sleep(1)
mouse.position = skip_arcane_pack_pos
time.sleep(0.1)
mouse.click(Button.left)
time.sleep(1)
else:
print("Skip 1 is not a booster pack")
if has_second:
# Do the same for the second skip
mouse.position = skip_2_btn_pos
time.sleep(0.1)
mouse.click(Button.left)
print("Skip 2")
time.sleep(2.5)
if has_second:
print("Skip 2 is a booster pack")
select_arcane_card()
time.sleep(1)
mouse.position = skip_arcane_pack_pos
time.sleep(0.1)
mouse.click(Button.left)
time.sleep(1)
else:
print("Skip 2 is not a booster pack")
reset_game()
for i in range(10):
print("Waiting 5 sec")
time.sleep(5)
print("Starting")
while True:
click_arcane_pack()
Warning
Mi preme sottolinear come lo script sia stato scritto di getto, di notte, e testato unicamente sul mio laptop. Non è garantito che funzioni su altre macchine o che non possa causare danni. Usatelo a vostro rischio e pericolo.
Utilizzo
Per poter utilizzare lo script, la prima cosa da fare è avere Python pronto sul proprio computer e installare le librerie necessarie:
pip install pynput pyautogui
Il passo successivo è aprire Balatro, avviare una partita qualsiasi e iniziare a fare un paio di screenshot come quelli sotto, con lo scopo di individuare le coordinate dei vari elementi grafici.
Per ottenere ristati migliori, è meglio disabilitare il movimento e ridurre a zero il filtro CRT dalle opzioni del gioco.
Ci interessa scoprire le coordinate dei bottoni di skip delle blind e dei pacchetti arcani, il pulsante per utilizzare il tarocco e i tre pulsanti che bisogna premere in sequenza per riavviare la partita.
Inoltre, ci occorre conoscere la posizione e il colore delle icone che identificano il tag sotto le blind che ci interessa e l’indicatore che appare sopra i tarocchi che ci permette di distinguerli dalla carta spettrale che stiamo cercando.
Aprendo l’immagine con un qualsiasi programma di grafica, possiamo ottenere le coordinate a noi utili ed inserirle nello script, avendo cura di non spostare il gioco o cambiare risoluzione.


A quel punto il gioco è quasi fatto. Basta aprire il terminale, posizionarsi nella cartella in cui si trova lo script e lanciarlo con il comando:
python3 script.py
Avremo 5 secondi per riportare il gioco in focus e lo script si occuperà di fare tutto il resto.
Warning
Lo script non contiene un meccanismo di uscita, quindi l’unico modo per fermarlo è chiudere il terminale o premere Ctrl+C
.
Ci sono sufficienti tempi morti fra un’operazione e l’altra, per cui ciò non dovrebbe essere un problema.
Troubleshooting
Gli screenshot sono stati la parte che mi hanno dato più grattacapi. Sembra che ci siano parecchie incompatibilità fra Wayland, Python 3.13 e le librerie che ho utilizzato per fare gli screenshot. In particolare, ho riscontrato l’errore
ImportError: this platform is not supported: ('failed to acquire X connection: Can\'t connect to display ":0": b\'Authorization required, but no authorization protocol specified\\n\'', DisplayConnectionError(':0', b'Authorization required, but no authorization protocol specified\n'))
Try one of the following resolutions:
* Please make sure that you have an X server running, and that the DISPLAY environment variable is set correctly
che sono riuscito a risolvere con
xhost +
# Su Ubuntu o derivati
sudo apt install gnome-screenshot
# Su arch
sudo pacman -S gnome-screenshot
Risultati
Come ogni programmatore che si rispetti, dopo aver finito di scrivere il codice e aver testato un minimo il suo funzionamento, il tutto rigorosamente alle 3 di notte, ho lanciato il software e sono andato a dormire. La mattina seguente mi sono svegliato con la piacevole sorpresa di aver completato l’achievement alle 6:00 circa. Queste sono le statistiche raccolte dallo script:
partite_avviate: 2191
arcane_pack_aperti: 305
arcane_pack_in_skip_1: 149
arcane_pack_in_skip_2: 156
carte_leggendarie_trovate: 6