@@ -1594,7 +1594,11 @@ def remove(self, square):
# all weapons must accept power1 and power2 arguments even if the weapon doesn't actually support upgrades.
# All weapons must have a shoot() method to shoot the weapon.
# All weapons must have a genShots() method to generate all possible shots the wieldingunit in its current position with this weapon can take. It must yield arguments to the weapons shoot() method.
# Any weapons that deal damage should store the amount of damage as self.damage
# genShots() should generate a shot to take, but not test the entire state of the board to ensure it is valid. It makes more sense to have shoot() invalidate the shot when it discovers that it's invalid.
# That way if it is valid, we may have temporary variables ready to go for the shot and we avoided checking to see if it's valid twice.
# The weapon should raise NullWeaponShot when it detects an invalid shot. The board should NOT be changed before NullWeaponShot is raised.
# Any weapons that deal damage must store the amount of damage as self.damage
# Any weapons that deal self damage must store the amount of damage as self.selfdamage
# Any weapons that have limited range must store their range as self.range
# Weapons with limited uses must accept the argument usesremaining=int() in __init__(). Set the number of uses left as self.usesremaining
# self.game will be set by the unit that owns the weapon.
@@ -1603,20 +1607,18 @@ def remove(self, square):

# Generator base classes:
class Weapon_DirectionalGen_Base():
"The base class for weapons that need a direction to be shot."
"The base class for weapons that only need a direction to be shot, like projectiles."
def genShots(self):
"""A shot generator that yields each direction unless the weapon wielder is on the edge and trying to shoot off the board.
For example, if you have a unit in the bottom left corner (1, 1), you can only shoot up and right, but not left or down."""
for d in Direction.gen():
if self.game.board[self.wieldingunit.square].getRelSquare(d, 1): # false if it goes off the board
yield d
yield d

class Weapon_ArtilleryGen_Base():
"The generator for artillery weapons."
def genShots(self, minimumdistance=2):
"""Generate every possible shot that the weapon wielder can take from their position with an artillery weapon. Yields a tuple of (direction, relativedistance).
minimumdistance is how near the wielder the weapon can shoot. Artillery weapons typically can't shoot the square next to them, but Hydraulic Legs can.
genShots() methods usually don't take arguments, only child objects should use this argument."""
genShots() methods usually don't take arguments, only child objects should use this argument.
This genShots can only return valid shots by nature, no need to validate them in weapons that use this."""
for direction in Direction.gen():
relativedistance = minimumdistance # artillery weapons can't shoot the tile next to them, they start at one tile past that.
while True:
@@ -1647,6 +1649,13 @@ def genShots(self):
for r in range(1, self.range+1):
yield (d, r)

class Weapon_MirrorGen_Base():
"A base class for weapons that shoot out of both sides of the wielder"
def genShots(self):
"There are only 2 possible shots here since it shoots out of both sides at once. Being in a corner can't invalidate a shot."
yield Direction.UP
yield Direction.RIGHT

# Low-level shared weapon functionality:
class Weapon_hurtAndPush_Base():
"A base class for weapons that need to hurt and push a unit."
@@ -1661,6 +1670,8 @@ def _hurtAndPush(self, square, direction, damage):
raise AttributeError
except AttributeError: # there was no unit or there was an unshielded unit
dmgtile = True
except KeyError: # game.board[False]
raise NullWeaponShot # invalid shot detected
else: # The unit had a shield or ice and protected the tile
dmgtile = False
self.game.board[square].push(direction) # now push it
@@ -1679,13 +1690,14 @@ def _hurtAndPushSelf(self, square, direction):
super()._hurtAndPush(square, direction, self.selfdamage)

class Weapon_getSquareOfUnitInDirection_Base():
def _getSquareOfUnitInDirection(self, direction, edgeok=False):
def _getSquareOfUnitInDirection(self, direction, edgeok=False, startrel=1):
"""Travel from the weapon wielder's tile in direction, returning the square of the first unit we find.
If none is found, return False.
If none is found and edgeok is True, return the square on the edge of the board."""
targetsquare = self.game.board[self.wieldingunit.square].getRelSquare(direction, 1) # start the projectile at square in direction from the unit that used the weapon...
If none is found and edgeok is True, return the square on the edge of the board.
startrel is which relative tile to start on by default. Most weapons use 1, but the grappling hook can't grab a unit that's already next to it."""
targetsquare = self.game.board[self.wieldingunit.square].getRelSquare(direction, startrel) # start the projectile at square in direction from the unit that used the weapon...
if not targetsquare:
raise NullWeaponShot # the first square we tried to get was off the board, this is an invalid shot. XXX TODO: implement more of this!
raise NullWeaponShot # the first square we tried to get was off the board, this is an invalid shot.
while True:
try:
if self.game.board[targetsquare].unit:
@@ -1712,18 +1724,28 @@ def __init__(self, power1=False, power2=False):
class Weapon_PushAdjacent_Base():
"A base class that provides a method to push all tiles around a target."
def _pushAdjacent(self, targetsquare):
for d in Direction.gen(): # now push all the tiles around targetsquare
for d in Direction.gen(): # push all the tiles around targetsquare
try:
self.game.board[self.game.board[targetsquare].getRelSquare(d, 1)].push(d)
except KeyError: # game.board[False]
pass

