1+ import random
2+ import polars as pl
3+ import pgzrun
4+ from types import SimpleNamespace
5+
6+ # --- Settings ---
7+ WIDTH = 800
8+ HEIGHT = 600
9+ TITLE = "Wizarding Duel: Turn-Based Strategy"
10+
11+ # --- State ---
12+ hp = {"Harry" : 100 , "Voldemort" : 100 }
13+ display = SimpleNamespace (Harry = 100 , Voldemort = 100 )
14+
15+ message = "A wild VOLDEMORT appeared!"
16+ sub_message = "What will HARRY do?"
17+ current_turn = "Harry" # Who is currently attacking
18+ waiting_for_input = True # Controls if buttons are visible
19+ game_active = True
20+
21+ # --- Load Data ---
22+ spells_df = pl .read_csv ("spells.csv" )
23+
24+ def get_options (character ):
25+ return spells_df .filter (pl .col ("character" ) == character ).to_dicts ()
26+
27+ # --- Battle Logic ---
28+
29+ def player_choice (spell_data ):
30+ """Triggered when Harry clicks a button."""
31+ global waiting_for_input , message , sub_message
32+
33+ waiting_for_input = False
34+ execute_move ("Harry" , "Voldemort" , spell_data )
35+
36+ # If Voldemort survived, schedule his turn in 2 seconds
37+ if game_active :
38+ clock .schedule_unique (voldemort_turn , 2.0 )
39+
40+ def voldemort_turn ():
41+ """Triggered automatically after Harry's turn."""
42+ global message , sub_message , waiting_for_input
43+
44+ if not game_active : return
45+
46+ choices = get_options ("Voldemort" )
47+ spell = random .choice (choices )
48+
49+ execute_move ("Voldemort" , "Harry" , spell )
50+
51+ # After Voldemort attacks, give Harry control back in 1.5 seconds
52+ if game_active :
53+ clock .schedule_unique (reset_to_player , 1.5 )
54+
55+ def execute_move (attacker , defender , spell_data ):
56+ global message , sub_message , game_active
57+
58+ raw_damage = float (spell_data ["damage" ])
59+
60+ # --- HEALING LOGIC ---
61+ if raw_damage < 0 :
62+ # It's a heal! Target is the attacker, not the defender
63+ heal_amount = abs (raw_damage )
64+ hp [attacker ] = min (100 , hp [attacker ] + heal_amount ) # Cap at 100
65+
66+ message = f"{ attacker .upper ()} used { spell_data ['spell' ].upper ()} !"
67+ sub_message = f"It recovered { heal_amount } HP!"
68+
69+ # Animate the attacker's bar
70+ if attacker == "Harry" :
71+ animate (display , duration = 0.6 , Harry = hp ["Harry" ])
72+ else :
73+ animate (display , duration = 0.6 , Voldemort = hp ["Voldemort" ])
74+
75+ # --- ATTACK LOGIC ---
76+ else :
77+ hp [defender ] = max (0 , hp [defender ] - raw_damage )
78+ message = f"{ attacker .upper ()} used { spell_data ['spell' ].upper ()} !"
79+ sub_message = f"It dealt { raw_damage } damage!"
80+
81+ # Animate the defender's bar
82+ if defender == "Harry" :
83+ animate (display , duration = 0.6 , Harry = hp ["Harry" ])
84+ else :
85+ animate (display , duration = 0.6 , Voldemort = hp ["Voldemort" ])
86+
87+ # Check for win/loss (only matters if damage was dealt)
88+ if hp [defender ] <= 0 :
89+ game_active = False
90+ message = f"{ defender .upper ()} fainted!"
91+ sub_message = f"{ attacker .upper ()} is the winner!"
92+
93+ def reset_to_player ():
94+ global message , sub_message , waiting_for_input
95+ message = "What will HARRY do?"
96+ sub_message = "Choose a spell to cast!"
97+ waiting_for_input = True
98+
99+ # --- Draw Functions ---
100+
101+ def draw ():
102+ screen .draw .filled_rect (Rect ((0 , 0 ), (800 , 400 )), (200 , 230 , 255 ))
103+ screen .draw .filled_rect (Rect ((0 , 400 ), (800 , 200 )), (120 , 180 , 120 ))
104+
105+ # Status Boxes
106+ draw_status_box ("VOLDEMORT" , display .Voldemort , 50 , 50 )
107+ draw_status_box ("HARRY" , display .Harry , 450 , 300 )
108+
109+ # UI Box
110+ screen .draw .filled_rect (Rect ((10 , 410 ), (780 , 180 )), (50 , 50 , 60 ))
111+ screen .draw .rect (Rect ((10 , 410 ), (780 , 180 )), "white" )
112+
113+ if waiting_for_input and game_active :
114+ draw_move_menu ()
115+ else :
116+ # Show text messages during animations or enemy turn
117+ screen .draw .text (message , (40 , 450 ), fontsize = 40 , color = "white" )
118+ screen .draw .text (sub_message , (40 , 510 ), fontsize = 30 , color = "lightgray" )
119+
120+ def draw_status_box (name , current_hp , x , y ):
121+ screen .draw .filled_rect (Rect ((x , y ), (300 , 80 )), "white" )
122+ screen .draw .rect (Rect ((x , y ), (300 , 80 )), "black" )
123+ screen .draw .text (name , (x + 20 , y + 15 ), color = "black" , fontsize = 30 )
124+ # HP Bar Border
125+ screen .draw .rect (Rect ((x + 100 , y + 45 ), (160 , 15 )), "black" )
126+ # Fill
127+ bar_width = (current_hp / 100 ) * 158
128+ color = "green" if current_hp > 50 else "orange" if current_hp > 20 else "red"
129+ if bar_width > 0 :
130+ screen .draw .filled_rect (Rect ((x + 101 , y + 46 ), (bar_width , 13 )), color )
131+
132+ def draw_move_menu ():
133+ options = get_options ("Harry" )[:4 ]
134+ for i , spell in enumerate (options ):
135+ x = 40 + (i % 2 ) * 380
136+ y = 440 + (i // 2 ) * 60
137+ screen .draw .rect (Rect ((x , y ), (350 , 50 )), "white" )
138+ screen .draw .text (f"> { spell ['spell' ].upper ()} " , (x + 20 , y + 15 ), fontsize = 30 )
139+
140+ # --- Input ---
141+
142+ def on_mouse_down (pos ):
143+ if game_active and waiting_for_input :
144+ options = get_options ("Harry" )[:4 ]
145+ for i in range (len (options )):
146+ x = 40 + (i % 2 ) * 380
147+ y = 440 + (i // 2 ) * 60
148+ if Rect ((x , y ), (350 , 50 )).collidepoint (pos ):
149+ player_choice (options [i ])
150+
151+ pgzrun .go ()
0 commit comments