1+ import time
2+ import sys
3+
4+ from curtsies import FullscreenWindow , Input , FSArray , fmtstr , fsarray
5+ from curtsies .fmtfuncs import (
6+ bold ,
7+ yellow ,
8+ on_blue ,
9+ cyan ,
10+ on_yellow ,
11+ on_red ,
12+ black ,
13+ )
14+ import curtsies .events
15+
16+
17+ class Cycle :
18+ def __init__ (self , attr_dict ):
19+ self .appearance = attr_dict ["appearance" ]
20+ self .x , self .y = attr_dict ["x" ], attr_dict ["y" ]
21+ self .keylist = attr_dict ["keys" ]
22+ self .dir = 0
23+
24+ def move (self , grid ):
25+ d = self .dir
26+ if d == 0 :
27+ self .x += 1
28+ elif d == 90 :
29+ self .y -= 1
30+ elif d == 180 :
31+ self .x -= 1
32+ elif d == 270 :
33+ self .y += 1
34+
35+ def face (self , newdir ):
36+ """ turn to face the given direction """
37+ if not (newdir % 180 == self .dir % 180 ):
38+ self .dir = newdir
39+
40+ def paint (self , grid ):
41+ """ given a grid object, adds self to the grid and returns new grid
42+ """
43+ grid [self .y , self .x ] = self .appearance
44+ return grid
45+
46+
47+ class Bot (Cycle ):
48+ """ Dumb bot that only turns right when it's about to crash
49+ """
50+
51+ def move (self , grid ):
52+
53+ a = self .getNextSquareCoords ()
54+ if grid [a [0 ], a [1 ]] != [" " ]:
55+ self .turnLeft ()
56+ super ().move (grid )
57+
58+ def turnRight (self ):
59+ self .dir = (self .dir - 90 ) % 360
60+
61+ def turnLeft (self ):
62+ self .dir = (self .dir + 90 ) % 360
63+
64+ def getNextSquareCoords (self , n = 2 ):
65+ """ Returns the x, y coords of the box n spots in front of the bot.
66+ """
67+ if self .dir == 0 :
68+ return (self .y , self .x + n )
69+ elif self .dir == 90 :
70+ return (self .y - n , self .x )
71+ elif self .dir == 180 :
72+ return (self .y , self .x - n )
73+ else :
74+ return (self .y + n , self .x )
75+
76+
77+ class gameboard :
78+ def __init__ (self , width , height , players ):
79+ self .width = width
80+ self .height = height
81+ self .grid = FSArray (height , width )
82+
83+ self .players = players
84+ self .numplayers = len (self .players )
85+
86+ def draw_border (self ):
87+ """ draw outer border """
88+ box = on_yellow (black (bold ("!" )))
89+ for x in range (1 , self .width ):
90+ self .grid [1 , x ] = box
91+ self .grid [self .height - 1 , x ] = box
92+ for y in range (1 , self .height ):
93+ self .grid [y , 1 ] = box
94+ self .grid [y , self .width - 1 ] = box
95+
96+ def process_event (self , key ):
97+ if key == " " :
98+ sys .exit ()
99+
100+ for player in self .players :
101+ if key in player .keylist :
102+ player .face (player .keylist [key ])
103+ return False
104+
105+ def tick (self ):
106+ """ Do one frame of work. Returns the winner if there
107+ is a crash
108+ """
109+ # list of players alive this frame
110+ temp_players = self .players [:]
111+
112+ for bike in self .players :
113+ bike .move (self .grid )
114+
115+ if self .grid [bike .y , bike .x ] == [" " ]:
116+ self .grid [bike .y , bike .x ] = bike .appearance
117+ else : # crashed
118+ temp_players .remove (bike )
119+ self .grid [bike .y , bike .x ] = fmtstr ("X" , "red" , "bold" , "on_black" )
120+
121+ if len (temp_players ) == 0 :
122+ return "tie"
123+ elif len (temp_players ) < len (self .players ):
124+ return temp_players
125+
126+ def winner_msg (self , tick ):
127+ if len (tick ) != 1 :
128+ msg = fmtstr ("it's a tie!" , "red" )
129+ else :
130+ winner = tick [0 ]
131+ winner_name = winner .appearance
132+ msg = fmtstr ("winner:" , "yellow" )
133+ msg += " " + (winner_name * 5 )
134+ return msg
135+
136+
137+ class Frame (curtsies .events .ScheduledEvent ):
138+ pass
139+
140+
141+ def do_introduction (window ):
142+ h , w = window .height , window .width
143+
144+ messages = [
145+ "two player tron" ,
146+ fmtstr ("player 1:" , "on_blue" , "cyan" ) + " wasd" ,
147+ fmtstr ("player 2:" , "on_red" , "yellow" ) + " arrow keys" ,
148+ ]
149+
150+ billboard = FSArray (h , w )
151+ msg_row = h // 2 - 2
152+ for msg in messages :
153+ billboard [
154+ msg_row , w // 2 - len (msg ) // 2 : w // 2 + len (msg ) // 2 + 1
155+ ] = fsarray ([msg ])
156+ msg_row += 1
157+ window .render_to_terminal (billboard )
158+
159+ # countdown msg
160+ for i in range (3 , 0 , - 1 ):
161+ billboard [msg_row , w // 2 ] = fmtstr (str (i ), "red" )
162+ window .render_to_terminal (billboard )
163+ time .sleep (1 )
164+
165+
166+ def mainloop (window , p2_bot = False ):
167+ p1_attrs = {
168+ "appearance" : on_blue ((cyan ("1" ))),
169+ "x" : window .width // 4 ,
170+ "y" : window .height // 2 ,
171+ "keys" : {"w" : 90 , "a" : 180 , "s" : 270 , "d" : 0 },
172+ }
173+
174+ p2_attrs = {
175+ "appearance" : on_red ((yellow ("2" ))),
176+ "x" : 3 * window .width // 4 ,
177+ "y" : window .height // 2 ,
178+ "keys" : {"<UP>" : 90 , "<LEFT>" : 180 , "<DOWN>" : 270 , "<RIGHT>" : 0 },
179+ }
180+
181+ FPS = 15
182+
183+ players = [Cycle (p1_attrs ), Cycle (p2_attrs )]
184+ if p2_bot : # make p2 a bot
185+ players [1 ] = Bot (p2_attrs )
186+
187+ world = gameboard (window .width , window .height , players )
188+ dt = 1 / FPS
189+ world .draw_border ()
190+ window .render_to_terminal (world .grid )
191+
192+ reactor = Input ()
193+ schedule_next_frame = reactor .scheduled_event_trigger (Frame )
194+ schedule_next_frame (when = time .time ())
195+ with reactor :
196+ for c in reactor :
197+ if isinstance (c , Frame ):
198+ tick = world .tick ()
199+ window .render_to_terminal (world .grid )
200+ if not tick : # if no crashes
201+ when = c .when + dt
202+ while when < time .time ():
203+ when += dt
204+ schedule_next_frame (when )
205+ else : # if crashed
206+ world .grid [0 :4 , 0 :25 ] = fsarray (
207+ [
208+ world .winner_msg (tick ),
209+ "r to restart" ,
210+ "q to quit" ,
211+ "b to make player 2 a bot" ,
212+ ]
213+ )
214+ window .render_to_terminal (world .grid )
215+ elif c .lower () in ["r" , "q" , "b" ]:
216+ break
217+ else : # common case
218+ world .process_event (c )
219+ if c .lower () == "r" :
220+ mainloop (window , p2_bot )
221+ elif c .lower () == "b" :
222+ mainloop (window , True )
223+
224+
225+ def main ():
226+ with FullscreenWindow (sys .stdout ) as window :
227+ do_introduction (window )
228+
229+ mainloop (window )
230+
231+
232+ if __name__ == "__main__" :
233+ main ()
0 commit comments