class Weapon_MirrorGen_Base():
"A base class for weapons that shoot out of both sides of the wielder"
def genShots(self):
"There are only 2 possible shots here since it shoots out of both sides at once. Being in a corner can't invalidate a shot."
yield Direction.UP
yield Direction.RIGHT
class Weapon_hurtPushAdjacent_Base(Weapon_hurtAndPushEnemy_Base): #xxx
"A base class that provides a method to hurt and push all tiles around a target."
def _hurtPushAdjacent(self, targetsquare):
for d in Direction.gen(): # push all the tiles around targetsquare
try:
self._hurtAndPushEnemy(square=self.game.board[targetsquare].getRelSquare(d, 1), direction=d)
except (KeyError, NullWeaponShot): # raised from game.board[False] trying to hurt and push off the board or the relative square being False inside of _hurtAndPush(), just ignore it and continue
pass

class Weapon_isMountain_Base():
"A base class that provides a method to test a unit for mountainness."
def isMountain(self, unit):
try:
return unit._mountain
except AttributeError:
return False

# High level weapon bases:
class Weapon_Charge_Base(Weapon_DirectionalGen_Base, Weapon_hurtAndPushEnemy_Base, Weapon_getSquareOfUnitInDirection_Base):
@@ -1825,21 +1847,24 @@ def __init__(self, power1=False, power2=False):
def shoot(self, direction):
relsquare = 1 # start 1 square from the wielder
currentdamage = self.damage # damage being dealt as the beam travels. This decreases the further we go until we reach 1
try:
targettile = self.game.board[self.game.board[self.wieldingunit.square].getRelSquare(direction, relsquare)] # get the target tile, not square
except KeyError: # self.game.board[False] means we went off the board
raise NullWeaponShot
while True:
try:
targettile = self.game.board[self.game.board[self.wieldingunit.square].getRelSquare(direction, relsquare)] # get the target tile, not square
except KeyError: # self.game.board[False] means we went off the board
break # no more pew pew
if self.allyimmune and self.isBeamAlly(targettile.unit):
pass # no damage
else:
targettile.takeDamage(currentdamage) # damage the tile
if self.blocksBeamShot(targettile.unit): # no more pew pew
break
currentdamage -= 1
if currentdamage < 1:
currentdamage = 1
if currentdamage != 1:
currentdamage -= 1
relsquare += 1
try:
targettile = self.game.board[self.game.board[self.wieldingunit.square].getRelSquare(direction, relsquare)] # get the target tile, not square
except KeyError: # self.game.board[False] means we went off the board
break # no more pew pew
def isBeamAlly(self, unit):
"return True if unit is considered an ally to the beam weapon when it has the first upgrade powered."
try:
@@ -1904,7 +1929,7 @@ def shoot(self, direction, distance):
except KeyError:
pass # tried to shield off the board

