This repository has been archived by the owner on Nov 12, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathPlay.pyxl
503 lines (471 loc) · 21.2 KB
/
Play.pyxl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
Play
════════════════════════════════════════════════════════════════════════
/** Clone of map for the game */
const PLAYER_COUNT = 4 // players are skipped if player_types[i] == PLAYER_TYPE_NONE
const PLAYER_NEUTRAL = -1
const INVALID_HEX = -2
const PLAYER_START_HEXES = [xy(1,9), xy(9,1), xy(5,1), xy(5,9)]
const PLAYER_CLICK_SOUNDS = [
{sound:blip_05_sound, pan: -50%, pitch:85%, volume: 100%},
{sound:blip_05_sound, pan: +50%, pitch:170%, volume: 100%},
{sound:blip_07_sound, pan: -50%, pitch:101%, volume: 120%},
{sound:blip_07_sound, pan: +50%, pitch:128%, volume: 100%},
]
const PLAYER_TERRITORY_SPRITES = [
hex_sprite.territory_p1,
hex_sprite.territory_p2,
hex_sprite.territory_p3,
hex_sprite.territory_p4,
]
const PLAYER_OVERLAY_SPRITES = [
hex_sprite.overlay_p1,
hex_sprite.overlay_p2,
hex_sprite.overlay_p3,
hex_sprite.overlay_p4,
]
const PLAYER_PREVIEW_MAP_COORDS = [
xy(3,8),
xy(12,2),
xy(3,2),
xy(12,8),
]
const NUM_BOARD_HEXES = 61
const VALID_HEX_COORD_MASK = [
0b0000000000000000,
0b0000001111100000,
0b0000001111110000,
0b0000001111111000,
0b0000001111111100,
0b0000001111111110,
0b0000000111111110,
0b0000000011111110,
0b0000000001111110,
0b0000000000111110,
0b0000000000000000,
]
const DIR_NE = 0
const DIR_E = 1
const DIR_SE = 2
const DIR_SW = 3
const DIR_W = 4
const DIR_NW = 5
const HEX_COORD_NEIGHBOR_OFFSETS = [
xy(+0,+1), // NE
xy(+1,+0), // E
xy(+1,-1), // SE
xy(+0,-1), // SW
xy(-1,+0), // W
xy(-1,+1), // NW
]
const ARROW_SOLID_SPRITES = [
arrows_sprite.solid_nw[0], // NE
arrows_sprite.solid_w[0], // E
arrows_sprite.solid_nw[0], // SE
arrows_sprite.solid_nw[0], // SW
arrows_sprite.solid_w[0], // W
arrows_sprite.solid_nw[0], // NW
]
const ARROW_OUTLINE_SPRITES = [
arrows_sprite.outline_nw[0], // NE
arrows_sprite.outline_w[0], // E
arrows_sprite.outline_nw[0], // SE
arrows_sprite.outline_nw[0], // SW
arrows_sprite.outline_w[0], // W
arrows_sprite.outline_nw[0], // NW
]
const ARROW_SPRITE_SCALES = [
xy(-1,+1), // NE
xy(-1,+1), // E
xy(-1,-1), // SE
xy(+1,-1), // SW
xy(+1,+1), // W
xy(+1,+1), // NW
]
// Draw a sprite at the provided map coordinates
def draw_sprite_on_hex_map(sprite, map_coord, z default 0, scale default xy(1,1), override_color default rgba(0,0,0,0), override_blend default "lerp"):
let pos = xy(
MUL(HEX_WIDTH, MAD(½, map_coord.y bitand 1, map_coord.x)),
MUL(HEX_HEIGHT_75, map_coord.y))
draw_sprite({sprite:sprite, pos:pos, z:z, scale:scale, override_color:override_color,
override_blend:override_blend})
// Per-cell owner info
// Get the player ID of a map cell (PLAYER_NEUTRAL for unclaimed cells, INVALID_HEX for cells outside the play area)
def get_cell_owner_map_coord(map_coord):
return map_cell_owner[map_coord.x][map_coord.y]
def get_cell_owner_hex_coord(hex_coord):
return get_cell_owner_map_coord(transform_hex_to_map_coord(hex_coord))
// Change the owner of a cell (including swapping its sprite)
def set_cell_owner_map_coord(map_coord, player_id):
map_cell_owner[map_coord.x][map_coord.y] = player_id
set_map_sprite(map, map_coord,
if player_id == PLAYER_NEUTRAL then hex_sprite.territory_neutral else PLAYER_TERRITORY_SPRITES[player_id])
def set_cell_owner_hex_coord(hex_coord, player_id):
set_cell_owner_map_coord(transform_hex_to_map_coord(hex_coord), player_id)
// Check whether a cell is within the play area
def is_valid_board_hex(hex_coord):
return get_cell_owner_hex_coord(hex_coord) ≠ INVALID_HEX
// Generate a move code. Not all moves are equally likely.
def generate_move_code(player_id):
// Sweep all cells and figure out which directions a player COULD potentially
// capture a tile, so we can favor moves containing that direction if possible.
let valid_capture_mask = 0b000000
for x < map.size.x:
for y < map.size.y:
const mc = xy(x,y)
// only consider valid cells for this player to move
if get_cell_owner_map_coord(mc) != player_id:
continue
const hc = transform_map_coord_to_hex(mc)
for neighbor < 6:
const neighbor_owner = get_cell_owner_hex_coord(hc + HEX_COORD_NEIGHBOR_OFFSETS[neighbor])
if neighbor_owner == PLAYER_NEUTRAL:
valid_capture_mask ∪= (1<<neighbor)
const p = ξ
let bit_count = 0
if p < 0.50:
bit_count = 1
else if p < 0.80:
bit_count = 2
else if p < 0.90:
bit_count = 3
else if p < 0.95:
bit_count = 4
else if p < 0.98:
bit_count = 5
else:
bit_count = 6
let move = 0b000000
const set_bits = slice(shuffled([0,1,2,3,4,5]), 0, bit_count)
for b in set_bits:
move ∪= 1<<b
if (move bitand valid_capture_mask) == 0:
let valid_capture_bits = []
for i < 6:
if valid_capture_mask bitand (1<<i):
push(valid_capture_bits, i)
move ∪= (1 << random_value(valid_capture_bits))
// Random chance of one direction being able to flip non-neutral tiles
// Chance of a super arrow increases based on how far behind the current player is from the lead.
let num_players_with_higher_score = 0
for i < 4:
if player_scores[i] > player_scores[player_id]:
num_players_with_higher_score += 1
if ξ < (BASE_SUPER_ARROW_CHANCE + num_players_with_higher_score*SUPER_ARROW_CHANCE_BOOST):
move ∪= 1<<(set_bits[0]+6)
return move
// turn-taking
def advance_to_next_player():
// Advance current player to next non-NULL player whose score is not zero
// (the "score is not zero" is necessary to avoid an infinite loop where a player has NO valid moves,
// but the board is not yet complete)
current_player = (current_player+1) mod 4
while player_types[current_player] == PLAYER_TYPE_NONE or player_scores[current_player] == 0:
current_player = (current_player+1) mod 4
// move current players next move to their current move and generate a new next move
player_current_moves[current_player] = player_next_moves[current_player]
player_next_moves[current_player] = generate_move_code(current_player)
// Update player "next move" previews
let preview_mc = PLAYER_PREVIEW_MAP_COORDS[current_player]
let preview_hc = transform_map_coord_to_hex(preview_mc)
// Need a special fade-out function here, since we don't know exactly how many frames
// it will take to reach the target point in the music.
def at_beat0_enter_gameover(frames_left, total_frames, data):
if bgm_beat_in_measure == 0 and bgm_beat_ff:
set_mode(GameOver, player_types, player_scores) because "Game finished"
def draw_gizmos() preserving_transform:
reset_transform()
const sq3 = sqrt(3)
const verts = [xy(0,2), xy(-sq3, 1), xy(-sq3,-1), xy(0,-2), xy(sq3,-1), xy(sq3,1)]
const hex_poly_args = {vertex_array:verts, z:-10,}
const t = bgm_measure + bgm_measure_t
const b = bgm_beat mod (64*12)
local:
// Spinning gizmo
const theta = clamp((3*bgm_measure_t)^3,0,1) * pi/3 + π/2
const p = 50% SCREEN_SIZE + xy(40*sin(π*t), 0)
const c = hsv(t mod 1,1,1)
draw_poly({pos:p, scale:xy(60,60), angle:theta, outline:c, ...hex_poly_args})
local:
// pulsing bass drum gizmo
bass_gizmo_scale = if bgm_beat_ff and find(BD_BEATS, b) != ∅ then 80 else bass_gizmo_scale-0.5
draw_poly({pos:50% SCREEN_SIZE, angle:π/2, scale:xy(bass_gizmo_scale,bass_gizmo_scale),
outline:#FFC0C0, ...hex_poly_args})
// pulsing snare drum gizmo
snare_gizmo_scale = if bgm_beat_ff and find(SD_BEATS, b) != ∅ then 80 else snare_gizmo_scale-0.7
draw_poly({pos:50% SCREEN_SIZE, angle:0, scale:xy(snare_gizmo_scale,snare_gizmo_scale),
outline:#C0F0C0, ...hex_poly_args})
// Returns a player ID, or ∅ if nobody has won yet
def get_winner(scores):
// 1. No neutral hexes left
const high_score = max(scores[0], scores[1], scores[2], scores[3])
if scores[0] + scores[1] + scores[2] + scores[3] == NUM_BOARD_HEXES:
for i < PLAYER_COUNT:
if scores[i] == high_score:
return i
// 2. Only one player has territory on the board
let players_with_nonzero_scores = 0
let winner = ∅
for i < PLAYER_COUNT:
if player_scores[i] > 0:
players_with_nonzero_scores += 1
winner = i
if players_with_nonzero_scores == 1:
return winner
// No winner yet
return ∅
// Returns a {hex, flips} object. the hex field may be ∅, in which case the player has no valid move and should
// pass.
def get_best_move_hex(player_id):
let best_hc = nil
let best_flips = -1
let best_super_flips = 0
const move = player_current_moves[player_id]
// Build a list of all hexes owned by the player.
// (yeah we could maintain this incrementally instead, whatever, it's fine.
let player_hexes = []
for x < map.size.x:
for y < map.size.y:
const mc = xy(x,y)
// only consider valid cells for this player to move
if get_cell_owner_map_coord(mc) != player_id:
continue
// count how many neighboring cells would be flipped
const hc = transform_map_coord_to_hex(mc)
push(player_hexes, hc)
assert(size(player_hexes) > 0, "players with no hexes shouldn't get into get_best_move_hex()!")
// Randomize the iteration order to avoid any bias for which move is picked
shuffle(player_hexes)
for hc in player_hexes:
let new_scores = clone(player_scores)
let flips = 0
let super_flips = 0
for neighbor < 6:
if (move bitand (1<<neighbor)) ≠ 0:
const neighbor_hex = hc + HEX_COORD_NEIGHBOR_OFFSETS[neighbor]
const arrow_can_capture = (move bitand (1<<(neighbor+6))) ≠ 0
const neighbor_hex_owner = get_cell_owner_hex_coord(neighbor_hex)
// out-of-bounds hexes can't be flipped
if neighbor_hex_owner == INVALID_HEX:
continue
// Normal arrows can only flip neutral tiles
else if neighbor_hex_owner == PLAYER_NEUTRAL:
flips += 1
new_scores[player_id] += 1
// "super" arrows can flip any tiles
else if arrow_can_capture and neighbor_hex_owner != player_id:
flips += 1
super_flips += 1
new_scores[player_id] += 1
new_scores[neighbor_hex_owner] -= 1
// If this move would cause this player to win, then obviously do that one!
if get_winner(new_scores) == player_id:
return {hex:hc, flips:flips}
// Otherwise, save this move if it's the best candidate so far.
if flips > best_flips or (flips == best_flips and super_flips > best_super_flips):
best_hc = hc
best_flips = flips
best_super_flips = super_flips
return {hex:best_hc, flips:best_flips}
def move_cpu_player():
let best_move = get_best_move_hex(current_player)
assert(best_move.hex ≠ ∅, "player has NO best move?")
player_hexes[current_player] = best_move.hex
def commit_cpu_player():
current_player_move_committed = true
let map = nil
let player_types = []
let player_hexes = []
let player_current_moves = []
let player_next_moves = []
let player_scores = []
let BD_BEATS = []
let SD_BEATS = []
let bass_gizmo_scale = 0
let snare_gizmo_scale = 0
let map_cell_owner = []
let disable_input = false
let current_player = 0
let current_player_move_committed = false
let cpu_move_sequence = ∅
let invalid_move_message_f = -1000
enter(game_info)
────────────────────────────────────────────────────────────────────────
map = clone(game_board_map)
player_types = clone(game_info.player_types)
player_hexes = [xy(1,9), xy(9,1), xy(5,1), xy(5,9)]
player_current_moves = [0,0,0,0]
player_next_moves = [0,0,0,0]
player_scores = [0,0,0,0]
BD_BEATS = []
SD_BEATS = []
bass_gizmo_scale = 0
snare_gizmo_scale = 0
map_cell_owner = []
disable_input = false
current_player = 0
current_player_move_committed = false
cpu_move_sequence = ∅
invalid_move_message_f = -1000
set_random_seed() // default is time-based
// construct bass/snare beat lists
for beats at measure in RAW_BD_BEATS:
for b in beats:
push(BD_BEATS, 12*measure+b)
for beats at measure in RAW_SD_BEATS:
for b in beats:
push(SD_BEATS, 12*measure+b)
set_transform(xy(-48, SCREEN_SIZE.y + 8), xy(1, -1))
// player 0 may not actually be valid, so advance until we find one who is
while player_types[current_player] == PLAYER_TYPE_NONE:
current_player += 1
for x < size(map):
let new_col = []
for y < size(map[x]):
let hc = transform_map_coord_to_hex(xy(x,y))
new_col.push(if (VALID_HEX_COORD_MASK[hc.y] bitand (1<<hc.x)) ≠ 0 then PLAYER_NEUTRAL else INVALID_HEX)
map_cell_owner.push(new_col)
for i < PLAYER_COUNT:
if player_types[i] == PLAYER_TYPE_NONE:
continue
// Players start on their own territory
set_cell_owner_hex_coord(PLAYER_START_HEXES[i], i)
player_scores[i] += 1
// random starting moves for each player
player_current_moves[i] = generate_move_code(i)
player_next_moves[i] = generate_move_code(i)
let loops = [
{sound:bgmusic_sound, loop:true, volume:100%},
]
bgm_start(loops)
frame
────────────────────────────────────────────────────────────────────────
//
// reactive-music stuff goes here
//
bgm_update()
let bgm_loop_measure = bgm_measure mod 64
reset_post_effects()
if bgm_loop_measure == 0 or bgm_loop_measure == 8 or bgm_loop_measure == 24 or bgm_loop_measure == 32 or bgm_loop_measure == 48:
const bloom_amount = -((bgm_measure_t-1)^3)
if bloom_amount > 0.05:
let postfx = get_post_effects()
postfx.bloom = bloom_amount
set_post_effects(postfx)
if bgm_loop_measure ≥ 24 and bgm_loop_measure < 32:
let postfx = get_post_effects()
let t = bgm_measure + bgm_measure_t
let u = (t-24) / 8
postfx.background = #0000
postfx.opacity = 25%
postfx.angle = 0.05*sin(π/2 * t)
if bgm_loop_measure == 31:
postfx.scale = 1 + 0.1*bgm_measure_t^5
set_post_effects(postfx)
// Check for victory conditions
if get_winner(player_scores) ≠ ∅ or (DEBUG_DEV_MODE and joy.ff):
add_frame_hook(at_beat0_enter_gameover, nil, infinity, Play, {})
// handle dpad input
if not disable_input:
for i < PLAYER_COUNT:
if player_types[i] != PLAYER_TYPE_HUMAN:
continue
// any player can move around the board at any time
const new_hex_coord = xy(player_hexes[i].x + gamepad_array[i].xx,
player_hexes[i].y + gamepad_array[i].yy)
if is_valid_board_hex(new_hex_coord):
player_hexes[i] = new_hex_coord;
// Current player can push A to commit their move
if player_types[current_player] == PLAYER_TYPE_HUMAN:
if gamepad_array[current_player].aa:
current_player_move_committed = true
// CPU players enqueue their moves for future frames, so they don't just finish instantly
if player_types[current_player] == PLAYER_TYPE_CPU and cpu_move_sequence == nil:
cpu_move_sequence = sequence(CPU_MOVE_FRAMES/2, move_cpu_player,
(CPU_MOVE_FRAMES+1)/2, commit_cpu_player)
// Handle a newly-committed move
const player_hex = player_hexes[current_player]
if current_player_move_committed:
current_player_move_committed = false
cpu_move_sequence = nil
// Can only flip on a cell you own
if get_cell_owner_hex_coord(player_hex) ≠ current_player:
play_sound(blip_12_sound) // error
invalid_move_message_f = mode_frames
else:
// successful flip
play_sound(PLAYER_CLICK_SOUNDS[current_player])
// change neighboring tiles to be owned by this player
for neighbor < 6:
if (player_current_moves[current_player] bitand (1<<neighbor)) ≠ 0:
const neighbor_hex = player_hex + HEX_COORD_NEIGHBOR_OFFSETS[neighbor]
const arrow_can_capture = (player_current_moves[current_player] bitand (1<<(neighbor+6))) ≠ 0
const neighbor_hex_owner = get_cell_owner_hex_coord(neighbor_hex)
// out-of-bounds hexes can't be flipped
if neighbor_hex_owner == INVALID_HEX:
continue
// Normal arrows can only flip neutral tiles
else if neighbor_hex_owner == PLAYER_NEUTRAL:
set_cell_owner_hex_coord(neighbor_hex, current_player)
player_scores[current_player] += 1
// "super" arrows can flip other player's tiles
else if arrow_can_capture and neighbor_hex_owner != current_player:
player_scores[neighbor_hex_owner] -= 1
set_cell_owner_hex_coord(neighbor_hex, current_player)
player_scores[current_player] += 1
// advance current player
advance_to_next_player()
// draw things
draw_hex_map(map)
draw_gizmos()
preserving_transform:
reset_transform()
const frames_since_error = min(mode_frames - invalid_move_message_f, 1000)
let invalid_move_message_y = 50% SCREEN_SIZE.y + pow(frames_since_error/10, 2)
draw_text({font:font, text:"Select a hex you control!", z:10, color:#f00, shadow:#000,
pos:xy(50% SCREEN_SIZE.x,invalid_move_message_y), x_align:"center", y_align:"top"})
//bgm_draw_debug(10)
for i < PLAYER_COUNT:
if player_types[i] == PLAYER_TYPE_NONE:
continue
// Draw players on map
draw_sprite_on_hex_map(PLAYER_OVERLAY_SPRITES[i], transform_hex_to_map_coord(player_hexes[i]))
// Draw preview hexes for active players
draw_sprite_on_hex_map(PLAYER_TERRITORY_SPRITES[i], PLAYER_PREVIEW_MAP_COORDS[i], 1)
draw_sprite_on_hex_map(hex_sprite.overlay_next, PLAYER_PREVIEW_MAP_COORDS[i], 1)
// Draw arrows around preview hex
let preview_mc = PLAYER_PREVIEW_MAP_COORDS[i]
let preview_hc = transform_map_coord_to_hex(preview_mc)
for neighbor < 6:
if (player_next_moves[i] bitand (1<<neighbor)) ≠ 0:
const arrow_can_capture = (player_next_moves[i] bitand (1<<(neighbor+6))) ≠ 0
const arrow_sprite = ARROW_SOLID_SPRITES[neighbor]
let arrow_hex = preview_hc + HEX_COORD_NEIGHBOR_OFFSETS[neighbor]
let override_color = if arrow_can_capture then hsv((mode_frames mod 72)/72,1,1) else rgba(0,0,0,0)
let override_blend = if arrow_can_capture then "multiply" else "lerp"
draw_sprite_on_hex_map(arrow_sprite.animation.frame(mode_frames),
transform_hex_to_map_coord(arrow_hex), 0, ARROW_SPRITE_SCALES[neighbor], override_color, override_blend)
// Draw current player's move around their hex on the map itself
for neighbor < 6:
if (player_current_moves[current_player] bitand (1<<neighbor)) ≠ 0:
const arrow_hex = player_hexes[current_player] + HEX_COORD_NEIGHBOR_OFFSETS[neighbor]
const arrow_can_capture = (player_current_moves[current_player] bitand (1<<(neighbor+6))) ≠ 0
let arrow_sprite = ARROW_SOLID_SPRITES[neighbor]
let override_color = if arrow_can_capture then hsv((mode_frames mod 72)/72,1,1) else rgba(0,0,0,0)
let override_blend = if arrow_can_capture then "multiply" else lerp
// Replace arrows with outlines if they'd have no effect
const player_hex_owner = get_cell_owner_hex_coord(player_hexes[current_player])
const neighbor_hex = player_hexes[current_player] + HEX_COORD_NEIGHBOR_OFFSETS[neighbor]
const neighbor_hex_owner = get_cell_owner_hex_coord(neighbor_hex)
const not_on_own_cell = player_hex_owner ≠ current_player
const target_is_outside_map = neighbor_hex_owner == INVALID_HEX
const target_is_own_cell = neighbor_hex_owner == current_player
const target_is_other_player_without_capture = neighbor_hex_owner ≠ PLAYER_NEUTRAL and neighbor_hex_owner ≠ current_player and not arrow_can_capture
if not_on_own_cell or target_is_outside_map or target_is_own_cell or target_is_other_player_without_capture:
arrow_sprite = ARROW_OUTLINE_SPRITES[neighbor]
draw_sprite_on_hex_map(arrow_sprite.animation.frame(mode_frames),
transform_hex_to_map_coord(arrow_hex), 0, ARROW_SPRITE_SCALES[neighbor], override_color, override_blend)
leave
────────────────────────────────────────────────────────────────────────
reset_transform()
reset_post_effects()
remove_frame_hooks_by_mode(get_mode())