Repository navigation
Expand file tree
/
Copy pathplanetblast.py
More file actions
6385 lines (5965 loc) · 254 KB
/
Copy pathplanetblast.py
File metadata and controls
6385 lines (5965 loc) · 254 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
# =====================================================================================
# PLANET BLAST — orbital strike tour of a procedural world
#
# Title: "PLANET BLAST" letter zoom, then parallax space → planet approach →
# surface flyover with city targeting, bombing, night vision, and patrol.
#
# Launch:
# LEDsim key 7 / LEDpanel / action "launch_planet" / ?planet
# Idle rotation includes Planet Blast between games.
# =====================================================================================
from __future__ import annotations
import copy
import math
import random
import time
import LEDarcade as LED
LED.Initialize()
try:
import pygame
HAS_PYGAME = True
except Exception:
HAS_PYGAME = False
# Numba: cache=True writes machine code under __pycache__ for reuse across runs.
try:
from numba import njit as _numba_njit
HAS_NUMBA = True
except ImportError:
HAS_NUMBA = False
_numba_njit = None
def _jit(fn):
"""@njit(cache=True) when Numba is available; identity otherwise."""
if HAS_NUMBA:
return _numba_njit(cache=True)(fn)
return fn
_numba_warmed = False
# ---------------- Configuration ----------------
TARGET_FPS = 30
# Flight altitudes (feet) — top-down view; FOV → ground span
_FOV_DEG = 50.0
# City tour: zoom in until city ≈ CITY_TARGET_PX wide, bomb until gone, next
CITY_TARGET_PX = 20.0 # zoom until city footprint is ~this many pixels wide
CITY_WIDE_PX = 4.0 # overview size before zoom-in / after zoom-out
ZOOM_IN_SEC = 2.8 # SCAN → LOCK (was 4.5 — snappier acquire)
ZOOM_OUT_SEC = 4.0
# After a kill: pull out to full globe, orbit, pick next target, dive in
ORBIT_OUT_SEC = 7.0 # smooth climb surface → full planet (no hard cut)
ORBIT_OUT_FLAT_U = 0.55 # fraction of pullback spent as flat map zoom-out
ORBIT_TURN_SEC = 5.5 # rotate around the world before/while locking next tgt
ORBIT_IN_SEC = 7.5 # continuous dive globe → surface → strike alt (no jump)
ORBIT_IN_FLAT_U = 0.38 # fraction of dive spent flattening sphere → map
ORBIT_SPIN_RAD = 1.35 # radians of free spin before target lock
# (Megacity stamp overlays removed — cities only via baked city_paint)
# Night vision: only true night (below twilight). Day + twilight stay normal color.
NV_NIGHT_MAX = 0.20 # day_factor < this → night (NV on)
# After a kill (legacy cruise path kept for first strike / patrol handoff)
NEXT_MIN_SEP_M = 22_000.0 # ignore targets basically under the crater
CRUISE_TO_ARRIVE_M = 52_000.0 # must be this close before zoom-in starts
CRUISE_TO_MPS = 48_000.0 # strike-alt transit (m/s) — doubled for readable scan flight
CRUISE_TO_MAX_SEC = 10.0 # if ETA > this at strike speed → climb + go faster
CRUISE_TO_FAST_MPS = 120_000.0 # high-altitude dash speed cap (m/s)
CRUISE_TO_APPROACH_M = 120_000.0 # begin descent (shorter slow segment than 200 km)
ZOOM_TRACK_MPS = 28_000.0 # reticle fine-track during zoom-in / SCAN
# Alternate batches: 5 night targets, then 5 day, repeat until all gone
TARGETS_PER_BATCH = 5
NIGHT_DAY_MAX = 0.40 # day_factor ≤ this → night target
DAY_DAY_MIN = 0.58 # day_factor ≥ this → day target
# After every N cities bombed: peaceful patrol (no weapons)
PATROL_EVERY = 10 # cities destroyed before patrol
PATROL_SEC = 60.0 # patrol duration
PATROL_LEG_MIN = 5.0 # seconds between patrol waypoints
PATROL_LEG_MAX = 9.0
# Engagement: roll city quota before strike; dice surrender after each kill
SURRENDER_QUOTA_MIN = 2 # min cities WARCOM expects to hit
SURRENDER_QUOTA_MAX = 8 # max engagement target (capped by planet city count)
SURRENDER_BASE_CHANCE = 0.10 # surrender chance after 1st kill
SURRENDER_PER_KILL = 0.12 # added chance per additional kill
SURRENDER_AT_QUOTA = 0.55 # chance when engagement quota met (not forced)
SURRENDER_SHOW_SEC = 3.5 # hold SURRENDER HUD before leaving the world
# Opening: hold full globe with clock + WARCOM chatter, then attack order, then dive
ORBIT_HOLD_SEC = 50.0 # chatter / hold at planet level before WARCOM GO
CRUISE_SEC = 60.0 # legacy surface-cruise duration (patrol ticker fallback)
CRUISE_LEG_MIN = 6.0
CRUISE_LEG_MAX = 10.0
CRUISE_TICKER_PPS = 30.0 # pixels/sec — ~1 px/frame @ TARGET_FPS for smooth marquee
# WARCOM / intel chatter + stats (orbit hold, transit, patrol)
_WARCOM_LINES = (
"WARCOM: HOLD PATTERN",
"WARCOM: WEAPONS COLD",
"WARCOM: AWAIT AUTHORIZATION",
"WARCOM: UPLINK NOMINAL",
"WARCOM: SENSOR SWEEP ACTIVE",
"WARCOM: DO NOT ENGAGE",
"WARCOM: PACKAGE STANDBY",
"WARCOM: CONFIRM ROE",
"WARCOM: EYES ON SURFACE",
"WARCOM: TRACKING THERMALS",
"WARCOM: GRID LOCK PENDING",
"WARCOM: ORBITAL FEED GREEN",
"WARCOM: MAINTAIN HEADING",
"WARCOM: NO FRIENDLIES MARKED",
"WARCOM: CIVILIAN MASK HIGH",
"WARCOM: ECM QUIET",
"WARCOM: BIRDS IN THE PIPE",
"WARCOM: STANDBY FOR TASKING",
"WARCOM: PRIORITY TARGET LIST LOADING",
"WARCOM: ATMOS ENTRY COMPLETE",
"WARCOM: ALT HOLD",
"WARCOM: COMM CHECK 1 2",
"WARCOM: STRIKE WINDOW OPENING",
"WARCOM: WATCH THE THERMALS",
"WARCOM: CITY LIGHTS CONFIRMED",
"WARCOM: NIGHT SIDE APPROACH",
"WARCOM: DAY SIDE CLEAR",
"INTEL: POPULATION DENSITY HIGH",
"INTEL: INFRA GRID ONLINE",
"INTEL: POWER NODES ACTIVE",
"INTEL: TRANSIT LINKS LIVE",
"INTEL: UNKNOWN AA SIGNATURE",
"INTEL: FOREST FIRE RISK ELEVATED",
"INTEL: COASTAL CITIES LIT",
"INTEL: MEGA HUB IDENTIFIED",
"INTEL: RURAL OUTPOSTS SPARSE",
"INTEL: RIVER CROSSINGS MAPPED",
"INTEL: HIGHWAY SPINE DETECTED",
"INTEL: RAIL CORRIDOR HOT",
"PILOT: VISUAL ON SURFACE",
"PILOT: CRUISE NOMINAL",
"PILOT: FUEL STATE GOOD",
"PILOT: CAM FEED CLEAN",
"PILOT: REQUESTING NEXT WAYPOINT",
"PILOT: HOLDING PATTERN",
"PILOT: CROSSWIND LIGHT",
"SYS: NAV FIX OK",
"SYS: HUD SYNC",
"SYS: TICKER BUFFER FULL",
"SYS: LINK 16 RELAY",
"SYS: RECORDING PASS",
"SYS: TELEMETRY NOMINAL",
"ALERT: THERMAL SPIKE EAST",
"ALERT: SMOKE COLUMN SIGHTED",
"ALERT: POWER FLICKER IN GRID 7",
"ALERT: UNKNOWN CONTACT LOST",
"ALERT: WEATHER FRONT MOVING",
"NOTE: REMEMBER THE ROE",
"NOTE: CITIES FIRST",
"NOTE: MINIMIZE COLLATERAL",
"NOTE: LOG ALL IMPACTS",
"NOTE: FIRE SPREAD POSSIBLE",
"CHATTER: ANOTHER QUIET PASS",
"CHATTER: WORLD LOOKS ALIVE",
"CHATTER: TOO MANY LIGHTS",
"CHATTER: THEY DONT KNOW YET",
"CHATTER: CLOCK IS TICKING",
"CHATTER: PRETTY FROM UP HERE",
"CHATTER: DONT GET COMFORTABLE",
"CHATTER: WARCOM IS LISTENING",
)
# End-of-cruise urgent attack orders ({world} filled at runtime)
CRUISE_ORDER_ALERT_SEC = 0.45 # red ALERT shows before scroll starts
CRUISE_ORDER_END_HOLD = 0.55 # hold after message finishes scrolling
_WARCOM_ATTACK_ORDERS = (
"WARCOM URGENT: ATTACK {world}",
"WARCOM FLASH: WEAPONS FREE — {world}",
"WARCOM PRIORITY: ENGAGE ALL CITIES ON {world}",
"WARCOM ORDER: BEGIN ORBITAL STRIKE ON {world}",
"WARCOM URGENT: AUTHORIZE FULL PACKAGE — {world}",
"WARCOM FLASH: GO HOT ON {world} NOW",
"WARCOM ORDER: DESTROY SURFACE TARGETS — {world}",
"WARCOM URGENT: STRIKE WINDOW OPEN — ATTACK {world}",
"WARCOM FLASH: NO DELAY — BOMB {world}",
"WARCOM PRIORITY: EXECUTE TASKING ON {world}",
"WARCOM ORDER: WEAPONS RELEASE AUTHORIZED — {world}",
"WARCOM URGENT: PLANET {world} IS HOSTILE — ATTACK",
"WARCOM FLASH: CLEAR TO ENGAGE — {world}",
"WARCOM ORDER: COMMENCE BOMBING RUNS ON {world}",
"WARCOM URGENT: ALL FLIGHTS — ATTACK {world}",
"WARCOM FLASH: ROE UPDATED — DESTROY CITIES ON {world}",
"WARCOM PRIORITY: LIGHT THEM UP — {world}",
"WARCOM ORDER: TARGET MEGACITIES FIRST — {world}",
"WARCOM URGENT: BREAK CRUISE — ATTACK {world}",
"WARCOM FLASH: MISSION IS GO ON {world}",
"WARCOM ORDER: RAIN FIRE ON {world}",
"WARCOM URGENT: SURFACE STRIKE AUTHORIZED — {world}",
"WARCOM FLASH: DO NOT HOLD — ATTACK {world}",
"WARCOM PRIORITY: LEVEL THE GRID ON {world}",
"WARCOM ORDER: OPEN FIRE ON {world}",
"WARCOM URGENT: PACKAGE AWAY WHEN READY — {world}",
"WARCOM FLASH: WARCOM SAYS ATTACK {world}",
"WARCOM ORDER: END THE QUIET — STRIKE {world}",
"WARCOM URGENT: HOSTILE WORLD {world} — ENGAGE",
"WARCOM FLASH: CLOCK ZERO — ATTACK {world}",
"WARCOM PRIORITY: BURN THE SPINES OF {world}",
"WARCOM ORDER: ALL BIRDS GO — {world}",
"WARCOM URGENT: NO MORE HOLD — DESTROY {world}",
"WARCOM FLASH: EXECUTE ORBITAL DOCTRINE ON {world}",
"WARCOM ORDER: CITIES ARE THE PRIORITY — {world}",
"WARCOM URGENT: YOU ARE CLEARED HOT ON {world}",
"WARCOM FLASH: WAR PLAN ALPHA — ATTACK {world}",
"WARCOM PRIORITY: LEAVE NOTHING STANDING ON {world}",
"WARCOM ORDER: BEGIN GLASSING RUNS — {world}",
"WARCOM URGENT: TARGET LIST LIVE — ATTACK {world}",
)
# Bombing run
BOMB_DROP_INTERVAL = 0.85 # seconds between bombs
BOMB_FALL_SEC = 1.15 # hang time while falling (shrinks)
BOMB_AIM_SPREAD = 0.4125 # aim scatter as fraction of city radius (−25% error)
BOMB_AIM_LONGSHOT = 0.165 # chance of a long miss beyond normal spread (−25%)
BOMB_DAMAGE = 0.14 # damage per hit near center (size scales)
SMOKE_RING_SEC = 2.4 # slow expanding smoke ring
FIRE_SEC = 1.8
CROSSHAIR_UP_PX = 4 # reticle is panel-center, this many px up
# Camera track error while bombing (city under reticle, imperfect)
CAM_AIM_ERR_M = 24_000.0 # max wander from ideal city lock (m) (−25%)
CAM_AIM_WANDER = 0.41 # how fast aim error drifts (−25%)
CROSSHAIR_LOCK_RGB = (255, 30, 20) # red when weapons free / locked
CROSSHAIR_ACQ_LO = (12, 70, 28) # dark green (targeting pulse low)
CROSSHAIR_ACQ_HI = (40, 255, 90) # bright green (targeting pulse high)
BOMB_RGB = (48, 12, 72) # dark purple shell
BOMB_SHELL_HI = (95, 35, 130) # muted purple highlight
BOMB_CORE_RGB = (255, 30, 40) # bright red core
SMOKE_RGB = (220, 218, 215) # default pale white smoke
FIRE_RGB = (255, 40, 15)
FIRE_CORE = (255, 200, 40)
RUBBLE_RGB = (55, 48, 42)
ASH_RGB = (42, 36, 30) # scorched forest floor (map paint)
# Fire/ember colors stamped onto the map (varying mix — not a separate layer)
_FIRE_PAINT_COLORS = (
(255, 48, 12), # hot red
(230, 60, 18), # red-orange
(200, 55, 22), # deep ember
(160, 42, 20), # dark ember
(110, 48, 28), # charred brown
(70, 40, 28), # near-ash
(48, 36, 28), # ash
(255, 90, 25), # bright edge
(180, 70, 30), # warm coal
)
# Globe / far view: soft cities (less flicker) + optional 1px pins
GLOBE_CITY_PIN_MIN_SIZE = 3 # stamp stable 1px for cities this size and up
GLOBE_CITY_PIN_MPP = 6_500.0 # only pin when mpp above this (far / full planet)
# Cities + fire share one paint map: stamped once, overwritten by bombs/spread
FIRE_CELL = 8_000.0 # meters per paint cell (coarser → fewer embers)
FIRE_TICK_SEC = 12.0 # slower crawl between spread ticks
FIRE_SPREAD_PER_TICK = 4 # fewer new green cells per tick
FIRE_MAX_CELLS = 3_500 # hard cap on burned cells
FIRE_SEED_DMG = 0.18 # need a bit more damage before wildfire seeds
FIRE_IMPACT_R_CELLS = 1 # impact seed radius in cells (diameter ~1–2)
FIRE_CITY_RING_SCALE = 0.55 # city ring radii vs urban footprint (smaller patches)
# If engagement quota is hit and planet still fights: faster wildfire
FIRE_RAGE_TICK_SEC = 7.0 # shorter interval (still calmer than old rage)
FIRE_RAGE_SPREAD_PER_TICK = 7 # moderate boost when planet refuses to fold
# Night vision (when dark): mono green; hot explosions → black
NV_DAY_THRESH = 0.38 # day_factor below this → NV on
NV_GAIN = 2.15 # amplify surface contrast so terrain reads
NV_FLOOR = 18.0 # pedestal so oceans/land stay visible
NV_SURF_DAY = 0.78 # sample surface lit enough to show biome detail
# HUD — 3×5 micro font (teeny but readable on LED panels)
# HUD text colors at 80% brightness (20% dimmer than full panel primaries)
HUD_RGB = (32, 204, 72) # targeting / scan green
HUD_FIRE_RGB = (204, 160, 32) # firing amber
HUD_ALERT_RGB = (204, 40, 32) # destroyed red
HUD_DIM_RGB = (16, 112, 56)
HUD_NAME_RGB = (144, 204, 160) # acquired city name
# Alien-but-readable English city name parts
_NAME_ONSET = (
"Zor", "Kel", "Vyn", "Ash", "Tor", "Xel", "Mir", "Qan", "Dra", "Nex",
"Syl", "Vor", "Lun", "Pha", "Ryn", "Thal", "Bry", "Kor", "Jex", "Wyn",
"Sael", "Orn", "Vesk", "Nyx", "Cra", "Hel", "Isk", "Ul", "Pra", "Ghy",
)
_NAME_CORE = (
"a", "e", "i", "o", "u", "ae", "ia", "io", "ou", "y", "ei", "au",
)
_NAME_CODA = (
"thar", "voss", "dria", "nox", "veil", "spire", "reach", "holm", "gate",
"mere", "strand", "fall", "wick", "borne", "crest", "shade", "march",
"haven", "forge", "rift", "vale", "watch", "point", "ridge", "delta",
)
_NAME_TITLE = (
"", "", "", "Prime", "Minor", "Deep", "High", "Outer", "New", "Old",
)
# 50 named worlds (star-system ordinals: Vulcanis III, Tantr X, …)
_PLANET_NAMES = (
"Vulcanis III", "Tantr X", "Keldor IV", "Nyxara VII", "Ashmere II",
"Xelion IX", "Miros V", "Thalos VIII", "Zoran Prime", "Saelith VI",
"Ornax III", "Dravak XII", "Lunaris IV", "Pharos XI", "Rynos II",
"Veskara IX", "Helion VII", "Iskan V", "Praxis III", "Qantor VIII",
"Brynnos IV", "Corvex VI", "Deneb Minor", "Eryndor X", "Fomal X",
"Ganymar III", "Hyperion IX", "Iridon II", "Jotun IV", "Krynn VII",
"Lothor V", "Manticore III", "Nexara VIII", "Oberon VI", "Phaeton IX",
"Quoros II", "Rigel Minor", "Solara IV", "Titanos VII", "Umbra III",
"Vortex V", "Weyland IX", "Xandar II", "Ymir IV", "Zephyros VIII",
"Andaros VI", "Boreas III", "Cygnus IX", "Delphos IV", "Erebus VII",
)
# 3 wide × 5 tall bit rows (MSB left). Space = empty.
_HUD_GLYPHS = {
" ": (0, 0, 0, 0, 0),
"A": (0b010, 0b101, 0b111, 0b101, 0b101),
"B": (0b110, 0b101, 0b110, 0b101, 0b110),
"C": (0b011, 0b100, 0b100, 0b100, 0b011),
"D": (0b110, 0b101, 0b101, 0b101, 0b110),
"E": (0b111, 0b100, 0b110, 0b100, 0b111),
"F": (0b111, 0b100, 0b110, 0b100, 0b100),
"G": (0b011, 0b100, 0b101, 0b101, 0b011),
"H": (0b101, 0b101, 0b111, 0b101, 0b101),
"I": (0b111, 0b010, 0b010, 0b010, 0b111),
"J": (0b001, 0b001, 0b001, 0b101, 0b010),
"K": (0b101, 0b101, 0b110, 0b101, 0b101),
"L": (0b100, 0b100, 0b100, 0b100, 0b111),
"M": (0b101, 0b111, 0b111, 0b101, 0b101),
"N": (0b101, 0b111, 0b111, 0b111, 0b101),
"O": (0b010, 0b101, 0b101, 0b101, 0b010),
"P": (0b110, 0b101, 0b110, 0b100, 0b100),
"Q": (0b010, 0b101, 0b101, 0b111, 0b011),
"R": (0b110, 0b101, 0b110, 0b101, 0b101),
"S": (0b011, 0b100, 0b010, 0b001, 0b110),
"T": (0b111, 0b010, 0b010, 0b010, 0b010),
"U": (0b101, 0b101, 0b101, 0b101, 0b111),
"V": (0b101, 0b101, 0b101, 0b101, 0b010),
"W": (0b101, 0b101, 0b111, 0b111, 0b101),
"X": (0b101, 0b101, 0b010, 0b101, 0b101),
"Y": (0b101, 0b101, 0b010, 0b010, 0b010),
"Z": (0b111, 0b001, 0b010, 0b100, 0b111),
"0": (0b111, 0b101, 0b101, 0b101, 0b111),
"1": (0b010, 0b110, 0b010, 0b010, 0b111),
"2": (0b111, 0b001, 0b111, 0b100, 0b111),
"3": (0b111, 0b001, 0b111, 0b001, 0b111),
"4": (0b101, 0b101, 0b111, 0b001, 0b001),
"5": (0b111, 0b100, 0b111, 0b001, 0b111),
"6": (0b111, 0b100, 0b111, 0b101, 0b111),
"7": (0b111, 0b001, 0b010, 0b010, 0b010),
"8": (0b111, 0b101, 0b111, 0b101, 0b111),
"9": (0b111, 0b101, 0b111, 0b001, 0b111),
".": (0b000, 0b000, 0b000, 0b000, 0b010),
":": (0b000, 0b010, 0b000, 0b010, 0b000),
"-": (0b000, 0b000, 0b111, 0b000, 0b000),
"!": (0b010, 0b010, 0b010, 0b000, 0b010),
"?": (0b111, 0b001, 0b010, 0b000, 0b010),
"/": (0b001, 0b001, 0b010, 0b100, 0b100),
">": (0b100, 0b010, 0b001, 0b010, 0b100),
"*": (0b101, 0b010, 0b111, 0b010, 0b101),
}
_HUD_CHAR_W = 3
_HUD_CHAR_H = 5
_HUD_GAP = 1 # 1px between letters
# Ground speed as fraction of visible span per second (scroll feel)
CRUISE_SPAN_PER_SEC = 0.18
TURN_PERIOD = 22.0
LEG_MIN = 6.0
LEG_MAX = 12.0
# Noise scales CONTINENT_SCALE / DETAIL_SCALE / … defined with planet map constants
# ---- Space intro (parallax stars → clock → fly-through → planet zoom) ----
STAR_DRIFT_SEC = 3.0 # pure starfield before the clock appears
SPACE_CLOCK_SEC = 30.0 # floating digital clock in space before planet
SPACE_CLOCK_FLY_SEC = 2.8 # fly-through: clock scales up and we pass through
PLANET_DOT_SEC = 2.2 # bright planet dot after clock fly-through
PLANET_ZOOM_SEC = 5.5 # approach / grow the planet disc to large globe
BRIEFING_HOLD_SEC = 1.2 # brief pause after terminal finishes typing
PLANET_DESCENT_SEC = 0.0 # intro ends at full globe; camera owns continuous dive
# First approach dive (globe → flat → strike) reuses ORBIT_IN_*; clock/chatter on
# Large retro digital clock — 5×9 hairline digits (1px strokes, open counters)
_CLOCK_DIGIT_W = 5
_CLOCK_DIGIT_H = 9
_CLOCK_DIGIT_GAP = 1
_CLOCK_RGB = (40, 220, 180) # teal CRT / retro digital
# 5-wide bit rows (MSB left) — single-pixel stroke skeleton
_CLOCK_DIGITS = {
"0": (
0b01110,
0b10001,
0b10001,
0b10001,
0b10001,
0b10001,
0b10001,
0b10001,
0b01110,
),
"1": (
0b00100,
0b01100,
0b00100,
0b00100,
0b00100,
0b00100,
0b00100,
0b00100,
0b01110,
),
"2": (
0b01110,
0b10001,
0b00001,
0b00001,
0b00010,
0b00100,
0b01000,
0b10000,
0b11111,
),
"3": (
0b01110,
0b10001,
0b00001,
0b00001,
0b00110,
0b00001,
0b00001,
0b10001,
0b01110,
),
"4": (
0b00010,
0b00110,
0b01010,
0b10010,
0b11111,
0b00010,
0b00010,
0b00010,
0b00010,
),
"5": (
0b11111,
0b10000,
0b10000,
0b11110,
0b00001,
0b00001,
0b00001,
0b10001,
0b01110,
),
"6": (
0b00110,
0b01000,
0b10000,
0b11110,
0b10001,
0b10001,
0b10001,
0b10001,
0b01110,
),
"7": (
0b11111,
0b00001,
0b00010,
0b00010,
0b00100,
0b00100,
0b01000,
0b01000,
0b01000,
),
"8": (
0b01110,
0b10001,
0b10001,
0b10001,
0b01110,
0b10001,
0b10001,
0b10001,
0b01110,
),
"9": (
0b01110,
0b10001,
0b10001,
0b10001,
0b01111,
0b00001,
0b00001,
0b00010,
0b01100,
),
":": (
0b00000,
0b00100,
0b00100,
0b00000,
0b00000,
0b00000,
0b00100,
0b00100,
0b00000,
),
}
# Empire news ticker during space clock (built once, ≥100 items)
# Frame-locked 1 px per update — matches TARGET_FPS for even LED marquee motion
SPACE_NEWS_PPS = float(TARGET_FPS)
TERM_CPS = 14.0 # approach dossier typewriter chars/sec
TERM_LINE_GAP = 1 # px between terminal rows
TERM_SCROLL_SEC = 0.30 # smooth line-up: one row height over this many seconds
# Random dossier STATUS: lines (picked once per approach)
TERM_STATUSES = (
"PENDING",
"GUILTY",
"ENDANGERED",
"INDICTED",
"WANTED",
"HOSTILE",
"QUARANTINED",
"CONDEMNED",
"AT LARGE",
"SENTENCED",
"HIGH PRIORITY",
"UNDER REVIEW",
"EXTERMINATUS PENDING",
"CLEARED FOR STRIKE",
"ARMED RESPONSE",
"NO SURRENDER",
"THREAT LEVEL RED",
"ORBITAL AUTHORIZED",
)
# Terminal starts only once the disc is a large globe (end of zoom)
STAR_DIM = 0.70 # match Space Explorer dimming feel
# Oversized wrap layers (Space Explorer style) — tall for vertical scroll
SPACE_LAYER_W = 160
SPACE_LAYER_H = 240
# Scroll rate divisors: higher = slower layer (Defender2 / SpaceExplorer)
# Foreground scrolls fastest (rate 3), far stars slowest (rate 14)
SPACE_FAR_RATE = 14.0
SPACE_BG_RATE = 8.0
SPACE_MID_RATE = 5.0
SPACE_FG_RATE = 3.0
# Base downward world speed (layer-px per second at rate=1 equivalent)
SPACE_SCROLL_V = 48.0
def _stop(StopEvent):
try:
return StopEvent is not None and StopEvent.is_set()
except Exception:
return False
def _clamp(v, lo, hi):
return lo if v < lo else hi if v > hi else v
def _lerp(a, b, t):
return a + (b - a) * t
def _smoothstep(t):
t = _clamp(t, 0.0, 1.0)
return t * t * (3.0 - 2.0 * t)
def _lerp_rgb(c0, c1, t):
t = _clamp(t, 0.0, 1.0)
return (
int(c0[0] + (c1[0] - c0[0]) * t),
int(c0[1] + (c1[1] - c0[1]) * t),
int(c0[2] + (c1[2] - c0[2]) * t),
)
# ---------------- Shared planet surface map (constants before Numba kernels) ---
# World coords are meters on an equirectangular unwrap; same map for globe + flyover.
PLANET_R = 2_000_000.0 # baseline map scale: lon/lat * R → meters (LARGEST)
# Per-planet radius multiplier — always strictly below baseline (1.0×)
# Log-uniform in [MIN, MAX]; baseline 1.0× is never rolled.
PLANET_SIZE_MIN = 0.22 # dwarf / moonlet
PLANET_SIZE_MAX = 0.98 # near-baseline (still smaller than 1.0×)
# Elevation threshold: ~75% of the surface is ocean (land only above this)
SEA_LEVEL = 0.58
CITY_CELL = 80_000.0 # spatial hash cell for cities (m)
CITY_COUNT_MIN = 10
CITY_COUNT_MAX = 50 # hard cap — fewer cities, no road network
CITY_DAY = (105, 105, 112) # readable gray urban mass from altitude
CITY_DAY_CORE = (130, 128, 125) # denser core
CITY_NIGHT = (255, 200, 90)
CITY_GLOW = (255, 140, 40)
# Noise scales (also used as module globals inside @njit)
CONTINENT_SCALE = 1.0 / 720_000.0 # broad continents
DETAIL_SCALE = 1.0 / 70_000.0
RIVER_SCALE = 1.0 / 32_000.0
MOUNTAIN_SCALE = 1.0 / 110_000.0
# ---------------- Hash noise + terrain (Numba @njit cache=True) ----------------
# Hot path: every surface pixel samples elev via FBM.
# cache=True → machine code saved under __pycache__; reloads next process.
_INV_I31 = 1.0 / 2147483647.0
@_jit
def _n_clamp(v, lo, hi):
if v < lo:
return lo
if v > hi:
return hi
return v
@_jit
def _n_smoothstep(t):
if t < 0.0:
t = 0.0
elif t > 1.0:
t = 1.0
return t * t * (3.0 - 2.0 * t)
@_jit
def _n_smooth_edge(v, a, b):
if b <= a:
return 1.0 if v >= a else 0.0
return _n_smoothstep((v - a) / (b - a))
@_jit
def _hash2(ix, iy, seed):
"""Deterministic 0..1 hash of integer lattice + seed."""
n = (ix * 374761393 + iy * 668265263 + seed * 1274126177) & 0x7FFFFFFF
n = (n ^ (n >> 13)) * 1274126177
n = (n ^ (n >> 16)) & 0x7FFFFFFF
return n * _INV_I31
@_jit
def _value_noise(x, y, seed):
"""Bilinear value noise; x,y continuous. Hash corners inlined."""
x0 = int(math.floor(x))
y0 = int(math.floor(y))
fx = x - x0
fy = y - y0
ux = fx * fx * (3.0 - 2.0 * fx)
uy = fy * fy * (3.0 - 2.0 * fy)
s = seed
n = (x0 * 374761393 + y0 * 668265263 + s * 1274126177) & 0x7FFFFFFF
n = (n ^ (n >> 13)) * 1274126177
v00 = ((n ^ (n >> 16)) & 0x7FFFFFFF) * _INV_I31
n = ((x0 + 1) * 374761393 + y0 * 668265263 + s * 1274126177) & 0x7FFFFFFF
n = (n ^ (n >> 13)) * 1274126177
v10 = ((n ^ (n >> 16)) & 0x7FFFFFFF) * _INV_I31
n = (x0 * 374761393 + (y0 + 1) * 668265263 + s * 1274126177) & 0x7FFFFFFF
n = (n ^ (n >> 13)) * 1274126177
v01 = ((n ^ (n >> 16)) & 0x7FFFFFFF) * _INV_I31
n = ((x0 + 1) * 374761393 + (y0 + 1) * 668265263 + s * 1274126177) & 0x7FFFFFFF
n = (n ^ (n >> 13)) * 1274126177
v11 = ((n ^ (n >> 16)) & 0x7FFFFFFF) * _INV_I31
a = v00 + (v10 - v00) * ux
b = v01 + (v11 - v01) * ux
return a + (b - a) * uy
@_jit
def _fbm(x, y, seed, octaves, lacunarity, gain):
amp = 1.0
freq = 1.0
total = 0.0
norm = 0.0
for i in range(octaves):
total += amp * _value_noise(x * freq, y * freq, seed + i * 1013)
norm += amp
amp *= gain
freq *= lacunarity
if norm < 1e-9:
return 0.0
return total / norm
@_jit
def _fbm_fast(x, y, seed, octaves):
"""
Cheap FBM for realtime surface (fixed lacunarity 2, gain 0.5).
Unrolled for 1–3 octaves — the elev hot path.
"""
if octaves <= 1:
return _value_noise(x, y, seed)
v = _value_noise(x, y, seed)
v2 = _value_noise(x * 2.0, y * 2.0, seed + 1013)
if octaves == 2:
return (v + 0.5 * v2) * (1.0 / 1.5)
v3 = _value_noise(x * 4.0, y * 4.0, seed + 2026)
return (v + 0.5 * v2 + 0.25 * v3) * (1.0 / 1.75)
@_jit
def _elev_raw_n(wx, wy, seed, lod):
"""
Elevation + continent/detail/mountain.
lod 0: 1 octave continent only; lod 1–2: 2+1+1 octaves.
Returns (elev, c, d, mountain).
"""
if lod <= 0:
c = _fbm_fast(wx * CONTINENT_SCALE, wy * CONTINENT_SCALE, seed, 1)
if c < 0.0:
c = 0.0
elif c > 1.0:
c = 1.0
return c, c, 0.5, 0.0
c = _fbm_fast(wx * CONTINENT_SCALE, wy * CONTINENT_SCALE, seed, 2)
d = _fbm_fast(wx * DETAIL_SCALE, wy * DETAIL_SCALE, seed + 7, 1)
m_raw = _fbm_fast(wx * MOUNTAIN_SCALE, wy * MOUNTAIN_SCALE, seed + 19, 1)
mountain = 1.0 - abs(2.0 * m_raw - 1.0)
mountain *= mountain
elev = c * 0.72 + d * 0.14 + mountain * 0.14
if elev < 0.0:
elev = 0.0
elif elev > 1.0:
elev = 1.0
return elev, c, d, mountain
@_jit
def _is_river_n(wx, wy, elev, moist, seed):
river_n = _value_noise(wx * RIVER_SCALE, wy * RIVER_SCALE, seed + 55)
river_ridge = 1.0 - abs(2.0 * river_n - 1.0)
river_w = _n_smooth_edge(river_ridge, 0.84, 0.96)
river_ok = _n_smooth_edge(elev, SEA_LEVEL + 0.01, SEA_LEVEL + 0.04)
river_ok *= 1.0 - _n_smooth_edge(elev, 0.68, 0.78)
river_ok *= _n_smooth_edge(moist, 0.28, 0.45)
return river_w * river_ok
@_jit
def _sample_biome_n(wx, wy, seed, lod):
"""
Daytime surface color + meta.
Returns (r, g, b, elev, river_amt, land_flag) with land_flag 1.0 or 0.0.
"""
elev, c, d, mountain = _elev_raw_n(wx, wy, seed, lod)
if lod <= 0:
moist = 0.45
else:
moist_oct = 1 if lod < 2 else 2
moist = _fbm_fast(
wx * DETAIL_SCALE * 0.7, wy * DETAIL_SCALE * 0.7,
seed + 31, moist_oct,
)
lat_norm = wy / (PLANET_R * (math.pi * 0.5))
if lat_norm < -1.0:
lat_norm = -1.0
elif lat_norm > 1.0:
lat_norm = 1.0
river_amt = 0.0
land = 0.0
if elev < SEA_LEVEL:
# Oceans: deep navy → mid cobalt → thin shallow shelf (high contrast)
# deep abyss (low elev)
t_deep = _n_smooth_edge(elev, SEA_LEVEL - 0.16, SEA_LEVEL - 0.05)
r = 2.0 + (6.0 - 2.0) * t_deep
g = 12.0 + (32.0 - 12.0) * t_deep
b = 55.0 + (110.0 - 55.0) * t_deep
# shallow shelf — still blue, not teal/cyan wash
t_shal = _n_smooth_edge(elev, SEA_LEVEL - 0.055, SEA_LEVEL - 0.008)
r = r + (18.0 - r) * t_shal
g = g + (70.0 - g) * t_shal
b = b + (145.0 - b) * t_shal
if lod >= 1:
# thin shore line only — less foam so blue stays deep
t_foam = _n_smooth_edge(elev, SEA_LEVEL - 0.012, SEA_LEVEL) * 0.14
r = r + (90.0 - r) * t_foam
g = g + (130.0 - g) * t_foam
b = b + (155.0 - b) * t_foam
land = 0.0
else:
land = 1.0
denom = 1.0 - SEA_LEVEL
if denom < 1e-6:
denom = 1e-6
land_h = (elev - SEA_LEVEL) / denom
# desert (warm sand/ochre) vs grass/forest (saturated green) — push apart
# sand / desert: yellower, less green mud
dr = 195.0 + (225.0 - 195.0) * land_h
dg = 145.0 + (165.0 - 145.0) * land_h
db = 55.0 + (70.0 - 55.0) * land_h
# grass: deeper emerald, less brown
gt = land_h * 0.45 + moist * 0.4
gr = 28.0 + (55.0 - 28.0) * gt
gg = 105.0 + (155.0 - 105.0) * gt
gb = 28.0 + (42.0 - 28.0) * gt
# forest: dark green canopy
ft = 0.35 + 0.45 * land_h
fr = 10.0 + (22.0 - 10.0) * ft
fg = 70.0 + (105.0 - 70.0) * ft
fb = 18.0 + (32.0 - 18.0) * ft
rt = _n_smooth_edge(land_h, 0.45, 0.85)
rr = 90.0 + (160.0 - 90.0) * rt
rg = 85.0 + (155.0 - 85.0) * rt
rb = 75.0 + (145.0 - 75.0) * rt
rm = _n_smooth_edge(moist, 0.45, 0.75) * 0.3
rr = rr + (70.0 - rr) * rm
rg = rg + (95.0 - rg) * rm
rb = rb + (60.0 - rb) * rm
st = _n_smooth_edge(land_h, 0.55, 0.90)
sr = 200.0 + (235.0 - 200.0) * st
sg = 210.0 + (240.0 - 210.0) * st
sb = 220.0 + (245.0 - 220.0) * st
# Sharper dry↔wet so sand and green don't muddy together
t_dry = 1.0 - _n_smooth_edge(moist, 0.28, 0.40)
t_wet = _n_smooth_edge(moist, 0.52, 0.68)
r = gr + (dr - gr) * t_dry
g = gg + (dg - gg) * t_dry
b = gb + (db - gb) * t_dry
r = r + (fr - r) * t_wet
g = g + (fg - g) * t_wet
b = b + (fb - b) * t_wet
rk = _n_smooth_edge(land_h, 0.48, 0.68)
r = r + (rr - r) * rk
g = g + (rg - g) * rk
b = b + (rb - b) * rk
ice_lat = _n_smooth_edge(abs(lat_norm), 2.0 / 3.0, 2.0 / 3.0 + 0.06)
ice = ice_lat * (0.55 + 0.45 * _n_smooth_edge(land_h, 0.15, 0.70))
r = r + (sr - r) * ice
g = g + (sg - g) * ice
b = b + (sb - b) * ice
# Narrower beach so sand doesn't wash out the coast green/blue split
t_beach = (1.0 - _n_smooth_edge(elev, SEA_LEVEL, SEA_LEVEL + 0.022)) * (1.0 - ice) * 0.72
r = r + (210.0 - r) * t_beach
g = g + (175.0 - g) * t_beach
b = b + (95.0 - b) * t_beach
if lod >= 1:
river_amt = _is_river_n(wx, wy, elev, moist, seed)
r = r + (18.0 - r) * river_amt
g = g + (70.0 - g) * river_amt
b = b + (130.0 - b) * river_amt
# Slightly stronger relief for biome readability on 64×32
ndot = 0.55 + 0.48 * (mountain * 0.55 + (d - 0.5) * -0.45)
if ndot < 0.32:
ndot = 0.32
elif ndot > 1.15:
ndot = 1.15
r = r * ndot
g = g * ndot
b = b * ndot
# No atmospheric haze — keep biome colors crisp on the LED panel
return r, g, b, elev, river_amt, land
def _smooth_edge(v, a, b):
"""0..1 smoothstep across [a,b]."""
if b <= a:
return 1.0 if v >= a else 0.0
return _smoothstep((v - a) / (b - a))
def _warm_planet_numba(StopEvent=None):
"""
Compile or load cached Numba kernels for surface sampling.
First cold compile can take a few seconds; later launches load from disk.
"""
global _numba_warmed
if _numba_warmed:
return
if not HAS_NUMBA:
print("[PlanetBlast] Numba not available — pure Python surface path")
_numba_warmed = True
return
try:
if StopEvent is not None and StopEvent.is_set():
return
except Exception:
pass
print(
"[PlanetBlast] Warming Numba surface kernels (cache=True)...",
flush=True,
)
t0 = time.time()
# Touch every cached kernel (lod 0/1/2)
_hash2(1, 2, 3)
_value_noise(0.25, 0.75, 42)
_fbm(0.1, 0.2, 7, 2, 2.05, 0.5)
_fbm_fast(0.1, 0.2, 7, 2)
_elev_raw_n(1000.0, -500.0, 42, 0)
_elev_raw_n(1000.0, -500.0, 42, 2)
_is_river_n(1000.0, -500.0, 0.7, 0.5, 42)
_sample_biome_n(1000.0, -500.0, 42, 0)
_sample_biome_n(1000.0, -500.0, 42, 1)
_sample_biome_n(1000.0, -500.0, 42, 2)
_numba_warmed = True
print(
f"[PlanetBlast] Surface kernels ready ({time.time() - t0:.1f}s; "
f"cached for next launch).",
flush=True,
)
def _dist_point_seg(px, py, x0, y0, x1, y1):
dx, dy = x1 - x0, y1 - y0
len2 = dx * dx + dy * dy
if len2 < 1e-6:
return math.hypot(px - x0, py - y0)
t = _clamp(((px - x0) * dx + (py - y0) * dy) / len2, 0.0, 1.0)
return math.hypot(px - (x0 + t * dx), py - (y0 + t * dy))
def _generate_alien_city_name(rng, size=3):
"""
Readable English-shaped name with alien flavor.
Examples: Zorvoss, Kelthar Prime, Vynspire, Ashdria, Xelreach.
"""
onset = rng.choice(_NAME_ONSET)
# Sometimes glue onset + coda; sometimes insert a vowel core
if rng.random() < 0.55:
name = onset + rng.choice(_NAME_CODA)
else:
core = rng.choice(_NAME_CORE)
coda = rng.choice(_NAME_CODA)
# Avoid ugly triple vowels
if onset[-1] in "aeiouyAEIOUY" and core[0] in "aeiouy":
name = onset + coda
else:
name = onset + core + coda
# Title for larger settlements
if size >= 4 and rng.random() < 0.45:
title = rng.choice(_NAME_TITLE)
if title:
name = name + " " + title
elif size >= 5 and rng.random() < 0.55:
title = rng.choice([t for t in _NAME_TITLE if t] or ["Prime"])
name = name + " " + title
# Normalize casing: Title Case for multi-word
parts = name.split()
parts = [p[:1].upper() + p[1:].lower() if p else p for p in parts]
return " ".join(parts)
def _assign_city_names(cities, seed):
"""Unique alien names for every city."""
rng = random.Random(int(seed) ^ 0xA11E11)
used = set()
for c in cities:
for _ in range(40):
nm = _generate_alien_city_name(rng, size=int(c.get("size", 3)))
key = nm.upper()
if key not in used:
used.add(key)
c["name"] = nm
break
else:
c["name"] = "Node " + str(len(used) + 1)
used.add(c["name"].upper())
# 50 alien species (readable English shape)
_SPECIES_NAMES = (
"the Vorthari", "the Kelion Hives", "the Nyxari Collective",
"the Ashmeer Clans", "the Xelith Swarm", "House Mirox",
"the Thalos Dominion", "the Zorani Choir", "the Free Saelith",
"the Ornavi Mandate", "the Dravak Hordes", "the Lunari Weavers",
"the Pharosi Guild", "the Rynax Brethren", "the Veskari League",
"the Helion Spinners", "the Iskari Lattice", "the Praxari Court",
"the Qantor Blades", "the Brynnos Kin", "the Corvexi",
"the Denebi Drift", "the Eryndari", "the Fomari Reef",
"the Ganymari", "the Hyperion Veil", "the Iridonites",
"the Jotunari", "the Krynnari", "the Lothorii",
"the Manticore Host", "the Nexari Cabal", "the Oberoni",
"the Phaetoni", "the Quorosi", "the Rigellian March",
"the Solari Choir", "the Titanos Guard", "the Umbri Shade",
"the Vortexi", "the Weylandi", "the Xandari",
"the Ymirfolk", "the Zephyrosi", "the Andarosi",
"the Boreali", "the Cygnari", "the Delphosi",
"the Erebim", "the Krellari Assembly",
)
# 50 crimes against the Human Empire (serious + silly)
_EMPIRE_CRIMES = (
# Serious
"TREASON AGAINST THE HUMAN EMPIRE",
"HARBORING ENEMY FLEETS",
"ATTACK ON IMPERIAL COLONIES",
"BLOCKADE OF TRADE LANES",
"THEFT OF IMPERIAL SHIPS",
"SABOTAGE OF JUMP GATES",
"ASSASSINATION OF IMPERIAL ENVOYS",
"GENOCIDE OF HUMAN OUTPOSTS",
"ILLEGAL WEAPONS RESEARCH",
"BIOWEAPON DEPLOYMENT",
"SLAVERY OF HUMAN CITIZENS",
"PIRACY IN EMPIRE SPACE",
"DESECRATION OF HUMAN TOMBS",
"BREACH OF THE CEASEFIRE",
"SPYING ON IMPERIAL COMMAND",
"FALSIFYING PEACE TREATIES",
"RAIDING SUPPLY CONVOYS",
"COLLUSION WITH OUTER REBELS",
"DESTRUCTION OF STARPORTS",
"POISONING OF COLONY WORLDS",
"KIDNAPPING IMPERIAL OFFICERS",
"REFUSAL OF IMPERIAL TAX",
"SHELTERING WAR CRIMINALS",