-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscene.py
More file actions
3090 lines (2923 loc) · 157 KB
/
scene.py
File metadata and controls
3090 lines (2923 loc) · 157 KB
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
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from rolls import *
from loading_screen import loading_screen
from tile_map import *
from entity import *
from euclidean import manhattan_distance, chebyshev_distance
from functional import *
from console import *
import modifiers
from alert_level import AlertLevel
import sort_keys
from random import choice, randint, shuffle, randrange
from unit_tile import *
import time
import heapq
from mission import *
from mini_map import MiniMap
from calendar import Calendar
from pygame.math import Vector2
from camera import *
from sheets import *
class Scene:
def __init__(self, game):
self.speed = GAME_UPDATE_TICK_MS
self.display_changed = True
self.game = game
self.wakes = []
self.alerts = []
self.debug = game.debug
self.log_sim_events = game.log_sim_events
self.log_ai_routines = game.log_ai_routines
self.pathfinding_perf = game.pathfinding_perf
self.screen = game.screen
self.screen_wh_cells_tuple = game.screen_wh_cells_tuple
self.screen_wh_cells_tuple_zoomed_out = game.screen_wh_cells_tuple_zoomed_out
self.tilemap = None
self.camera = Camera((0, 0))
self.entities = []
self.explosions = []
self.console = Console()
self.displaying_hud = True
self.console_scrolled_up_by = 0
self.hud_font = pygame.font.Font(FONT_PATH, HUD_FONT_SIZE)
self.hud_font_bold = pygame.font.Font(BOLD_FONT_PATH, HUD_FONT_SIZE)
self.observation_index = 0
self.time_units_passed = 0
self.processing = False
self.mini_map = None
self.displaying_mini_map = False
self.displaying_briefing_splash = True
self.displaying_big_splash = False
self.hud_swapped = False
self.help_splash = None
self.displaying_help_splash = False
self.paused = True
pygame.time.set_timer(self.game.QUIT_CHECK_RESET_CONFIRM, CONFIRM_RESET_TICK_MS)
def processing_event_dependency_working(self, event_type) -> bool:
dep = first(lambda x: x[0] == event_type, list(self.processing_events.values()))[3]
if dep is not None:
return self.processing_events[dep][2]
return False
def set_processing_event_working(self, name):
self.processing_events[name][2] = True
def set_processing_event_done(self, name):
self.processing_events[name][2] = False
def processing_event_is_working(self, name) -> bool:
return self.processing_events[name][2]
def is_processing_event(self, event_type, name) -> bool:
return name in self.events.keys() and self.events[name][0] == event_type
def get_processing_event_fn(self, event_type):
return first(lambda x: x[0] == event_type, list(self.processing_events.values()))[1]
def get_processing_event_type(self, name):
return self.processing_events[name][0]
def quit_check_reset_confirm(self):
self.game.exit_game_confirm = False
def reset_observation_index(self):
self.observation_index = 0
self.set_processing_event_done("reset_observation_index")
def moved(self) -> bool:
move_key = self.movement_key_pressed()
if move_key and KEY_TO_DIRECTION[move_key] != "wait" and self.shift_pressed():
self.move_camera(KEY_TO_DIRECTION[move_key])
return True
elif move_key and KEY_TO_DIRECTION[move_key] == "wait" and self.ctrl_pressed():
self.camera.set(self.player.xy_tuple)
return True
elif move_key:
self.player.orientation = KEY_TO_DIRECTION[move_key]
return True
return False
def generate_big_splash_txt_surf(self, lines) -> pygame.Surface:
surf = pygame.Surface(BIG_SPLASH_SIZE)
line_height = HUD_FONT_SIZE + 1
width = surf.get_width()
for line in range(len(lines)):
txt = lines[line]
line_surface = self.hud_font.render(txt, True, "white")
x = width // 2 - line_surface.get_width() // 2
surf.blit(line_surface, (x, line * line_height))
pygame.draw.rect(surf, "green", (0, 0, BIG_SPLASH_SIZE[0], BIG_SPLASH_SIZE[1]), 1)
return surf
def escape_pressed(self):
if pygame.key.get_pressed()[K_ESCAPE]:
if isinstance(self, TacticalScene) and self.targeting_ability:
self.reset_target_mode()
self.displaying_mini_map = False
self.display_changed = True
self.displaying_big_splash = False
self.displaying_briefing_splash = False
self.displaying_help_splash = False
if isinstance(self, CampaignScene) and self.game_over:
self.game.running = False
return True
return False
def djikstra_map_distance_to(self, xy_tuple, valid_tile_types=["ocean"], land_buffer=False) -> list:
def fitness_function(tile):
if tile.tile_type not in valid_tile_types:
return INVALID_DJIKSTRA_SCORE
score = manhattan_distance(tile.xy_tuple, xy_tuple)
if land_buffer:
score += self.tilemap.land_buffer_mod(tile.xy_tuple)
return score
djikstra_map = [list(map(lambda tile: fitness_function(tile), col)) for col in self.tilemap.tiles]
return djikstra_map
def update_mini_map(self):
if isinstance(self, CampaignScene):
scene_type = "campaign"
if isinstance(self, TacticalScene):
scene_type = "tactical"
if self.time_units_passed % UPDATE_MINI_MAP_TU_FREQ == 0:
self.mini_map.update(scene_type)
self.set_processing_event_done("update_mini_map")
# Gets the shortest available route
def shortest_path(self, start_loc, end_loc, djikstra_map, valid_tile_types=["ocean"]) -> list:
tiles_searched = 0
traceback_start, traceback_end = None, None
if self.pathfinding_perf:
start = time.process_time_ns()
def print_end_time(start, tiles_searched, found, msg=None):
end = time.process_time_ns()
tot = end - start
if found:
traceback_tot = traceback_end - traceback_start
player_neighbors = list(map(lambda x: x.xy_tuple, self.tilemap.neighbors_of(self.player.xy_tuple)))
player_surrounded = len(list(filter(lambda x: x.xy_tuple in player_neighbors, self.entities))) == 8
start_entity = first(lambda x: x.xy_tuple == start_loc, self.entities)
start_neighbors = list(map(lambda x: x.xy_tuple, self.tilemap.neighbors_of(start_loc)))
start_entity_surrounded = len(list(filter(lambda x: x.xy_tuple in start_neighbors, self.entities))) == 8
if end_loc is not None:
next_to_target = chebyshev_distance(start_loc, end_loc) == 1
distance = manhattan_distance(start_loc, end_loc)
end_entity = first(lambda x: x.xy_tuple == end_loc, self.entities)
print("__profiling shortest_entity_path()")
print("\ttiles_searched: {}".format(tiles_searched))
print("\ttot: {}ns".format(tot))
if found:
print("\tsearch: {}ns".format(traceback_start - start))
print("\ttraceback: {}ns".format(traceback_tot))
if end_loc is not None:
print("\tdistance: {}".format(distance))
print("\tfound: {}".format(found))
if start_entity is not None:
print("\tstart_entity: {}".format(start_entity.name))
print("\tstart_entity_id: {}".format(start_entity.id))
print("\tstart_entity surrounded: {}".format(start_entity_surrounded))
if end_loc is not None and end_entity is not None:
print("\tend_entity: {}".format(end_entity.name))
if end_loc is not None:
print("\tplayer surrounded: {}".format(player_surrounded))
print("\tnext_to_target: {}".format(next_to_target))
if msg is not None:
print("\tmsg: {}".format(msg))
if end_loc is not None:
# edge case (1) of target surrounded and searching entity not among the surrounders
end_neighbors = list(map(lambda x: x.xy_tuple, self.tilemap.neighbors_of(end_loc)))
end_entity_surrounded = len(list(filter(lambda x: x.xy_tuple in end_neighbors, self.entities))) == 8
start_entity = first(lambda x: x.xy_tuple == start_loc, self.entities)
if end_entity_surrounded and start_entity.xy_tuple not in end_neighbors:
if self.pathfinding_perf:
print_end_time(start, tiles_searched, False, msg="edge case (1)")
return None
# edge case (2) of target being next to entity
next_to_target = chebyshev_distance(start_loc, end_loc) == 1
if next_to_target:
if self.pathfinding_perf:
print_end_time(start, tiles_searched, False, msg="edge case (2)")
return None
def get_traceback(ls, goal) -> list:
def loc(node):
return node[2]["loc"]
def via(node):
return node[2]["via"]
nonlocal traceback_start, traceback_end
traceback_start = time.process_time_ns()
current = goal
traceback = [loc(goal)]
while loc(current) is not start_loc:
for node in ls:
if loc(node) == via(current):
if loc(node) is not start_loc:
traceback.append(loc(node))
current = node
break
traceback.reverse()
if self.pathfinding_perf:
traceback_end = time.process_time_ns()
print_end_time(start, tiles_searched, True)
return traceback
def sorted_by_score(ls):
new_ls = []
entry_count = 0
for tile in ls:
x, y = tile.xy_tuple
score = djikstra_map[x][y]
item = [score, entry_count, tile]
entry_count += 1
heapq.heappush(new_ls, item)
return list(map(lambda x: x[2], new_ls))
w, h = self.tilemap.wh_tuple
x0, y0 = start_loc
seen = []
visited = []
seen_bools = [[False for _ in range(h)] for _ in range(w)]
visited_bools = [[False for _ in range(h)] for _ in range(w)]
seen_count = 1
start_score = djikstra_map[x0][y0]
visited_bools[x0][y0] = True
start_node = [start_score, 0, {"loc": start_loc, "via": None}]
heapq.heappush(seen, start_node)
visited.append(start_node)
while len(seen) > 0:
node = heapq.heappop(seen)
x1, y1 = node[2]["loc"]
neighbors = list(filter(lambda x: djikstra_map[x.xy_tuple[0]][x.xy_tuple[1]] != INVALID_DJIKSTRA_SCORE, \
sorted_by_score(self.tilemap.neighbors_of((x1, y1)))))
tile = first(lambda x: visited_bools[x.xy_tuple[0]][x.xy_tuple[1]] == False, neighbors)
if tile is not None:
x2, y2 = tile.xy_tuple
seen_count += 1
tiles_searched += 1
score = djikstra_map[x2][y2]
if tile.occupied:
score = INVALID_DJIKSTRA_SCORE
new_node = {"loc": (x2, y2), "via": (x1, y1)}
full_node = [score, seen_count, new_node]
if not visited_bools[x2][y2]:
visited.append(full_node)
visited_bools[x2][y2] = True
heapq.heappush(seen, full_node)
if end_loc is None:
if is_edge((w, h), (x2, y2)):
return get_traceback(visited, full_node)
else:
if tile.xy_tuple == end_loc:
return get_traceback(visited, full_node)
if any(map(lambda x: visited_bools[x.xy_tuple[0]][x.xy_tuple[1]] == False, neighbors)):
seen.append(node)
return None
def input_blocked(self):
# NOTE: later will also block for some pop-up animations and stuff!
return self.processing
def draw_big_splash(self, surf):
x = self.screen.get_width() // 2 - surf.get_width() // 2
y = 26
self.screen.blit(surf, (x, y))
self.display_changed = True
self.displaying_big_splash = True
def draw_processing_splash(self):
surf = self.hud_font.render("...processing...", True, "green", "black")
y = 26
x = self.screen.get_width() // 2 - surf.get_width() // 2
self.screen.blit(surf, (x, y))
self.display_changed = True
def draw(self):
self.screen.fill("black")
self.draw_level()
self.draw_hud()
self.display_changed = False
no_flip = isinstance(self, TacticalScene) and self.missiles_are_animating
if not no_flip:
pygame.display.flip()
def handle_events(self):
def processing() -> bool:
return any(map(lambda x: x[2], list(self.processing_events.values())))
def tactical_scene_update_tick(event_type) -> bool:
return event.type == self.game.GAME_UPDATE_TICK_TACTICAL \
and isinstance(self, TacticalScene) \
and not self.game.campaign_mode \
and not self.paused \
and not processing()
def campaign_scene_update_tick(event_type) -> bool:
return event.type == self.game.GAME_UPDATE_TICK_CAMPAIGN \
and isinstance(self, CampaignScene) \
and self.game.campaign_mode \
and not self.paused \
and self.turn_ready() \
and not processing()
def launch_processing_events():
for k in self.processing_events.keys():
if self.processing_events[k][3] is None \
or self.processing_event_dependency_working(self.processing_events[k][0]):
self.set_processing_event_working(k)
etype = self.get_processing_event_type(k)
pygame.event.post(pygame.event.Event(etype))
def is_processing_event(event_type) -> bool:
valid = list(map(lambda x: x[0], list(self.processing_events.values())))
return event_type in valid
for event in pygame.event.get():
# quit game:
if event.type == QUIT:
self.game.running = False
# window events
elif event.type == WINDOWFOCUSGAINED:
self.display_changed = True
elif event.type == WINDOWMAXIMIZED:
self.display_changed = True
elif event.type == WINDOWRESTORED:
self.display_changed = True
# Game updates:
elif tactical_scene_update_tick(event.type):
launch_processing_events()
elif campaign_scene_update_tick(event.type):
launch_processing_events()
elif is_processing_event(event.type):
if not self.processing_event_dependency_working(event.type):
fn = self.get_processing_event_fn(event.type)
if self.game.perfing:
self.game.perf_call(fn)
else:
fn()
self.display_changed = True
elif event.type == self.game.MISSION_OVER_CHECK_RESET_CONFIRM and isinstance(self, TacticalScene):
self.mission_over_check_reset_confirm()
elif event.type == self.game.MISSION_OVER_CHECK and isinstance(self, TacticalScene):
self.mission_over_check()
elif event.type == self.game.QUIT_CHECK_RESET_CONFIRM:
self.quit_check_reset_confirm()
# Keyboard Buttons:
elif event.type == KEYDOWN and not self.input_blocked():
self.display_changed = self.keyboard_event_changed_display()
pygame.event.pump()
def update(self):
self.handle_events()
self.update_alerts()
if isinstance(self, TacticalScene) and not self.paused:
self.weapon_update()
if isinstance(self, CampaignScene):
self.encounter_post_check()
def update_alerts(self):
finished = []
for alert in self.alerts:
alert.update()
if alert.complete:
finished.append(alert)
self.alerts = list(filter(lambda x: x not in finished, self.alerts))
def weapon_update(self):
if self.zoomed_out:
cell_size = ZOOMED_OUT_CELL_SIZE
else:
cell_size = CELL_SIZE
for weapon in self.animating_weapons:
target_vec = Vector2((weapon.target.xy_tuple[0] * cell_size, weapon.target.xy_tuple[1] * cell_size))
new_pos = weapon.animation_vector.move_towards(target_vec, weapon.speed)
weapon.animation_vector = new_pos
weapon.update_count += 1
if weapon.update_count > weapon.launch_updates:
weapon.animation_complete = True
if isinstance(weapon, Missile):
pos = (weapon.animation_vector[0] / cell_size, weapon.animation_vector[1] / cell_size)
smoke = Explosion(pos, self.game.smoke_sheet)
self.explosions.append(smoke)
# Moves an entity in a completely random direction
def entity_ai_random_move(self, entity):
if self.log_ai_routines:
print("entity_ai_random_move()")
direction = choice(list(DIRECTIONS.keys()))
self.move_entity(entity, direction)
def relative_direction(self, from_xy, to_xy, opposite=False):
diff = (to_xy[0] - from_xy[0], to_xy[1] - from_xy[1])
if opposite:
diff = tuple(map(lambda x: x * -1, diff))
for k, v in DIRECTIONS.items():
if v == diff:
return k
return "wait"
def keyboard_event_changed_display(self) -> bool:
if isinstance(self, TacticalScene):
if self.mission_over_splash is None and not self.game.campaign_mode:
return self.moved() \
or self.toggled_time_compression() \
or self.swapped_hud() \
or self.toggled_mini_map() \
or self.toggled_briefing() \
or self.console_scrolled() \
or self.escape_pressed() \
or self.cycle_target() \
or self.toggled_zoom() \
or self.fire_at_target() \
or self.toggled_hud() \
or self.used_ability() \
or self.ended_mission() \
or self.help_button_pressed() \
or self.toggled_pause() \
or self.exited_game()
elif self.mission_over_splash is not None and not self.game.campaign_mode:
return self.ended_mission_mode()
elif isinstance(self, CampaignScene):
return self.moved() \
or self.toggled_time_compression() \
or self.swapped_hud() \
or self.toggled_mini_map() \
or self.toggled_briefing() \
or self.console_scrolled() \
or self.escape_pressed() \
or self.cycle_target() \
or self.toggled_hud() \
or self.help_button_pressed() \
or self.toggled_pause() \
or self.exited_game()
def toggled_time_compression(self) -> bool:
changed = False
if pygame.key.get_pressed()[K_t] and self.shift_pressed():
if self.speed == GAME_UPDATE_TICK_MS:
self.speed = GAME_UPDATE_TICK_FAST_MS
changed = True
elif self.speed == GAME_UPDATE_TICK_FAST_MS:
self.speed = GAME_UPDATE_TICK_MS
changed = True
if changed and isinstance(self, CampaignScene):
pygame.time.set_timer(self.game.GAME_UPDATE_TICK_CAMPAIGN, self.speed)
elif changed and isinstance(self, TacticalScene):
pygame.time.set_timer(self.game.GAME_UPDATE_TICK_TACTICAL, self.speed)
return changed
def toggled_pause(self) -> bool:
if pygame.key.get_pressed()[K_p]:
self.paused = not self.paused
self.push_to_console("Paused: {}".format(self.paused))
self.display_changed = True
return True
return False
def toggled_zoom(self) -> bool:
if pygame.key.get_pressed()[K_z]:
self.zoomed_out = not self.zoomed_out
self.display_changed = True
self.push_to_console("Zoomed Out: {}".format(self.zoomed_out))
return True
return False
def help_button_pressed(self) -> bool:
if pygame.key.get_pressed()[K_SLASH] and self.shift_pressed():
self.help_splash = self.generate_big_splash_txt_surf(self.help_lines())
self.displaying_help_splash = True
return True
return False
def help_lines(self) -> list:
if isinstance(self, TacticalScene):
lines = [
"___CONTROLS___", "",
"[h, k, k, l, y, u, b, n, period]: steering", "",
"[shift] + [h, k, k, l, y, u, b, n]: camera movement", "",
"[ctrl + period]: reset camera", "",
"[TAB]: cycle targets in view", "",
"[m]: mini map", "",
"[r]: briefing", "",
"[z]: toggle zoomed in/out", "",
"[p]: pause", "",
"[ctrl + q]: quit game", "",
"[ctrl + e]: end mission", "",
"[ctrl + w]: swap hud placement", "",
"[shift + w]: toggle hud", "",
"[shift + t]: toggle time compression", "",
"[1, 2, f]: torps, missiles, fire", "",
"[9, 0]: sonar ratings, use radar", "",
"[8]: use active sonar", "",
"[7]: toggle normal/fast speed", "",
"[left/right brackets, HOME]: scroll console, reset console", "",
]
elif isinstance(self, CampaignScene):
lines = [
"___CONTROLS___", "",
"[h, k, k, l, y, u, b, n, period]: steering", "",
"[shift] + [h, k, k, l, y, u, b, n]: camera movement", "",
"[ctrl + period]: reset camera", "",
"[TAB]: cycle targets in view", "",
"[m]: mini map", "",
"[p]: pause", "",
"[ctrl + q]: quit game", "",
"[ctrl + w]: swap hud placement", "",
"[shift + w]: toggle hud", "",
"[shift + t]: toggle time compression", "",
"[left/right brackets, HOME]: scroll console, reset console", "",
]
return lines
def exited_game(self) -> bool:
if pygame.key.get_pressed()[K_q] and self.ctrl_pressed():
if self.game.exit_game_confirm:
self.game.running = False
else:
self.game.exit_game_confirm = True
self.push_to_console("Exit game? Press again to confirm.")
return True
return False
def swapped_hud(self) -> bool:
if pygame.key.get_pressed()[K_w] and self.ctrl_pressed():
self.hud_swapped = not self.hud_swapped
return True
return False
def toggled_mini_map(self) -> bool:
if pygame.key.get_pressed()[K_m]:
self.displaying_mini_map = not self.displaying_mini_map
return True
return False
def toggled_briefing(self) -> bool:
if pygame.key.get_pressed()[K_r]:
self.displaying_briefing_splash = not self.displaying_briefing_splash
return True
return False
def toggled_hud(self) -> bool:
if self.shift_pressed() and pygame.key.get_pressed()[K_w]:
self.displaying_hud = not self.displaying_hud
return True
return False
def push_to_console_if_player(self, msg, entities, tag="other"):
if any(filter(lambda x: x.player, entities)):
self.console.push(Message(msg, tag, self.calendar.clock_string()))
self.display_changed = True
def push_to_console(self, msg, tag="other"):
self.console.push(Message(msg, tag, self.calendar.clock_string()))
self.display_changed = True
def console_scrolled(self) -> bool:
valid = False
if pygame.key.get_pressed()[K_RIGHTBRACKET]:
if self.console_scrolled_up_by > 0:
self.console_scrolled_up_by -= 1
valid = True
elif pygame.key.get_pressed()[K_LEFTBRACKET]:
if len(self.console.messages) - (self.console_scrolled_up_by + 1) >= CONSOLE_LINES:
self.console_scrolled_up_by += 1
valid = True
elif pygame.key.get_pressed()[K_HOME]:
self.console_scrolled_up_by = 0
valid = True
return valid
def movement_key_pressed(self): # returns Key constant or False
if pygame.key.get_pressed()[K_h]:
return K_h
elif pygame.key.get_pressed()[K_j]:
return K_j
elif pygame.key.get_pressed()[K_k]:
return K_k
elif pygame.key.get_pressed()[K_l]:
return K_l
elif pygame.key.get_pressed()[K_y]:
return K_y
elif pygame.key.get_pressed()[K_u]:
return K_u
elif pygame.key.get_pressed()[K_b]:
return K_b
elif pygame.key.get_pressed()[K_n]:
return K_n
elif pygame.key.get_pressed()[K_PERIOD]:
return K_PERIOD
return False
def shift_pressed(self) -> bool:
return pygame.key.get_pressed()[K_RSHIFT] or pygame.key.get_pressed()[K_LSHIFT]
def ctrl_pressed(self) -> bool:
return pygame.key.get_pressed()[K_RCTRL] or pygame.key.get_pressed()[K_LCTRL]
def move_camera(self, direction):
target_xy_tuple = (
self.camera.xy_tuple[0] + DIRECTIONS[direction][0],
self.camera.xy_tuple[1] + DIRECTIONS[direction][1]
)
if self.tilemap.tile_in_bounds(target_xy_tuple):
self.camera.set(target_xy_tuple)
def entity_can_move(self, entity, direction) -> bool:
target_xy = (entity.xy_tuple[0] + DIRECTIONS[direction][0], entity.xy_tuple[1] + DIRECTIONS[direction][1])
in_bounds = self.tilemap.tile_in_bounds(target_xy)
if not in_bounds:
return False
tile = self.tilemap.get_tile(target_xy)
ground_tile = tile.tile_type == "land" or tile.tile_type == "city"
if not entity.can_land_move and ground_tile:
return False
occupied = any(map(lambda x: x.xy_tuple == target_xy, self.entities))
return entity.is_mobile() and in_bounds and not occupied
def draw_console(self, tag):
applicable = list(filter(lambda x: x.tag == tag, self.console.messages))
line_height = HUD_FONT_SIZE + 1
num_lines = CONSOLE_LINES
console_width = int(self.screen.get_width() * .33)
console_size = (console_width, line_height * num_lines)
console_surface = pygame.Surface(console_size, flags=SRCALPHA)
console_surface.fill(HUD_OPAQUE_BLACK)
pygame.draw.rect(console_surface, "cyan", (0, 0, console_size[0], console_size[1]), 1)
last = len(applicable) - 1
msgs = []
for line in range(num_lines):
index = last - line - self.console_scrolled_up_by
if index >= 0 and index < len(applicable):
msg = applicable[index]
if msg.tag == tag:
txt = "[{}] {}".format(msg.turn, msg.msg)
msgs.append(txt)
msgs.reverse()
for line in range(len(msgs)):
line_surface = self.hud_font.render(msgs[line], True, "white")
console_surface.blit(line_surface, (0, line * line_height))
if self.hud_swapped:
y = HUD_Y_EGDE_PADDING
else:
y = self.screen.get_height() - line_height * num_lines - 3
if tag == "rolls":
x = 0
elif tag == "combat":
x = console_width + CONSOLE_PADDING
elif tag == "other":
x = (console_width + CONSOLE_PADDING) * 2
self.screen.blit(console_surface, (x, y))
class CampaignScene(Scene):
def __init__(self, game):
super().__init__(game)
self.calendar = Calendar()
self.push_to_console("The war is on, and our first landing will occur soon.")
self.generate_campaign()
self.last_repair_check = 0
self.game_over = False
self.game_over_splash = None
self.extra_lives = 1
self.last_extra_life = 0
self.had_encounter = False
self.accomplished_something = False
self.player_in_mission_zone = False
self.displaying_final_splash = False
self.cratered_tiles = []
self.processing_events = {
"run_entity_behavior": [pygame.event.custom_type(), self.run_entity_behavior, False, None],
"dead_entity_check": [pygame.event.custom_type(), self.dead_entity_check, False, "run_entity_behavior"],
"game_over_check": [pygame.event.custom_type(), self.game_over_check, False, None],
"sim_event_run_warsim": [pygame.event.custom_type(), self.sim_event_run_warsim, False, None],
"sim_event_encounter_check": [pygame.event.custom_type(), self.sim_event_encounter_check, False, \
"run_entity_behavior"],
"sim_event_resupply_check": [pygame.event.custom_type(), self.sim_event_resupply_check, False, \
"run_entity_behavior"],
"sim_event_repair_check": [pygame.event.custom_type(), self.sim_event_repair_check, False, \
"run_entity_behavior"],
"sim_event_extra_lives_check": [pygame.event.custom_type(), self.sim_event_extra_lives_check, False, \
"run_entity_behavior"],
"update_front_lines": [pygame.event.custom_type(), self.update_front_lines, False, "sim_event_run_warsim"],
"small_explosions_check": [pygame.event.custom_type(), self.small_explosions_check, False, None],
"flip_sea_tiles": [pygame.event.custom_type(), self.flip_sea_tiles, False, None],
"update_mini_map": [pygame.event.custom_type(), self.update_mini_map, False, "sim_event_run_warsim"],
"crater_check": [pygame.event.custom_type(), self.crater_check, False, "update_front_lines"],
"wake_check": [pygame.event.custom_type(), self.wake_check, False, "run_entity_behavior"],
}
pygame.time.set_timer(self.game.GAME_UPDATE_TICK_CAMPAIGN, GAME_UPDATE_TICK_MS)
pygame.time.set_timer(self.game.QUIT_CHECK_RESET_CONFIRM, CONFIRM_RESET_TICK_MS)
changed_tiles = [tile for tile in self.tilemap.changed_tiles]
self.master_surface = self.update_master_surface(changed_tiles, reset=True)
self.mini_map = MiniMap(self, "campaign")
def wake_check(self):
if self.time_units_passed % WAKE_CHECK_TU_FREQ == 0:
removed = []
for wake in self.wakes:
if self.time_units_passed > wake.eta:
removed.append(wake)
self.wakes = list(filter(lambda x: x not in removed, self.wakes))
self.set_processing_event_done("wake_check")
def crater_check(self):
if self.time_units_passed % CRATER_CHECK_TU_FREQ == 0:
removed = []
for tile in self.cratered_tiles:
if self.time_units_passed > tile.crater_eta:
tile.crater_index = None
tile.crater_eta = 0
removed.append(tile)
self.cratered_tiles = list(filter(lambda x: x not in removed, self.cratered_tiles))
self.set_processing_event_done("crater_check")
def small_explosions_check(self):
if self.time_units_passed % SMALL_EXPLOSIONS_CHECK_TU_FREQ == 0:
for tile in self.active_front_line_tiles:
if roll_for_front_line_small_explosion():
explosion = Explosion(tile[0], self.game.small_explosions_sheet)
self.explosions.append(explosion)
self.set_processing_event_done("small_explosions_check")
def flip_sea_tiles(self):
if self.time_units_passed % SEA_TILES_TO_FLIP_TU_FREQ == 0:
flippable = list(filter(lambda x: x.tile_type == "ocean", valid_tiles_in_range_of(self.tilemap.tiles, \
self.camera.xy_tuple, self.screen_wh_cells_tuple[0] // 2)))
for flip in range(SEA_TILES_TO_FLIP):
index = randrange(0, len(flippable))
self.tilemap.mark_changed(flippable[index].xy_tuple)
self.set_processing_event_done("flip_sea_tiles")
def update_master_surface(self, changed_tiles, reset=False, clear=False):
if reset:
changed_tiles = self.tilemap.all_tiles()
surf = pygame.Surface((self.tilemap.wh_tuple[0] * CELL_SIZE, self.tilemap.wh_tuple[1] * CELL_SIZE))
else:
if len(changed_tiles) > 0:
surf = self.master_surface.copy()
else:
surf = self.master_surface
w, h = self.tilemap.wh_tuple
for tile in changed_tiles:
x, y = tile.xy_tuple
rect = (x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE)
if tile.tile_type == "ocean":
cell = grab_cell_from_sheet(self.game.open_ocean_sheet, randint(0, 5))
surf.blit(cell, (rect[0], rect[1]))
elif tile.tile_type == "land":
cell = grab_cell_from_sheet(self.game.land_sheet, randint(0, 5))
surf.blit(cell, (rect[0], rect[1]))
elif tile.tile_type == "city":
if tile.faction == "allied":
cell = grab_cell_from_sheet(self.game.allied_cities_sheet, tile.city_index)
elif tile.faction == "enemy":
cell = grab_cell_from_sheet(self.game.enemy_cities_sheet, tile.city_index)
surf.blit(cell, (rect[0], rect[1]))
#pygame.draw.rect(surf, "gray", rect, 1)
if clear:
self.tilemap.changed_tiles = []
if len(changed_tiles) > 0:
self.display_changed = True
return surf
def generate_campaign(self):
self.tilemap = TileMap(CAMPAIGN_MAP_SIZE, "campaign")
self.orientation = self.tilemap.orientation
self.player_origin = self.tilemap.player_origin
self.player = PlayerCampaignEntity(self.player_origin, self.game.debug, \
self.game.entity_sheets[self.game.player_sub_sheet])
self.camera.set(self.player_origin)
self.entities.append(self.player)
self.phase = "island"
self.invasion_target = self.select_invasion_target()
self.invasion_eta = self.get_invasion_eta()
self.briefing_splash = self.generate_big_splash_txt_surf(self.generate_invasion_brief_txt_lines())
self.mission_tiles = self.get_mission_tiles()
self.sea_node_distance_map = self.djikstra_map_distance_to_sea_route_end_node()
for tile in self.tilemap.island_coastal_city_tiles + self.tilemap.mainland_coastal_city_tiles:
path = self.shortest_path(tile.xy_tuple, None, self.sea_node_distance_map)
if path is not None:
for node in path:
tile = self.tilemap.get_tile(node)
tile.sea_route_node = True
for tile in self.tilemap.island_coastal_city_tiles:
home = choice(list(filter(lambda x: x.tile_type == "ocean", \
self.tilemap.neighbors_of(choice(self.tilemap.mainland_coastal_city_tiles).xy_tuple))))
dmap = self.djikstra_map_distance_to(home.xy_tuple)
path = self.shortest_path(tile.xy_tuple, home.xy_tuple, dmap)
for node in path:
tile = self.tilemap.get_tile(node)
tile.logistical_sea_route = True
self.map_traffic_points = self.djikstra_map_sea_traffic()
self.sim_event_place_fleets()
self.active_front_tiles = []
self.next_front_shift = 0
self.map_fronts = self.map_front_lines()
self.active_front_line_tiles = []
def sim_event_place_fleets(self):
x, y = self.invasion_target.xy_tuple
for _ in range(MISSION_ZONE_FLEETS_PER_SIDE):
spots = list(filter(lambda x: x.tile_type == "ocean" and not x.coast and not x.occupied, \
valid_tiles_in_range_of(self.tilemap.tiles, (x, y), MISSION_RADIUS)))
origin = choice(spots).xy_tuple
fleet = AlliedFleet(origin, self.game.entity_sheets[self.game.allied_fleet_sheet])
self.tilemap.toggle_occupied(origin, True)
self.entities.append(fleet)
for _ in range(MISSION_ZONE_FLEETS_PER_SIDE):
spots = list(filter(lambda x: x.tile_type == "ocean" and not x.coast and not x.occupied, \
valid_tiles_in_range_of(self.tilemap.tiles, (x, y), MISSION_RADIUS)))
origin = choice(spots).xy_tuple
fleet = EnemyFleet(origin, self.game.entity_sheets[self.game.enemy_fleet_sheet])
self.tilemap.toggle_occupied(origin, True)
self.entities.append(fleet)
def djikstra_map_sea_traffic(self) -> list:
w, h = self.tilemap.wh_tuple
djikstra_map = []
for x in range(w):
djikstra_map.append([])
for y in range(h):
neighbors = self.tilemap.neighbors_of((x, y))
traffic_points = len(list(filter(lambda z: z.sea_route_node or z.logistical_sea_route, neighbors)))
djikstra_map[x].append(traffic_points)
return djikstra_map
def map_front_lines(self) -> list:
w, h = self.tilemap.wh_tuple
front_map = []
active_front_line_tiles = []
def crater(tile):
tile.crater_index = randrange(0, 6)
tile.crater_eta = self.time_units_passed + randint(CRATER_ETA_RANGE[0], CRATER_ETA_RANGE[1])
if tile not in self.cratered_tiles:
self.cratered_tiles.append(tile)
for x in range(w):
front_map.append([])
for y in range(h):
tile = self.tilemap.get_tile((x, y))
neighbors = self.tilemap.neighbors_of((x, y))
allied_front = tile.faction == "allied" and any(map(lambda x: x.faction == "enemy", neighbors))
enemy_front = tile.faction == "enemy" and any(map(lambda x: x.faction == "allied", neighbors))
if allied_front:
front_map[x].append("allied")
active_front_line_tiles.append(((x, y), "allied"))
tile.front_line_index = randrange(0, 4)
if tile.tile_type == "land":
crater(tile)
elif enemy_front:
front_map[x].append("enemy")
active_front_line_tiles.append(((x, y), "enemy"))
tile.front_line_index = randrange(0, 4)
if tile.tile_type == "land":
crater(tile)
else:
front_map[x].append(False)
self.active_front_line_tiles = active_front_line_tiles
return front_map
def djikstra_map_distance_to_sea_route_end_node(self) -> list:
map_tiles = []
(w, h) = self.tilemap.wh_tuple
for x in range(w):
map_tiles.append([])
for y in range(h):
tile = self.tilemap.get_tile((x, y))
if tile.tile_type != "ocean":
score = INVALID_DJIKSTRA_SCORE
else:
score = self.tilemap.distance_from_edge((x, y)) + self.tilemap.land_buffer_mod((x, y))
map_tiles[x].append(score)
return map_tiles
def get_mission_tiles(self) -> list:
return valid_tiles_in_range_of(self.tilemap.tiles, self.invasion_target.xy_tuple, MISSION_RADIUS)
def get_invasion_eta(self) -> int:
diff = randint(INVASION_ETA_RANGE[0], INVASION_ETA_RANGE[1])
return self.time_units_passed + diff
def generate_invasion_brief_txt_lines(self) -> list:
if self.phase == "island":
phase_line = "We are preparating for an invasion of one of the islands!"
elif self.phase == "mainland":
phase_line = "We are preparing to invade the mainland holdings of the enemy!"
lines = [
"___THEATER BRIEFING___", "", phase_line,
"The location of our attack will be in the viscinity of {}.".format(self.invasion_target.xy_tuple),
"The invasion will commence at around {}.".format(self.calendar.get_eta_str(self.time_units_passed, self.invasion_eta, "campaign")),
"High command requests you patrol the viscinity in order to attack enemy shipping",
"and targets of opportunity. Use your own discretion.", "",
"___NOTES___", "",
"> Your goal is to survive until the campaign is over. You only take the risks",
"you choose to take, but to get a high score you will have to take some.", "",
"> Seek the allied fleets near the mission area if you need more ammunition.",
"You can also resupply at friendly ports, once some are taken.", "",
"> If you take minor damage, find a spot outside the shipping lanes to wait and",
"do some field repairs. You can repair faster at any ports we have taken.", "",
"> The enemy has several heavy escort ships which are extremely dangerous to you.",
"you are best off avoiding them, unless you feel very confident in your approach.", "",
"> The enemy has a large number of escort submarines. Be wary of submerged contacts.", "",
"> When attacking convoys within {} tiles of an enemy-controlled port, be wary of the".format(OFFMAP_ASW_ENCOUNTER_RANGE),
"possibility for ASW patrol planes covering convoy targets.", "",
"> You gain an Extra Life every {} points. Earn points by destroying freighters!".format(EXTRA_LIFE_THRESHOLD), "",
"> Press '?' for controls (NOTE: The game starts paused. 'p' to unpause).", "",
"<ESCAPE to continue>",
]
return lines
def select_invasion_target(self) -> Tile:
if self.phase == "island":
potentials = list(filter(lambda x: x.island and x.coast and x.faction == "enemy", \
self.tilemap.all_tiles()))
if len(potentials) > 0:
sorted_potentials = sorted_by_distance_to(self.player_origin, potentials, manhattan_distance)
pick = sorted_potentials[0][2]
elif len(potentials) == 0:
self.phase = "mainland"
if self.phase == "mainland":
potentials = list(filter(lambda x: x.mainland and x.coast and x.faction == "enemy", \
self.tilemap.all_tiles()))
pick = choice(potentials)
return pick
def sim_event_encounter_check(self):
if self.game.no_encounters == True:
self.set_processing_event_done("sim_event_encounter_check")
return
if self.time_units_passed % ENCOUNTER_CHECK_TU_FREQ == 0:
x, y = self.player.xy_tuple
traffic_points = self.map_traffic_points[x][y]
nearby_enemy_fleets = len(list(filter(lambda z: z.name == "enemy fleet" \
and manhattan_distance((x, y), z.xy_tuple) <= ENEMY_FLEET_ASW_RADIUS, self.entities)))
nearby_enemy_cities = len(list(filter(lambda z: z.tile_type == "city" and z.faction == "enemy", \
valid_tiles_in_range_of(self.tilemap.tiles, (x, y), NEARBY_CITY_ASW_RADIUS, manhattan=True))))
if roll_shipping_encounter(traffic_points) <= 0:
loading_screen(self.game.loader_bg)
self.sim_event_shipping_encounter(traffic_points)
elif roll_asw_encounter(traffic_points, mods=[nearby_enemy_fleets, nearby_enemy_cities]) <= 0:
loading_screen(self.game.loader_bg)
self.sim_event_asw_encounter(traffic_points)
elif self.game.testing_encounter:
self.sim_event_shipping_encounter(4)
self.game.testing_encounter = False
self.set_processing_event_done("sim_event_encounter_check")
def sim_event_asw_encounter(self, traffic_points):
mission = AswPatrol(2, self.encounter_has_offmap_asw(self.player.xy_tuple, traffic_points))
self.game.scene_tactical_combat.generate_encounter(mission, self.calendar)
self.game.current_scene = self.game.scene_tactical_combat
self.game.campaign_mode = False
self.last_repair_check = self.time_units_passed
self.had_encounter = True
def sim_event_shipping_encounter(self, traffic_points):
self.push_to_console("Convoy contact!")
if roll_large_encounter(traffic_points):
scale = 2
else:
scale = 1
mission = ConvoyAttack(scale=scale, \
subs=self.encounter_has_subs(traffic_points), \
heavy_escort=self.encounter_has_heavy_escort(traffic_points), \
offmap_asw=self.encounter_has_offmap_asw(self.player.xy_tuple, traffic_points), \
neutral_freighters=roll_neutrals_encounter(traffic_points))
self.game.scene_tactical_combat.generate_encounter(mission, self.calendar)
self.game.current_scene = self.game.scene_tactical_combat
self.game.campaign_mode = False
self.last_repair_check = self.time_units_passed
self.had_encounter = True
def encounter_has_subs(self, traffic_points) -> bool:
bonus = [modifiers.traffic_points_encounter_mod(traffic_points)]
if self.game.subs > 0:
return roll_sub_encounter(traffic_points, mods=bonus)
return False
def encounter_has_heavy_escort(self, traffic_points) -> bool:
mods = [
modifiers.traffic_points_encounter_mod(traffic_points),
modifiers.heavy_escort_is_rare
]
if self.game.heavy_escorts > 0:
return roll_heavy_escort_encounter(traffic_points, mods=mods)
return False