class Weapon_ViceFist(Weapon_getRelSquare_Base):
class Weapon_ViceFist(Weapon_getRelSquare_Base, Weapon_DirectionalGen_Base):
"The default weapon for the Judo mech"
def __init__(self, power1=False, power2=False):
self.damage = 1
@@ -1914,18 +1939,22 @@ def __init__(self, power1=False, power2=False):
self.allyimmune = False
if power2:
self.damage += 2
def genShots(self):
"Yield squares next to the wieldingunit that have units on them. Empty squares can't be attacked."
for dir in Direction.gen():
try:
if self.game.board[self._getRelSquare(dir, 1)].unit and Attributes.STABLE not in self.game.board[self._getRelSquare(dir, 1)].unit and not self.game.board[self._getRelSquare(Direction.opposite(dir), 1)].unit:
# if there is a unit one square in direction from wielder and it's not stable and there is NO unit on the other side of the wielder...
yield dir
except KeyError: # either the target square or the destination square was off the board
pass
def shoot(self, direction):
destsquare = self._getRelSquare(Direction.opposite(direction), 1) # where the tossed unit lands
self.game.board[self._getRelSquare(direction, 1)].moveUnit(destsquare) # move the unit from the attack direction to the other side of the wielder
try:
if destsquare.unit: # can't toss a unit to an occupied tile
raise NullWeaponShot
except AttributeError: # False.unit, square was invalid
raise NullWeaponShot

targetsquare = self.game.board[self._getRelSquare(dir, 1)] # the tile where the victim is grabbed
try:
if Attributes.STABLE in self.game.board[targetsquare].unit.attributes: # if target unit is stable...
raise NullWeaponShot # we can't toss it
except (KeyError, AttributeError): # either the target square was off the board or there was no unit
raise NullWeaponShot
# now that we're here, we're sure we have a valid shot
self.game.board[targetsquare].moveUnit(destsquare) # move the unit from the attack direction to the other side of the wielder
try:
if self.allyimmune and self.game.board[destsquare].unit.alliance == Alliance.FRIENDLY:
pass # no damage to friendlies if allies are immune
@@ -1934,7 +1963,7 @@ def shoot(self, direction):
except AttributeError: # raised from None.alliance. This happens when you throw the unit into a chasm or such and it immediately dies
pass # unit died, no point in damaging the tile that killed it.

class Weapon_ClusterArtillery(Weapon_Artillery_Base, Weapon_hurtAndPushEnemy_Base):
class Weapon_ClusterArtillery(Weapon_Artillery_Base, Weapon_hurtAndPushEnemy_Base): # TODO: change artilleryGen to yield squares instead of relative dirs and distance. We can avoid calculating the square twice
"Default weapon for Siege Mech."
def __init__(self, power1=False, power2=False):
self.damage = 1
@@ -1976,7 +2005,10 @@ def __init__(self, power1=False, power2=False):
if power2:
self.damage += 1
def shoot(self, direction):
self.game.board[self._getRelSquare(direction, 1)].takeDamage(self.damage)
try:
self.game.board[self._getRelSquare(direction, 1)].takeDamage(self.damage)
except KeyError: # board[False]
raise NullWeaponShot
# TODO: Implement direction flipping!
if self.gainshield:
self.wieldingunit.applyShield()
@@ -2008,15 +2040,14 @@ def __init__(self, power1=False, power2=False):
self.damage += 1
if power2:
self.range += 1
def genShots(self):
for (d, r) in super().genShots():
try:
if self.game.board[self.game.board[self.wieldingunit.square].getRelSquare(d, r+1)].unit == None: # if the square where the wielder lands is clear...
yield (d, r)
except KeyError:
continue
def shoot(self, direction, distance):
"distance is the number of squares to jump over and damage. The wielder lands on one square past distance."
destsquare = self._getRelSquare(direction, distance+1)
try:
if self.game.board[destsquare].unit:
raise NullWeaponShot # can't land on an occupied square
except AttributeError: # landing spot was off the board
raise NullWeaponShot
targetsquare = self.wieldingunit.square # start where the unit is
for r in range(distance):
targetsquare = self.game.board[targetsquare].getRelSquare(direction, 1)
@@ -2032,8 +2063,6 @@ def __init__(self, power1=False, power2=False):
def shoot(self, direction, distance):
targetsquare = self._getRelSquare(direction, distance)
self._hurtAndPushEnemy(targetsquare, direction)
#self.game.board[targetsquare].takeDamage(self.damage) # target takes damage
#self.game.board[targetsquare].push(direction) # target is pushed
try:
self.game.board[self._getRelSquare(Direction.opposite(direction), 1)].applySmoke()
except KeyError: # self.game.board[False].applySmoke()
@@ -2064,13 +2093,13 @@ def shoot(self):
targetunit.applyShield()
self.game.board[targetsquare].push(d)

