-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathitbsolver.py
5570 lines (5152 loc) · 299 KB
/
itbsolver.py
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
#!/usr/bin/env python3
# This script brute forces the best possible single turn in Into The Breach
############ IMPORTS ######################
from itertools import permutations
from copy import deepcopy
############### GLOBALS ###################
# this generator and class are out of place and separate from the others, sue me
def numGen():
"A simple generator to count up from 0."
num = 0
while True:
num += 1
yield num
thegen = numGen()
class Constant():
"An object for building global constants."
def __init__(self, numgen, names):
"names is a tuple of strings, these are the constants to set. numgen is the number generator to continue from"
self.value2name = {}
for n in names:
num = next(numgen)
setattr(self, n, num)
self.value2name[num] = n
def pprint(self, iter):
"return a list of iter converted to readable names."
return [self.value2name[x] for x in iter]
class DirectionConst(Constant):
"This is the up/down/left/right directions with a couple other methods. THIS SET OF CONSTANTS MUST BE FIRST SO IT GETS THE FIRST 4 NUMBERS!"
def opposite(self, dir):
"return the opposite of the direction provided as dir."
try:
dir += 2
except TypeError:
raise InvalidDirection
if dir > 4:
dir -= 4
return dir
def gen(self):
"A generator that yields each direction."
for d in Direction.UP, Direction.RIGHT, Direction.DOWN, Direction.LEFT:
yield d
def genPerp(self, dir):
"A generator that yields 2 directions that are perpendicular to dir"
try:
dir += 1
except TypeError:
raise InvalidDirection
if dir == 5:
dir = 1
yield dir
yield self.opposite(dir)
def getClockwise(self, dir):
"return the next direction clockwise of dir"
try:
dir += 1
except TypeError:
raise InvalidDirection
if dir > 4:
return 1
return dir
def getCounterClockwise(self, dir):
"return the next direction counter clockwise of dir"
try:
dir -= 1
except TypeError:
raise InvalidDirection
if not dir:
return 4
return dir
Direction = DirectionConst(thegen, (
'UP',
'RIGHT',
'DOWN',
'LEFT'))
assert Direction.UP == 1
assert Direction.RIGHT == 2
assert Direction.DOWN == 3
assert Direction.LEFT == 4
# These are effects that can be applied to tiles and units.
Effects = Constant(thegen,
# These effects can be applied to both tiles and units:
('FIRE',
'ICE', # sometimes does nothing to tile
'ACID',
# These effects can only be applied to tiles:
'SMOKE',
'TIMEPOD',
'MINE',
'FREEZEMINE',
'SUBMERGED', # I'm twisting the rules here. In the game, submerged is an effect on the unit. Rather than apply and remove it as units move in and out of water, we'll just apply this to water tiles and check for it there.
# These effects can only be applied to units:
'SHIELD',
'EXPLOSIVE',
))
# These are attributes that are applied to units.
# Attributes typically don't change, the only exception being ARMORED which can be removed from a psion dying.
Attributes = Constant(thegen, (
'MASSIVE', # prevents drowning in water
'STABLE', # prevents units from being pushed
'FLYING', # prevents drowning in water and allows movement through other units
'ARMORED', # all attacks are reduced by 1
'BURROWER', # unit burrows after taking any damage
'UNPOWERED', # this is for friendly tanks that you can't control until later
'IMMUNEFIRE', # you are immune from fire if you have this attribute, you can't catch fire.
'IMMUNESMOKE')) # you are immune from smoke, you can fire weapons when clouded by smoke
Alliance = Constant(thegen, (
'FRIENDLY', # your mechs can move through these
'ENEMY', # but not these
'NEUTRAL')) # not these either
Passives = Constant(thegen, ( # This isn't all passives, just ones that need to be checked for presence only at certain times.
'REPAIRFIELD', # mech passives (from weapons)
'AUTOSHIELDS',
'STABILIZERS',
'KICKOFFBOOSTERS',
'FORCEAMP',
))
Actions = Constant(thegen, ( # These are the actions that a player can take with a unit they control.
'MOVE',
'SHOOT',
'MOVE2', # secondary move
'SHOOT2', # secondary shoot
))
# don't need these anymore
del Constant
del numGen
del thegen
del DirectionConst
############### FUNCTIONS #################
############### CLASSES #################
# Exceptions
class MissingCompanionTile(Exception):
"This is raised when something fails due to a missing companion tile."
def __init__(self, type, square):
super().__init__()
self.message = "No companion tile specified for %s on %s" % (type, square)
def __str__(self):
return self.message
class NullWeaponShot(Exception):
"""This is raised when a friendly unit fires a weapon that has no effect at all on the game or an invalid shot (like one that went off the board).
This prompts the logic to avoid continuing the current simulation. It's a bug if a user ever sees this."""
class InvalidDirection(Exception):
"This is raised when an invalid direction is given to something."
def __init__(self, dir):
super().__init__()
self.message = "Invalid Direction: {0}".format(dir)
def __str__(self):
return self.message
class CantHappenInGame(Exception):
"""This is raised when something happens that's not permitted in game.
For example, if the Terraformer's shot goes off the board. The in-game Terraformer will never be placed to allow this."""
class FakeException(Exception):
"This is raised to do some more efficient try/except if/else statements"
class GameOver(Exception):
"This is raised when your powergrid health is depleted, causing the end of the current simulation."
class DontGiveUnitAcid(Exception):
"This is raised when a tile tries to give acid to a building that can't take it. This signals that the tile should take the acid instead as if the unit isn't there."
class SimulationFinished(Exception):
"This is raised when an OrderSimulator runs out of actions to simulate"
class OutOfAmmo(Exception):
"This is raised when a weapon tries to fire a shot but doesn't have ammo."
# Onto the rest
class Powergrid():
"This represents your powergrid hp. When this hits 0, it's game over!"
def __init__(self, game, hp=7):
self.hp = hp
self.game = game
def takeDamage(self, damage):
self.hp -= damage
if self.hp < 1:
raise GameOver
self.game.score.submit(-50, 'powergrid_hurt', damage)
class Powergrid_CriticalShields(Powergrid):
"This is your powergrid hp when you have the CriticalShields passive in play."
def __init__(self, game, hp=7):
super().__init__(game, hp)
self.qdamage = 0 # damaged queued to take place when flushHurt tells us to
self._findBuildings()
def takeDamage(self, damage):
self.qdamage += damage
def _findBuildings(self):
"This is run when the CriticalShields passive is in play. It goes through the board and builds a set of every building."
self.buildings = set()
for sq in self.game.board:
try:
if self.game.board[sq].unit.isBuilding():
self.buildings.add(sq)
except AttributeError: # None.isbuilding()
pass
def _shieldBuildings(self):
"Shield all your buildings, return nothing."
for sq in self.buildings:
try:
if self.game.board[sq].unit.isBuilding(): # we have to check again because it's possible a building was destroyed and now another unit is standing in it's place.
self.game.board[sq].unit.applyShield()
except AttributeError:
continue
def flushHurt(self):
"""Call this to signal that buildings are done taking damage.
If we don't do this it'll be possible in the sim to shoot something that hits 2 buildings, and damaging the first one
causes the 2nd one to shield before it takes damage, which isn't what happens in the game.
However, if you have 2 powergrid hp and a 1 hp explosive vek next to a building and then you push the vek into the building, you take 1 damage from the bump
and then the critical shields fire, protecting the building from the explosion damage."""
if self.qdamage:
super().takeDamage(self.qdamage)
if self.hp == 1:
self._shieldBuildings()
self.qdamage = 0
############# THE MAIN GAME BOARD!
class Game():
"This represents the a single instance of a game. This is the highest level of the game."
def __init__(self, board=None, powergrid_hp=7, environeffect=None, vekemerge=None):
"""board is a dict of tiles to use. If it's left blank, a default board full of empty ground tiles is generated.
powergrid_hp is the amount of power (hp) you as a player have. When this reaches 0, the entire game ends.
environeffect is an environmental effect object that should be run during a turn.
vekemerge is the special VekEmerge environmental effect. If left blank, an empty one is created.
"""
if board:
self.board = board
else: # create a blank board of normal ground tiles
self.board = {} # a dictionary of all 64 squares on the board. Each key is a tuple of x,y coordinates and each value is the tile object: {(1, 1): Tile, ...}
# Each square is called a square. Each square must have a tile assigned to it, an optionally a unit on top of the square. The unit is part of the tile.
for letter in range(1, 9):
for num in range(1, 9):
self.board[(letter, num)] = Tile_Ground(self, square=(letter, num))
self.powergrid = Powergrid(self, powergrid_hp)
try:
environeffect.game = self
except AttributeError:
self.environeffect = None # no environmental effect being used
else:
self.environeffect = environeffect
if vekemerge:
self.vekemerge = vekemerge
else:
self.vekemerge = Environ_VekEmerge()
self.vekemerge.game = self
# playerunits and nonplayerunits are used for passives that need to be applied to all vek and/or all mechs.
# nonplayerunits is also a list of units that take a turn doing an action, such as vek and NPC friendlies.
# units without weapons that don't take turns such as the rock also need to be here so they can take fire damage.
# There are some units that do not add their new "replacement unit" to these lists. This is because it's not necessary.
# These replacement units don't have a weapon and don't take a turn and can't take fire damage.
self.playerunits = set() # All the units that the player has direct control over, including dead mech corpses
self.nonplayerunits = [] # all the units that the player can't control. This includes enemies and friendly NPCs such as the train.
self.hurtplayerunits = [] # a list of the player's units hurt by a single action. All units here must be checked for death after they all take damage and then this is reset.
self.hurtpsion = None # This is set to a single psion that was damaged since there can never be more than 1 psion on the board at at time
self.hurtenemies = [] # a list of all enemy units that were hurt during a single action. This includes robots
self.postattackmoves = set() # a set of tuples of squares. This is for Goo units that need to move to their victim's squares "after" the attack.
# If we don't do this, a goo will replace the unit it killed on the board, and then that unit will erase the goo when it's deaths replaces the square with None.
self.psionPassiveTurn = None # This will be replaced by a method provided by the Regeneration and Hive Targeting (tentacle) psion effects that take a turn to do their effect.
self.stormGeneratorTurn = None # This will be replaced by a method provided by the StormGenerator passive weapon
self.visceraheal = 0 # The amount to heal a mech that killed a vek. Each vek that is killed grants this amount of hp healing
self.otherpassives = set() # misc passives that only need to be checked for presence and nothing else.
self.score = ScoreKeeper()
self.actionlog = [] # a log of each action the player has taken in this particular game
self.idcount = 0 # this is used to count unique numbers assigned to player-controlled units.
# this is needed so we can later find the corresponding unit in different game instances, after it has moved
def flushHurt(self):
"resolve the effects of hurt units, returns the number of enemies killed (for use with viscera nanobots). Tiles are damaged first, then Psions are killed, then your mechs can explode, then vek/bots can die"
# print("hurtenemies:", self.hurtenemies)
# print("hurtplayerunits:", self.hurtplayerunits)
# print("hurtpsion:", self.hurtpsion)
killedenemies = 0
postattackmoves = self.postattackmoves
self.postattackmoves = set()
while True:
# First, copy our collections of hurt units, as these units dying could explode and then hurt more, hence the while loop.
hurtplayerunits = self.hurtplayerunits
hurtenemies = self.hurtenemies
self.hurtplayerunits = []
self.hurtenemies = []
try: # signal to the powergrid to damage multiple buildings at once if we're using Powergrid_CriticalShields
self.powergrid.flushHurt()
except AttributeError:
pass
try:
if self.hurtpsion._allowDeath():
killedenemies += 1
self.hurtpsion = None
except AttributeError:
pass
for hpu in hurtplayerunits:
hpu.explode()
for he in hurtenemies:
if he._allowDeath():
killedenemies += 1
for srcsquare, destsquare in postattackmoves:
self.board[srcsquare].moveUnit(destsquare)
if not (self.hurtenemies or self.hurtplayerunits or self.hurtpsion):
return killedenemies
def start(self):
"Initialize the simulation. Run this after all units have been placed on the board."
psionicreceiver = False
for unit in self.playerunits:
for weap in 'weapon1', 'weapon2':
try:
getattr(unit, weap).enable()
except AttributeError: # unit.None.enable(), or enable() doesn't exist
pass
except FakeException: # This is raised by the psionic receiver
psionicreceiver = True
for unit in self.nonplayerunits:
if psionicreceiver:
try:
unit.weapon1._enableMechs()
except AttributeError:
continue
try:
unit.weapon1.enable()
except AttributeError:
pass
try: # validate all enemy qshots
unit.weapon1.validate()
except AttributeError:
pass
try: # set companion tiles for multi-tile units
unit._setCompanion()
except AttributeError: # not a multi-tile unit
pass
def endPlayerTurn(self):
"""Simulate the outcome of this single turn, as if the player clicked the 'end turn' button in the game. Let the vek take their turn now.
Order of turn operations:
Fire
Storm Smoke
Regeneration / Psion Tentacle (Psion passive turn)
Environment
Enemy Actions
NPC actions
Enemies emerge
"""
# Do the fire turn:
for unit in self.playerunits: # copypasta instead of converting playerunits to a list
if Effects.FIRE in unit.effects:
unit.takeDamage(1, ignorearmor=True, ignoreacid=True)
for unit in self.nonplayerunits: # fire damage seems to happen to vek in the order of their turn
if Effects.FIRE in unit.effects:
unit.takeDamage(1, ignorearmor=True, ignoreacid=True)
self.flushHurt()
try: # Do the storm generator turn:
self.stormGeneratorTurn()
except TypeError: # self.None()
pass
try: # Do the psion healing / regeneration turn
self.psionPassiveTurn()
except TypeError: # self.None()
pass
try: # run the environmental turn
self.environeffect.run()
except AttributeError: # self.None.run()
pass
# Now run the enemy's turn:
for unit in self.nonplayerunits:
if unit.alliance == Alliance.ENEMY:
try:
unit.weapon1.shoot()
except AttributeError: # unit.None.shoot()
pass
# Now do NPC actions:
for unit in self.nonplayerunits:
if unit.alliance != Alliance.ENEMY:
try:
unit.weapon1.shoot()
except AttributeError: # unit.None.shoot()
pass
self.vekemerge.run()
def killMech(self, unit):
"""Run this to process a mech dying.
unit is the mech unit in self.playerunits
returns nothing, raises GameOver if the last mech dies."""
self.playerunits.discard(unit)
for u in self.playerunits:
if u.isMech(): # if there is a live mech still in play, the game goes on
return
raise GameOver # if not, the game ends
def getNextUnitID(self):
"Increment self.idcount and return the next id to use"
self.idcount += 1
return self.idcount
def getCopy(self):
"return a copy of this game object and copies of all the objects it contains."
try:
environeffect = type(self.environeffect)(list(self.environeffect.squares)) # make a new environeffect object
except (TypeError, AttributeError): # AttributeError: 'NoneType' object has no attribute 'squares'
environeffect = None # set it to be none when passed in
# create the new game object.
newgame = Game({}, self.powergrid.hp, environeffect, Environ_VekEmerge(list(self.vekemerge.squares)))
# Now go through each square and copy the properties of each to the new game object
for letter in range(1, 9):
for num in range(1, 9):
newgame.board[(letter, num)] = self.board[(letter, num)].getCopy(newgame)
try:
newgame.board[(letter, num)].createUnitHere(self.board[(letter, num)].unit.getCopy(newgame))
except AttributeError: # None._addUnitToGame()
pass # there was no unit on this tile
newgame.start()
return newgame
##############################################################################
######################################## TILES ###############################
##############################################################################
class TileUnit_Base():
"This is the base object that forms both Tiles and Units."
def __init__(self, game, square=None, type=None, effects=None):
self.game = game # this is a link back to the game board instance so tiles and units can change it
self.square = square # This is the (x, y) coordinate of the Tile or Unit. This is required for Tiles, but not for Units which have their square set when they are placed on a square.
self.type = type # the name of the unit or tile
if not effects:
self.effects = set()
else:
self.effects = set(effects) # Current effect(s) on the tile. Effects are on top of the tile. Some can be removed by having your mech repair while on the tile.
def isMoveBlocker(self):
"return True if the unit blocks mech movement, False if it doesn't. Chasm tiles block movement, as well as all enemies and some other units."
try:
return self._blocksmove
except AttributeError:
return False
class Tile_Base(TileUnit_Base):
"""The base class for all Tiles, all other tiles are based on this. Mountains and buildings are considered units since they have HP and block movement on a tile, thus they go on top of the tile."""
def __init__(self, game, square=None, type=None, effects=None, unit=None): # TODO: this unit argument is unreachable
super().__init__(game, square, type, effects=effects)
self.unit = unit # This is the unit on the tile. If it's None, there is no unit on it.
def takeDamage(self, damage=1, ignorearmor=False, ignoreacid=False):
"""Process the tile taking damage and the unit (if any) on this tile taking damage. Damage is usually done to the tile, the tile will then pass it onto the unit.
There are a few exceptions when takeDamage() will be called on the unit but not the tile, such as the Psion Tyrant damaging all player mechs which never has an effect on the tile.
Damage is an int of how much damage to take. DO NOT PASS 0 DAMAGE TO THIS or the tile will still take damage!
ignorearmor and ignoreacid have no effect on the tile and are passed onto the unit's takeDamage method.
returns nothing.
"""
try:
if (Effects.SHIELD not in self.unit.effects) and (Effects.ICE not in self.unit.effects): # if the unit on this tile was NOT shielded...
raise FakeException
except AttributeError: # raised from Effects.SHIELD in self.None.effects meaning there was no unit to check for shields/ice
self._tileTakeDamage()
return
except FakeException: # there was an unshielded unit present
self._tileTakeDamage()
self.game.board[self.square].unit.takeDamage(damage, ignorearmor=ignorearmor, ignoreacid=ignoreacid) # then the unit takes damage. If the unit was shielded, then only the unit took damage and not the tile
def hasShieldedUnit(self):
"If there is a unit on this tile that is shielded, return True, return False otherwise."
try:
return Effects.SHIELD in self.unit.effects
except AttributeError:
return False
def applyFire(self):
"set the current tile on fire"
self.effects.add(Effects.FIRE)
self._removeSmokeStormGen()
for e in Effects.SMOKE, Effects.ACID:
self.effects.discard(e) # Fire removes smoke and acid
try:
self.unit.applyFire()
except AttributeError:
return
def applySmoke(self):
"make a smoke cloud on the current tile"
self.effects.discard(Effects.FIRE) # smoke removes fire
self.effects.add(Effects.SMOKE)
self._addSmokeStormGen(self.square)
try:
self.unit.effects.discard(Effects.FIRE) # a unit moving into smoke removes fire.
except AttributeError: # self.None.effects.discard
pass # no unit which is fine
else:
if Attributes.IMMUNESMOKE not in self.unit.attributes:
try: # invalidate qshots of enemies that get smoked
self.unit.weapon1.qshot = None
except AttributeError: # either there was no unit or the unit had no weapon
pass
else:
if self.unit.alliance == Alliance.ENEMY: # if this was an enemy that was smoked, let's also break all its webs:
self.unit._breakAllWebs()
def applyIce(self):
"apply ice to the tile and unit."
if not self.hasShieldedUnit():
self.effects.discard(Effects.FIRE) # remove fire from the tile
try:
self.unit.applyIce() # give the unit ice
except AttributeError: # None.applyIce()
pass
def applyAcid(self):
try:
self.unit.applyAcid()
except (AttributeError, DontGiveUnitAcid): # the tile doesn't get acid if a unit is present to take it instead
self.effects.add(Effects.ACID)
self.effects.discard(Effects.FIRE)
def applyShield(self):
"Try to give a shield to a unit present. return True if a unit was shielded, False if there was no unit."
try: # Tiles can't be shielded, only units
self.unit.applyShield()
return True
except AttributeError:
return False
def repair(self, hp):
"Repair this tile and any mech on it. hp is the amount of hp to repair on the present unit. This method should only be used for mechs and not vek as they can be healed but they never repair the tile."
self.effects.discard(Effects.FIRE)
try:
self.unit.repair(hp)
except AttributeError:
return
def die(self):
"Instakill whatever unit is on the tile and damage the tile."
self._tileTakeDamage()
if self.unit:
self.unit.die()
def _spreadEffects(self):
"Spread effects from the tile to a unit that newly landed here. This also executes other things the tile can do to the unit when it lands there, such as dying if it falls into a chasm."
if not Effects.SHIELD in self.unit.effects: # If the unit is not shielded...
if Effects.FIRE in self.effects: # and the tile is on fire...
self.unit.applyFire() # spread fire to it.
if Effects.ACID in self.effects: # same with acid, but also remove it from the tile.
self.unit.applyAcid()
self.effects.discard(Effects.ACID)
def _tileTakeDamage(self):
"Process the effects of the tile taking damage. returns nothing."
pass
def _putUnitHere(self, unit):
"""Run this method whenever a unit lands on this tile whether from the player moving or a unit getting pushed. unit can be None to get rid of a unit.
If there's a unit already on the tile, it's overwritten but not properly deleted. returns nothing."""
self.unit = unit
try:
self.unit.square = self.square
except AttributeError: # raised by None.square
return # bail, the unit has been replaced by nothing which is ok.
self._spreadEffects()
try:
self.unit.weapon1.validate()
except AttributeError: # None.weapon1, _spreadEffects killed the unit or unit.None.validate(), unit didn't have a weapon1
pass
def createUnitHere(self, unit):
"Run this method when putting a unit on the board for the first time. This ensures that the unit is sorted into the proper set in the game."
unit._addUnitToGame()
self._putUnitHere(unit)
def replaceTile(self, newtile, keepeffects=True):
"""replace this tile with newtile. If keepeffects is True, add them to newtile without calling their apply methods.
Warning: effects are given to the new tile even if it can't support them! For example, this will happily give a chasm fire or acid.
Avoid this by manually removing these effects after the tile is replaced or setting keepeffects False and then manually keep only the effects you want."""
unit = self.unit
if keepeffects:
newtile.effects.update(self.effects)
newtile.square = self.square
self.game.board[self.square] = newtile
self.game.board[self.square]._putUnitHere(unit)
def moveUnit(self, destsquare):
"Move a unit from this square to destsquare, keeping the effects. This overwrites whatever is on destsquare! returns nothing."
# assert Attributes.STABLE not in self.unit.attributes # the train is a stable unit that moves
if destsquare == self.square:
return # tried to move a unit to the same square it's already one. This had the unintended consequence of leaving the square blank!
#print("moveUnit from", self.square, destsquare) # DEBUG
self.unit._breakAllWebs()
self.game.board[destsquare]._putUnitHere(self.unit)
self.unit = None
def push(self, direction):
"""push unit on this tile in direction.
direction is a Direction.UP type direction
This method should only be used when there is NO possibility of a unit being pushed to a square that also needs to be pushed during the same action e.g. conveyor belts or wind torrent
returns True if a unit was pushed or took bump damage, False if nothing happened."""
try:
if Attributes.STABLE in self.unit.attributes:
return False # stable units can't be pushed
except AttributeError:
return False # There was no unit to push
else: # push the unit
destinationsquare = self.getRelSquare(direction, 1)
try:
self.game.board[destinationsquare].unit.takeBumpDamage() # try to have the destination unit take bump damage
except AttributeError: # raised from None.takeBumpDamage, there is no unit there to bump into
self.moveUnit(destinationsquare) # move the unit from this tile to destination square
except KeyError:
#raise # DEBUG
return False # raised by self.board[None], attempted to push unit off the board, no action is taken
else:
self.unit.takeBumpDamage() # The destination took bump damage, now the unit that got pushed also takes damage
return True
def getRelSquare(self, direction, distance):
"""return the coordinates of the tile that starts at this tile and goes direction direction a certain distance. return False if that tile would be off the board.
direction is a Direction.UP type global constant
distance is an int
"""
if direction == Direction.UP:
destinationsquare = (self.square[0], self.square[1] + distance)
elif direction == Direction.RIGHT:
destinationsquare = (self.square[0] + distance, self.square[1])
elif direction == Direction.DOWN:
destinationsquare = (self.square[0], self.square[1] - distance)
elif direction == Direction.LEFT:
destinationsquare = (self.square[0] - distance, self.square[1])
else:
raise InvalidDirection(direction)
try:
self.game.board[destinationsquare]
except KeyError:
return False
return destinationsquare
def teleport(self, destsquare):
"Teleport from this tile to destsquare, swapping units if there is one on destsquare. This method does NOT make sure the unit is not stable!"
assert Attributes.STABLE not in self.unit.attributes
unitfromdest = self.game.board[destsquare].unit # grab the unit that's about to be overwritten on the destination
self.moveUnit(destsquare) # move unit from this square to destination
self._putUnitHere(unitfromdest)
def getEdgeSquare(self, direction):
"return a tuple of the square at the edge of the board in direction from this tile."
if direction == Direction.UP:
return (self.square[0], 8)
if direction == Direction.RIGHT:
return (8, self.square[1])
if direction == Direction.DOWN:
return (self.square[0], 1)
if direction == Direction.LEFT:
return (1, self.square[1])
raise InvalidDirection(direction)
def isSwallow(self):
"return True if this tile kills non-massive non-flying units like water and chasm"
try:
return self._swallow
except AttributeError:
return False
def isGrassland(self):
try:
return self._grassland
except AttributeError:
return False
def _addSmokeStormGen(self, square):
"This method is replaced by Weapon_StormGenerator when it is in play."
pass
def _removeSmokeStormGen(self):
"This method is replaced by Weapon_StormGenerator when it is in play."
pass
def _pass(self, fakearg=None):
"This is only here to replace the above 2 stormgem methods when destructing it."
pass
def getCopy(self, newgame):
"""return a copy of this tile for inclusion in a new copy of a game object
newgame is the newgame object to be set.
"""
return type(self)(newgame, self.square, effects=set(self.effects))
def __str__(self):
return "%s at %s. Effects: %s Unit: %s" % (self.type, self.square, set(Effects.pprint(self.effects)), self.unit)
class Tile_Ground(Tile_Base):
"This is a normal ground tile."
def __init__(self, game, square=None, type='ground', effects=None):
super().__init__(game, square, type, effects=effects)
def applyAcid(self):
super().applyAcid()
if not self.unit:
self._tileTakeDamage()
def applyFire(self):
self._tileTakeDamage() # fire removes timepods and mines just like damage does
super().applyFire()
def _spreadEffects(self):
"Ground tiles can have mines on them, but many other tile types can't."
super()._spreadEffects()
if Effects.MINE in self.effects:
self.unit.die()
self.effects.discard(Effects.MINE)
self.game.score.submit(-3, 'mine_die')
elif Effects.FREEZEMINE in self.effects:
self.effects.discard(Effects.FREEZEMINE)
self.unit.applyIce()
self.game.score.submit(-3, 'freezemine_die')
elif Effects.TIMEPOD in self.effects:
if self.unit.alliance == Alliance.FRIENDLY:
self.game.score.submit(2, 'timepod_pickup')
else:
self.game.score.submit(-30, 'timepod_die')
def _tileTakeDamage(self):
for (effect, event) in (Effects.TIMEPOD, 'timepod'), (Effects.MINE, 'mine'), (Effects.FREEZEMINE, 'freezemine'):
try:
self.effects.remove(effect)
except KeyError: # there was no effect on the tile
pass
else:
getattr(self, '_{0}DieScore'.format(event))()
class Tile_Forest_Sand_Base(Tile_Base):
"This is the base class for both Forest and Sand Tiles since they both share the same applyAcid mechanics."
def __init__(self, game, square=None, type=None, effects=None):
super().__init__(game, square, type, effects=effects)
def applyAcid(self):
try:
self.unit.applyAcid() # give the unit acid if present
except (AttributeError, DontGiveUnitAcid): # no unit present, so the tile gets acid
self.effects.discard(Effects.FIRE) # fire is put out by acid.
self.replaceTile(Tile_Ground(self.game, effects=(Effects.ACID,)), keepeffects=True) # Acid removes the forest/sand and makes it no longer flammable/smokable
# The tile doesn't get acid effects if the unit takes it instead.
class Tile_Forest(Tile_Forest_Sand_Base):
"If damaged, lights on fire."
def __init__(self, game, square=None, type='forest', effects=None):
super().__init__(game, square, type, effects=effects)
def _tileTakeDamage(self):
"tile gains the fire effect"
self.applyFire()
def _spreadEffects(self):
"Spread effects from the tile to a unit that newly landed here. Units that are on fire spread fire to a forest."
if Effects.FIRE in self.unit.effects: # if the unit is on fire...
self.applyFire() # the forest catches fire, removing smoke if there is any
elif not Effects.SHIELD in self.unit.effects: # If the unit is not on fire and not shielded...
if Effects.FIRE in self.effects: # and the tile is on fire...
self.unit.applyFire() # spread fire to the unit.
class Tile_Sand(Tile_Forest_Sand_Base):
"If damaged, turns into Smoke. Units in Smoke cannot attack or repair."
def __init__(self, game, square=None, type='sand', effects=None):
super().__init__(game, square, type, effects=effects)
def applyFire(self):
"Fire converts the sand tile to a ground tile"
self.replaceTile(Tile_Ground(self.game, effects=(Effects.FIRE,)), keepeffects=True) # Acid removes the forest/sand and makes it no longer flammable/smokable
super().applyFire()
def _tileTakeDamage(self):
self.applySmoke()
class Tile_Water_Ice_Damaged_Base(Tile_Base):
"This is the base unit for Water tiles, Ice tiles, and Ice_Damaged tiles."
def __init__(self, game, square=None, type=None, effects=None):
super().__init__(game, square, type, effects=effects)
def applyIce(self):
"replace the tile with ice and give ice to the unit if present."
if not self.hasShieldedUnit():
self.effects.discard(Effects.SUBMERGED) # Remove the submerged effect from the newly spawned ice tile in case we just froze water.
self.replaceTile(Tile_Ice(self.game))
try:
self.unit.applyIce()
except AttributeError:
return
def applyFire(self):
"Fire always removes smoke except over water and it removes acid from frozen acid tiles"
for e in Effects.SMOKE, Effects.ACID:
self.effects.discard(e)
self._removeSmokeStormGen()
try: # it's important that we set the unit on fire first. Otherwise the tile will be changed to water, then the unit will be set on fire in water. whoops.
self.unit.applyFire()
except AttributeError:
pass
self.replaceTile(Tile_Water(self.game))
def _spreadEffects(self):
"there are no effects to spread from ice or damaged ice to a unit. These tiles can't be on fire and any acid on these tiles is frozen and inert, even if added after freezing."
pass
def repair(self, hp):
"acid cannot be removed from water or ice by repairing it. There can't be any fire to repair either."
try:
self.unit.repair(hp)
except AttributeError:
return
class Tile_Water(Tile_Water_Ice_Damaged_Base):
"Non-huge land units die when pushed into water. Water cannot be set on fire."
def __init__(self, game, square=None, type='water', effects=None):
super().__init__(game, square, type, effects=effects)
self.effects.add(Effects.SUBMERGED)
self._swallow = True
def applyFire(self):
"Water can't be set on fire"
try: # spread the fire to the unit
self.unit.applyFire()
except AttributeError:
return # but not the tile. Fire does NOT remove smoke from a water tile!
def applyAcid(self):
try:
self.unit.applyAcid()
except (AttributeError, DontGiveUnitAcid):
pass
self.effects.add(Effects.ACID) # water gets acid regardless of a unit being there or not
def _spreadEffects(self):
if (Attributes.MASSIVE not in self.unit.attributes) and (Attributes.FLYING not in self.unit.attributes): # kill non-massive non-flying units that went into the water.
self.unit.die()
else: # the unit lived
if Attributes.FLYING not in self.unit.attributes:
self.unit.effects.discard(Effects.FIRE) # water puts out the fire, but if you're flying you remain on fire
if Effects.ACID in self.effects: # spread acid from tile to unit but don't remove it from the tile
self.unit.applyAcid()
if Effects.ACID in self.unit.effects: # if the unit has acid and is massive but not flying, spread acid from unit to tile
self.effects.add(Effects.ACID) # don't call self.applyAcid() here or it'll give it to the unit and not the tile.
try: # if this was a massive vek boss that fell in the water...
self.unit.weapon1.qshot = None # invalidate his shot
except AttributeError: # this has the side effect of giving mechs a qshot, but that shouldn't effect anything
pass
self.unit.effects.discard(Effects.ICE) # water breaks you out of the ice no matter what
class Tile_Ice(Tile_Water_Ice_Damaged_Base):
"Turns into Water when destroyed. Must be hit twice. (Turns into Ice_Damaged.)"
def __init__(self, game, square=None, type='ice', effects=None):
super().__init__(game, square, type, effects=effects)
def applyIce(self):
"Nothing happens when ice is frozen again"
try:
self.unit.applyIce()
except AttributeError:
return
def _tileTakeDamage(self):
self.replaceTile(Tile_Ice_Damaged(self.game))
class Tile_Ice_Damaged(Tile_Water_Ice_Damaged_Base):
def __init__(self, game, square=None, type='ice_damaged', effects=None):
super().__init__(game, square, type, effects=effects)
def _tileTakeDamage(self):
self.replaceTile(Tile_Water(self.game))
class Tile_Chasm(Tile_Base):
"Non-flying units die when pushed into a chasm. Chasm tiles cannot have acid or fire, but can have smoke."
_blocksmove = True
def __init__(self, game, square=None, type='chasm', effects=None):
super().__init__(game, square, type, effects=effects)
self._swallow = True
def applyFire(self):
try:
self.unit.applyFire()
except AttributeError:
return # fire does not remove smoke on a chasm. Seems like it only does this on solid surfaces which include ice but not water.
def applyIce(self):
try:
self.unit.applyIce()
except AttributeError:
return
def applyAcid(self):
try:
self.unit.applyAcid
except AttributeError: # there is no unit that can't take acid here
return
def _spreadEffects(self):
if (Attributes.FLYING in self.unit.attributes) and (Effects.ICE not in self.unit.effects): # if the unit can fly and is not frozen...
pass # congratulations, you live!
else:
self.unit.die()
try:
self.unit._realDeath() # try permanently killing a mech corpse
except AttributeError:
pass
self.unit = None # set the unit to None even though most units do this. Mech corpses are invincible units that can only be killed by being pushed into a chasm.
# no need to super()._spreadEffects() here since the only effects a chasm tile can have is smoke and that never spreads to the unit itself.
def repair(self, hp):
"There can't be any fire to repair"
try:
self.unit.repair(hp)
except AttributeError:
return
class Tile_Lava(Tile_Water):
def __init__(self, game, square=None, type='lava', effects=None):
super().__init__(game, square, type, effects=effects)
self.effects.add(Effects.FIRE)
def repair(self, hp):
"No effects can be removed from lava from repairing on it."
try:
self.unit.repair(hp)
except AttributeError:
return
def applyIce(self):
return # Ice does nothing
def applyFire(self):
try: # spread the fire to the unit
self.unit.applyFire()
except AttributeError:
return # but not the tile, it's always on fire
def applyAcid(self):
try: # spread the fire to the unit
self.unit.applyAcid()
except AttributeError:
return # but not the tile
def applySmoke(self):
"Smoke doesn't remove fire from the lava."
self.effects.add(Effects.SMOKE) # we don't break webs here since only flying units can be on lava and no flying units can web
self._addSmokeStormGen(self.square)
def _spreadEffects(self):
if (Attributes.MASSIVE not in self.unit.attributes) and (Attributes.FLYING not in self.unit.attributes): # kill non-massive non-flying units that went into the water.
self.unit.die()
else: # the unit lived
if Attributes.FLYING not in self.unit.attributes:
self.unit.applyFire() # lava is always on fire, now you are too!
self.unit.effects.discard(Effects.ICE) # water and lava breaks you out of the ice no matter what
class Tile_Grassland(Tile_Base):
"Your bonus objective is to terraform Grassland tiles into Sand. This is mostly just a regular ground tile."
def __init__(self, game, square=None, type='grassland', effects=None):
super().__init__(game, square, type, effects=effects)
self._grassland = True
class Tile_Teleporter(Tile_Base):
"End movement here to warp to the matching pad. Swap with any present unit."
def __init__(self, game, square=None, type='teleporter', effects=None, companion=None):
"companion is the square of the other tile linked to this one."
# teleporters can have smoke, fire and acid just like a normal ground tile.
super().__init__(game, square, type, effects=effects)
self.companion = companion
self.suppressteleport = False # this is set true when in the process of teleporting so we don't then teleport the unit back and fourth in an infinite loop.
def _spreadEffects(self):
"Spread effects like normal but teleport the unit to the companion tile afterward."
super()._spreadEffects()
if not self.suppressteleport:
try:
if self.unit.suppressteleport:
return # no teleporting for you!
except AttributeError: # unit didn't have suppressteleport attribute, only corpses do.
self.suppressteleport = True # suppress further teleports here until this finishes
try:
self.game.board[self.companion].suppressteleport = True # suppress teleport on the companion too
except KeyError:
raise MissingCompanionTile(self.type, self.square)
self.teleport(self.companion)
self.suppressteleport = False
##############################################################################
######################################## UNITS ###############################
##############################################################################
class Unit_Base(TileUnit_Base):
"The base class of all units. A unit is anything that occupies a square and stops other ground units from moving through it."
def __init__(self, game, type, hp, maxhp, effects=None, attributes=None, web=None):
"""
game is the Game instance
type is the name of the unit (str)
hp is the unit's current hitpoints (int)
maxhp is the unit's maximum hitpoints (int)
effects is a set of effects applied to this unit. Use Effects.EFFECTNAME for this.
attributes is a set of attributes or properties that the unit has. use Attributes.ATTRNAME for this.
web is a set of squares with which this unit is webbed to/from
"""
super().__init__(game=game, type=type, effects=effects)
self.hp = hp
self.maxhp = maxhp
if not attributes:
self.attributes = set()
else:
self.attributes = set(attributes)
self.damage_taken = 0 # This is a running count of how much damage this unit has taken during this turn.
# This is done so that points awarded to a solution can be removed on a unit's death. We don't want solutions to be more valuable if an enemy is damaged before it's killed. We don't care how much damage was dealt to it if it dies.
if not web:
self.web = set()
else:
self.web = set(web)
self.gotfire = self.gotacid = self.gotice = self.gotshield = False # These flags are set to true when this unit gets fire, acid, or ice applied to it.
# This is done so we can avoid scoring a unit catching on fire and then dying from damage being more valuable than just killing the unit.
self.lostfire = self.lostacid = self.lostice = self.lostshield = False # these flags are set to true when this unit loses fire, acid, or ice.
self._initScore()
def _applyEffectUnshielded(self, effect):
"A helper method to check for the presence of a shield before applying an effect. return True if the effect was added, False if not."
if Effects.SHIELD not in self.effects:
self.effects.add(effect)
return True
return False
def applyFire(self):
if Effects.FIRE not in self.effects: # if we don't already have fire
self._removeIce()
if not Attributes.IMMUNEFIRE in self.attributes:
if self._applyEffectUnshielded(Effects.FIRE): # no need to try to remove a timepod from a unit (from super())
self.gotfire = True
self.game.score.submit(self.score['fire_on'], '{0}_fire_on'.format(self.type))
def applyIce(self):
if Effects.ICE not in self.effects:
if self._applyEffectUnshielded(Effects.ICE): # If a unit has a shield and someone tries to freeze it, NOTHING HAPPENS!
self._removeFire()
try:
self.weapon1.qshot = None # invalidate the unit's queued shot
except AttributeError: # self.None.qshot
pass
self.gotice = True
self.game.score.submit(self.score['ice_on'], '{0}_ice_on'.format(self.type))
self.game.board[self.square]._spreadEffects() # spread effects after freezing because flying units frozen over chasms need to die
def applyAcid(self, ignoreprotection=False):
"give the unit acid. If ignoreprotection is True, don't check if the unit is protected by a shield or ice first (this is used by acid weapons). returns nothing."
if Effects.ACID not in self.effects:
if ignoreprotection:
self.effects.add(Effects.ACID)
self._applyAcidScore()
elif self._applyEffectUnshielded(Effects.ACID): # you only get acid if you don't have a shield.
self._applyAcidScore()
def _applyAcidScore(self):
"A helper method to score the unit getting acid."
self.gotacid = True
self.game.score.submit(self.score['acid_on'], '{0}_acid_on'.format(self.type))
def applyWeb(self):
self.effects.add(Effects.WEB)
def applyShield(self):
if Effects.SHIELD not in self.effects:
self.effects.add(Effects.SHIELD)
self.gotshield = True
self.game.score.submit(self.score['shield_on'], '{0}_shield_on'.format(self.type))
def takeDamage(self, damage, ignorearmor=False, ignoreacid=False):