55import string
66from abc import ABC , abstractmethod
77from datetime import datetime
8+ from typing import Any , TypedDict
89
910from .enums import Actions , Decks , Stakes , State
1011
1112
12- def cache_state (game_step , G ):
13- timestamp = datetime .now ().strftime ("%Y%m%d%H%M%S%f" )
14- if not os .path .exists (f"gamestate_cache/{ game_step } /" ):
15- os .makedirs (f"gamestate_cache/{ game_step } /" )
16- filename = f"gamestate_cache/{ game_step } /{ timestamp } .json"
17- with open (filename , "w" ) as f :
18- f .write (json .dumps (G , indent = 4 ))
13+ class ActionSchema (TypedDict ):
14+ """Schema for action dictionary with name and arguments fields."""
15+
16+ action : Actions
17+ args : list [Any ] | None
1918
2019
2120class Bot (ABC ):
21+ """Abstract base class for Balatro bots.
22+
23+ This class provides the framework for creating bots that can play Balatro.
24+ Subclasses must implement all abstract methods to define the bot's behavior.
25+
26+ Attributes:
27+ G (dict[str, Any] | None): The current game state.
28+ deck (Decks): The deck type to use.
29+ stake (Stakes): The stake level to play at.
30+ seed (str): The random seed for the game.
31+ challenge (str | None): The challenge mode, if any.
32+ bot_port (int): The port for bot communication.
33+ addr (tuple[str, int]): The socket address for communication.
34+ running (bool): Whether the bot is currently running.
35+ balatro_instance (Any): The Balatro game instance.
36+ sock (socket.socket | None): The socket for communication.
37+ state (dict[str, Any]): The bot's internal state.
38+ """
39+
2240 def __init__ (
2341 self ,
2442 deck : Decks ,
2543 stake : Stakes = Stakes .WHITE ,
2644 seed : str = "" ,
2745 challenge : str | None = None ,
2846 bot_port : int = 12346 ,
29- ):
30- self .G = None
31- self .deck = deck
32- self .stake = stake
33- self .seed = seed
34- self .challenge = challenge
47+ ) -> None :
48+ """Initialize a new Bot instance.
49+
50+ Args:
51+ deck (Decks): The deck type to use.
52+ stake (Stakes): The stake level to play at.
53+ seed (str): The random seed for the game.
54+ challenge (str | None): The challenge mode, if any.
55+ bot_port (int): The port for bot communication.
56+ """
57+ self .G : dict [str , Any ] | None = None
58+ self .deck : Decks = deck
59+ self .stake : Stakes = stake
60+ self .seed : str = seed
61+ self .challenge : str | None = challenge
3562
36- self .bot_port = bot_port
63+ self .bot_port : int = bot_port
3764
38- self .addr = ("127.0.0.1" , self .bot_port )
39- self .running = False
40- self .balatro_instance = None
65+ self .addr : tuple [ str , int ] = ("127.0.0.1" , self .bot_port )
66+ self .running : bool = False
67+ self .balatro_instance : Any = None
4168
42- self .sock = None
69+ self .sock : socket . socket | None = None
4370
44- self .state = {}
71+ self .state : dict [ str , Any ] = {}
4572
4673 @staticmethod
47- def random_seed ():
74+ def random_seed () -> str :
75+ """Generate a random seed for the game.
76+
77+ Returns:
78+ str: A random 7-character seed string.
79+ """
4880 return "" .join (random .choices (string .digits + string .ascii_uppercase , k = 7 ))
4981
5082 @abstractmethod
51- def skip_or_select_blind (self , env : dict ):
83+ def skip_or_select_blind (self , env : dict ) -> ActionSchema :
84+ """
85+ Decide whether to skip or select a blind.
86+
87+ Returns:
88+ ActionSchema with 'action' (Actions enum) and optional 'args' (any additional arguments)
89+ Example: {'action': Actions.SELECT_BLIND, 'args': [0]}
90+ """
5291 pass
5392
5493 @abstractmethod
55- def select_cards_from_hand (self , env : dict ):
94+ def select_cards_from_hand (self , env : dict ) -> ActionSchema :
95+ """
96+ Select cards from hand to play or discard.
97+
98+ Returns:
99+ ActionSchema with 'action' (Actions enum) and optional 'args' (card indices)
100+ Example: {'action': Actions.PLAY_HAND, 'args': [1, 2, 3]}
101+ """
56102 pass
57103
58104 @abstractmethod
59- def select_shop_action (self , env : dict ):
105+ def select_shop_action (self , env : dict ) -> ActionSchema :
106+ """
107+ Select an action in the shop.
108+
109+ Returns:
110+ ActionSchema with 'action' (Actions enum) and optional 'args' (shop item index)
111+ Example: {'action': Actions.BUY_CARD, 'args': [0]}
112+ """
60113 pass
61114
62115 @abstractmethod
63- def select_booster_action (self , env : dict ):
116+ def select_booster_action (self , env : dict ) -> ActionSchema :
117+ """
118+ Select an action for booster packs.
119+
120+ Returns:
121+ ActionSchema with 'action' (Actions enum) and optional 'args' (booster card index)
122+ Example: {'action': Actions.SELECT_BOOSTER_CARD, 'args': [0]}
123+ """
64124 pass
65125
66126 @abstractmethod
67- def sell_jokers (self , env : dict ):
127+ def sell_jokers (self , env : dict ) -> ActionSchema :
128+ """
129+ Sell jokers from the collection.
130+
131+ Returns:
132+ ActionSchema with 'action' (Actions enum) and optional 'args' (joker index)
133+ Example: {'action': Actions.SELL_JOKER, 'args': [0]}
134+ """
68135 pass
69136
70137 @abstractmethod
71- def rearrange_jokers (self , env : dict ):
138+ def rearrange_jokers (self , env : dict ) -> ActionSchema :
139+ """
140+ Rearrange jokers in the collection.
141+
142+ Returns:
143+ ActionSchema with 'action' (Actions enum) and optional 'args' (new arrangement)
144+ Example: {'action': Actions.REARRANGE_JOKERS, 'args': [0, 1, 2]}
145+ """
72146 pass
73147
74148 @abstractmethod
75- def use_or_sell_consumables (self , env : dict ):
149+ def use_or_sell_consumables (self , env : dict ) -> ActionSchema :
150+ """
151+ Use or sell consumable cards.
152+
153+ Returns:
154+ ActionSchema with 'action' (Actions enum) and optional 'args' (consumable index)
155+ Example: {'action': Actions.USE_CONSUMABLE, 'args': [0]}
156+ """
76157 pass
77158
78159 @abstractmethod
79- def rearrange_consumables (self , env : dict ):
160+ def rearrange_consumables (self , env : dict ) -> ActionSchema :
161+ """
162+ Rearrange consumable cards.
163+
164+ Returns:
165+ ActionSchema with 'action' (Actions enum) and optional 'args' (new arrangement)
166+ Example: {'action': Actions.REARRANGE_CONSUMABLES, 'args': [0, 1, 2]}
167+ """
80168 pass
81169
82170 @abstractmethod
83- def rearrange_hand (self , env : dict ):
171+ def rearrange_hand (self , env : dict ) -> ActionSchema :
172+ """
173+ Rearrange cards in hand.
174+
175+ Returns:
176+ ActionSchema with 'action' (Actions enum) and optional 'args' (new arrangement)
177+ Example: {'action': Actions.REARRANGE_HAND, 'args': [0, 1, 2, 3, 4]}
178+ """
84179 pass
85180
86- def _action_to_action_str (self , action ):
181+ def _action_to_action_str (self , action : ActionSchema ) -> str :
182+ """
183+ Convert action to string format expected by the game.
184+
185+ Args:
186+ action: ActionSchema dict with 'action' and optional 'args'
187+
188+ Returns:
189+ Pipe-separated string representation of the action
190+ """
87191 result = []
88192
89- for x in action :
90- if isinstance (x , Actions ):
91- result .append (x .name )
92- elif type (x ) is list :
93- result .append ("," .join ([str (y ) for y in x ]))
193+ # Add the action name
194+ action_enum = action .get ("action" )
195+ if isinstance (action_enum , Actions ):
196+ result .append (action_enum .name )
197+ else :
198+ result .append (str (action_enum ))
199+
200+ # Add arguments if present
201+ args = action .get ("args" )
202+ if args :
203+ if isinstance (args , list ):
204+ result .append ("," .join ([str (arg ) for arg in args ]))
94205 else :
95- result .append (str (x ))
206+ result .append (str (args ))
96207
97208 return "|" .join (result )
98209
99- def chooseaction (self , env : dict ):
210+ def chooseaction (self , env : dict [str , Any ]) -> ActionSchema :
211+ """Choose an action based on the current game state.
212+
213+ Args:
214+ env (dict[str, Any]): The current game environment state.
215+
216+ Returns:
217+ ActionSchema: The action to perform with 'action' and optional 'args'.
218+ """
100219 print ("Choosing action based on game state:" , env ["state" ])
101220 if env ["state" ] == State .GAME_OVER :
102221 self .running = False
@@ -105,13 +224,10 @@ def chooseaction(self, env: dict):
105224 case "start_run" :
106225 print ("Starting run with deck:" , self .deck )
107226 seed = self .seed or self .random_seed ()
108- return [
109- Actions .START_RUN ,
110- self .stake .value ,
111- self .deck .value ,
112- seed ,
113- self .challenge ,
114- ]
227+ return {
228+ "action" : Actions .START_RUN ,
229+ "args" : [self .stake .value , self .deck .value , seed , self .challenge ],
230+ }
115231 case "skip_or_select_blind" :
116232 print ("Choosing action: skip_or_select_blind" )
117233 return self .skip_or_select_blind (env )
@@ -139,8 +255,15 @@ def chooseaction(self, env: dict):
139255 case "rearrange_hand" :
140256 print ("Choosing action: rearrange_hand" )
141257 return self .rearrange_hand (env )
258+ case _:
259+ raise ValueError (f"Unhandled waitingFor state: { env ['waitingFor' ]} " )
142260
143- def run_step (self ):
261+ def run_step (self ) -> None :
262+ """Execute a single step of the bot's main loop.
263+
264+ This method handles socket communication, receives game state updates,
265+ and sends actions back to the game.
266+ """
144267 if self .sock is None :
145268 self .state = {}
146269 self .G = None
@@ -161,7 +284,6 @@ def run_step(self):
161284 print (env ["response" ])
162285 else :
163286 if env ["waitingForAction" ]:
164- cache_state (env ["waitingFor" ], env )
165287 action = self .chooseaction (env )
166288 if action :
167289 action_str = self ._action_to_action_str (action )
@@ -176,6 +298,11 @@ def run_step(self):
176298 self .sock .settimeout (1 )
177299 self .sock .connect (self .addr )
178300
179- def run (self ):
301+ def run (self ) -> None :
302+ """Run the bot's main game loop.
303+
304+ This method continues running until the bot is stopped,
305+ executing run_step() repeatedly.
306+ """
180307 while self .running :
181308 self .run_step ()
0 commit comments