class Weapon_ElectricWhip():
class Weapon_ElectricWhip(Weapon_isMountain_Base, Weapon_DirectionalGen_Base):
"""This is the lightning mech's default weapon.
When building chain is not powered (power1), you cannot hurt buildings or chain through them with this at all.
It does not go through mountains or supervolcano either. It does go through rocks.
Cannot attack mines on the ground.
Reddit said you can attack a building if it's webbed, this is not true. Even if you attack the scorpion webbing the building, the building won't pass the attack through or take damage.
When you chain through units that are explosive, they explode in the reverse order in which they were shocked.
When you chain through units that are explosive, they explode in the reverse order in which they were shocked. # TODO: this is true and not implemented!
You can never chain through yourself when you shoot!"""
def __init__(self, power1=False, power2=False):
if power1:
@@ -2086,21 +2115,21 @@ def shoot(self, direction):
# False is included because getRelSquare will return False when you go off the board. We can use this in the branching logic to tell it that anything off the board has been visited.
# we also include the unit that shot the weapon since you can NEVER chain through yourself!
self.branchChain(backwards=Direction.opposite(direction), targetsquare=self.game.board[self.wieldingunit.square].getRelSquare(direction, 1))
if len(self.hitsquares) == 2: # if histsquares never grew, that means that the first shot was invalid
raise NullWeaponShot
# done with the recursive method, now skip False and and the wielder's square and make all the units that need to take damage take damage
for hs in self.hitsquares[2:]:
if self.game.board[hs].unit.type not in ('building', 'buildingobjective'): # don't damage buildings. If they're here they're already not effected.
if not self.game.board[hs].unit.isBuilding(): # don't damage buildings. If they're here they're already not effected.
self.game.board[hs].takeDamage(self.damage)
def genShots(self):
for dir in Direction.gen():
if self.unitIsChainable(self.game.board[self.game.board[self.wieldingunit.square].getRelSquare(dir, 1)].unit):
yield dir
def unitIsChainable(self, unit):
"Pass a unit to this method and it will return true if you can chain through it, false if not or if there is no unit."
if unit:
try:
if (self.buildingchain and unit.isBuilding()) or \
(self.buildingchain and unit.type not in ('mountain', 'mountaindamaged', 'volcano')) or \
(unit.type not in ('building', 'buildingobjective', 'mountain', 'mountaindamaged', 'volcano')):
(self.buildingchain and not self.isMountain(unit)) or \
(not self.isMountain(unit) and not unit.isBuilding()):
return True
except AttributeError:
pass
return False
def branchChain(self, backwards, targetsquare):
""""A recursive method to facilitate the branching out of the electric whip shot.
@@ -2118,26 +2147,17 @@ def branchChain(self, backwards, targetsquare):
if self.unitIsChainable(self.game.board[nextsquare].unit):
self.branchChain(backwards=Direction.opposite(d), targetsquare=nextsquare)

class Weapon_GrapplingHook(Weapon_getSquareOfUnitInDirection_Base):
class Weapon_GrapplingHook(Weapon_getSquareOfUnitInDirection_Base, Weapon_DirectionalGen_Base):
"Default weapon for Hook Mech"
def __init__(self, power1=False, power2=False):
if power1:
self.shieldally = True
else:
self.shieldally = False
# power2 is unused
def genShots():
for d in Direction.gen():
try:
if self.game.board[self.game.board[self.wieldingunit.square].getRelSquare(d, 1)].unit: # if there's a unit directly next to this one, we can't fire even just to get the shield.
continue
except KeyError: # board[False] means we went off the board
continue
else: # There was an available square without a unit
yield d
def shoot(self, direction):
try:
targetunit = self.game.board[self._getSquareOfUnitInDirection(direction)].unit
targetunit = self.game.board[self._getSquareOfUnitInDirection(direction, startrel=2)].unit
except KeyError: # board[False], there was no unit to grapple
raise NullWeaponShot
if Attributes.STABLE in targetunit.attributes:
@@ -2173,10 +2193,17 @@ def __init__(self, power1=False, power2=False):
if p:
self.range += 1
def shoot(self, direction, distance):
hotsquares = [] # a list of squares to damage. Build the list first so we can determine if this is an invalid shot
for r in range(1, distance+1):
targetsquare = self._getRelSquare(direction, r)
if not targetsquare: # went off the board
try:
if self.isMountain(self.game.board[targetsquare].unit) and r < self.range: # if the unit on the targetsquare is a mountain (which stops flamethrower from going through it) and there was range remaining...
raise NullWeaponShot # bail since this shot was already taken with less range
except KeyError: # board[False]
raise NullWeaponShot
hotsquares.append(targetsquare)
# Now we know this is a valid shot.
for targetsquare in hotsquares:
try: # unit takes damage if it was already on fire
if Effects.FIRE in self.game.board[targetsquare].unit.effects:
raise FakeException
@@ -2185,8 +2212,6 @@ def shoot(self, direction, distance):
except AttributeError: # None.effects, there was no unit
pass
self.game.board[targetsquare].applyFire() # light it up
if self.isMountain(self.game.board[targetsquare].unit) and r < self.range: # if the unit on the targetsquare is a mountain (which stops flamethrower from going through it) and there was range remaining...
raise NullWeaponShot # bail since this shot was already taken with less range
self.game.board[targetsquare].push(direction) # and finally push the last tile
def isMountain(self, unit):
try:
@@ -2241,20 +2266,15 @@ def shoot(self, direction, distance):
pass
self.game.board[self.wieldingunit.square].teleport(targetsquare)

class Weapon_HydraulicLegs(Weapon_Artillery_Base, Weapon_hurtAndPushEnemy_Base, Weapon_HydraulicLegsUnstableInit_Base):
class Weapon_HydraulicLegs(Weapon_Artillery_Base, Weapon_HydraulicLegsUnstableInit_Base, Weapon_hurtPushAdjacent_Base):
"The default weapon for Leap Mech"
def shoot(self, direction, distance):
targetsquare = self._getRelSquare(direction, distance)
if self.game.board[targetsquare].unit:
raise NullWeaponShot # the tile you're leaping to must be clear of units
self.game.board[self.wieldingunit.square].moveUnit(targetsquare) # move the wielder first
self.game.board[self.wieldingunit.square].takeDamage(self.selfdamage) # then the wielder takes damage on the new tile
for d in Direction.gen():
targetsquare = self._getRelSquare(d, 1)
try:
self._hurtAndPushEnemy(targetsquare, d)
except KeyError: # tried to hurt and push a square off the board
pass
self._hurtPushAdjacent(targetsquare)

class Weapon_UnstableCannon(Weapon_HydraulicLegsUnstableInit_Base, Weapon_Projectile_Base, Weapon_hurtAndPushEnemy_Base, Weapon_hurtAndPushSelf_Base):
def __init__(self, power1=False, power2=False):
@@ -2595,36 +2595,37 @@ def t_WeaponRammingEnginesMiss():
assert g.board[(1, 1)].effects == set() # original forest tile didn't take damage
assert g.board[(8, 1)].effects == set() # destination forest tile undamaged as well

def t_NoOffBoardShotsGenCorner():
"test noOffBoardShotsGen by putting a unit in a corner"
g = Game()
g.board[(1, 1)].createUnitHere(Unit_Charge_Mech(g, weapon1=Weapon_RammingEngines(power1=False, power2=False)))
gs = g.board[(1, 1)].unit.weapon1.genShots()
g.flushHurt()
assert next(gs) == Direction.UP
assert next(gs) == Direction.RIGHT
try:
next(gs)
except StopIteration: # no more directions
pass # which is good
else:
assert False # we got another direction?

def t_NoOffBoardShotsGenSide():
"test noOffBoardShotsGen by putting a unit against a side"
g = Game()
g.board[(1, 2)].createUnitHere(Unit_Charge_Mech(g, weapon1=Weapon_RammingEngines(power1=False, power2=False)))
gs = g.board[(1, 2)].unit.weapon1.genShots()
g.flushHurt()
assert next(gs) == Direction.UP
assert next(gs) == Direction.RIGHT
assert next(gs) == Direction.DOWN
try:
next(gs)
except StopIteration: # no more directions
pass # which is good
else:
assert False # we got another direction?
# These 2 tests were removed because weapon shot generators were changed to not explicitly validate each shot like we tested here
# def t_NoOffBoardShotsGenCorner():
# "test noOffBoardShotsGen by putting a unit in a corner"
# g = Game()
# g.board[(1, 1)].createUnitHere(Unit_Charge_Mech(g, weapon1=Weapon_RammingEngines(power1=False, power2=False)))
# gs = g.board[(1, 1)].unit.weapon1.genShots()
# g.flushHurt()
# assert next(gs) == Direction.UP
# assert next(gs) == Direction.RIGHT
# try:
# next(gs)
# except StopIteration: # no more directions
# pass # which is good
# else:
# assert False # we got another direction?
#
# def t_NoOffBoardShotsGenSide():
# "test noOffBoardShotsGen by putting a unit against a side"
# g = Game()
# g.board[(1, 2)].createUnitHere(Unit_Charge_Mech(g, weapon1=Weapon_RammingEngines(power1=False, power2=False)))
# gs = g.board[(1, 2)].unit.weapon1.genShots()
# g.flushHurt()
# assert next(gs) == Direction.UP
# assert next(gs) == Direction.RIGHT
# assert next(gs) == Direction.DOWN
# try:
# next(gs)
# except StopIteration: # no more directions
# pass # which is good
# else:
# assert False # we got another direction?

def t_WeaponTaurusCannonDefaultPower():
"Shoot the Taurus Cannon with default power"
@@ -3227,13 +3228,15 @@ def t_WeaponAerialBombsGen1():
g.board[(1, 4)].createUnitHere(Unit_Alpha_Scorpion(g))
gs = g.board[(1, 1)].unit.weapon1.genShots()
assert next(gs) == (Direction.UP, 1)
assert next(gs) == (Direction.UP, 2)
assert next(gs) == (Direction.RIGHT, 1)
assert next(gs) == (Direction.RIGHT, 2)
try:
next(gs)
except StopIteration:
pass # this is expected
else:
assert False # there shouldn't be any more valid shots to generate in this configuration
# try: # weapon gens no longer give you known-good shots, the weapon determines if it's valid or not.
# next(gs)
# except StopIteration:
# pass # this is expected
# else:
# assert False # there shouldn't be any more valid shots to generate in this configuration

def t_WeaponRocketArtillery1():
"Shoot the Rocket Artillery weapon with default power with its back against the edge."
@@ -3918,6 +3921,7 @@ def t_WeaponHydraulicLegsLowPower():
g.board[(x, y)].replaceTile(Tile_Forest(g))
g.board[(1, 1)].unit.weapon1.shoot(Direction.RIGHT, 1)
g.flushHurt()
print(g.board[(1, 1)])
assert g.board[(1, 1)].effects == {Effects.FIRE} # forest caught fire
assert g.board[(1, 1)].unit == None # wielder leaped from here
assert g.board[(2, 1)].effects == {Effects.FIRE} # forest caught fire from self-damage