Skip to content

Commit be5de4f

Browse files
author
p.witvoet
committed
- implemented fuel consumption
- destroying transporting units now also adds unit removal updates for the transported units - supplying units now also resupply units that they're transporting, at the start of each turn - unloading units is now only possible when both the destination and transporting unit tiles are passable for the unloading unit type
1 parent 886d2f4 commit be5de4f

File tree

9 files changed

+146
-58
lines changed

9 files changed

+146
-58
lines changed

databases/aiwars.py

+35-20
Original file line numberDiff line numberDiff line change
@@ -33,31 +33,31 @@
3333
#================================================================================
3434
# Unit types
3535

36-
# name, cost, movementPoints, vision, maxAmmunition
36+
# name, cost, movementPoints, vision, maxAmmunition, maxFuel
3737
# Ground units
38-
infantery = UnitType('Infantery', 1000, 3, 2, 0, canCapture = True)
39-
mech = UnitType('Mech', 3000, 2, 2, 3, canCapture = True)
40-
recon = UnitType('Recon', 4000, 8, 5, 0)
41-
apc = UnitType('APC', 5000, 6, 1, 0, canSupply = True)
42-
antiAir = UnitType('Anti-Air', 8000, 6, 2, 9)
43-
tank = UnitType('Tank', 7000, 6, 3, 9)
44-
mediumTank = UnitType('Medium Tank', 16000, 5, 1, 8)
45-
heavyTank = UnitType('Heavy Tank', 22000, 6, 1, 9)
46-
artillery = UnitType('Artillery', 6000, 5, 1, 9, minRange = 2, maxRange = 3, canActAfterMoving = False, canRetaliate = False)
47-
rockets = UnitType('Rockets', 15000, 5, 1, 6, minRange = 3, maxRange = 5, canActAfterMoving = False, canRetaliate = False)
48-
missiles = UnitType('Missiles', 12000, 4, 5, 6, minRange = 3, maxRange = 5, canActAfterMoving = False, canRetaliate = False)
38+
infantery = UnitType('Infantery', 1000, 3, 2, 0, 99, canCapture = True)
39+
mech = UnitType('Mech', 3000, 2, 2, 3, 70, canCapture = True)
40+
recon = UnitType('Recon', 4000, 8, 5, 0, 80)
41+
apc = UnitType('APC', 5000, 6, 1, 0, 70, canSupply = True)
42+
antiAir = UnitType('Anti-Air', 8000, 6, 2, 9, 60)
43+
tank = UnitType('Tank', 7000, 6, 3, 9, 70)
44+
mediumTank = UnitType('Medium Tank', 16000, 5, 1, 8, 50)
45+
heavyTank = UnitType('Heavy Tank', 22000, 6, 1, 9, 99)
46+
artillery = UnitType('Artillery', 6000, 5, 1, 9, 50, minRange = 2, maxRange = 3, canActAfterMoving = False, canRetaliate = False)
47+
rockets = UnitType('Rockets', 15000, 5, 1, 6, 50, minRange = 3, maxRange = 5, canActAfterMoving = False, canRetaliate = False)
48+
missiles = UnitType('Missiles', 12000, 4, 5, 6, 50, minRange = 3, maxRange = 5, canActAfterMoving = False, canRetaliate = False)
4949

5050
# Aerial units
51-
fighter = UnitType('Fighter', 20000, 9, 2, 9)
52-
bomber = UnitType('Bomber', 22000, 7, 2, 9)
53-
battleCopter = UnitType('Battle copter', 9000, 6, 3, 6)
54-
transportCopter = UnitType('Transport copter', 5000, 6, 2, 0)
51+
fighter = UnitType('Fighter', 20000, 9, 2, 9, 99)
52+
bomber = UnitType('Bomber', 22000, 7, 2, 9, 99)
53+
battleCopter = UnitType('Battle copter', 9000, 6, 3, 6, 99)
54+
transportCopter = UnitType('Transport copter', 5000, 6, 2, 0, 99)
5555

5656
# Naval units
57-
battleShip = UnitType('Battleship', 28000, 5, 2, 9, minRange = 2, maxRange = 6, canActAfterMoving = False, canRetaliate = False)
58-
cruiser = UnitType('Cruiser', 18000, 6, 3, 9)
59-
lander = UnitType('Lander', 12000, 6, 1, 0)
60-
sub = UnitType('Sub', 20000, 5, 5, 6, canHide = True)
57+
battleShip = UnitType('Battleship', 28000, 5, 2, 9, 99, minRange = 2, maxRange = 6, canActAfterMoving = False, canRetaliate = False)
58+
cruiser = UnitType('Cruiser', 18000, 6, 3, 9, 99)
59+
lander = UnitType('Lander', 12000, 6, 1, 0, 99)
60+
sub = UnitType('Sub', 20000, 5, 5, 6, 60, canHide = True)
6161

6262
groundUnits = [infantery, mech, recon, apc, antiAir, tank, mediumTank, heavyTank, artillery, rockets, missiles]
6363
aerialUnits = [fighter, bomber, battleCopter, transportCopter]
@@ -68,6 +68,21 @@
6868
fighter, bomber, battleCopter, transportCopter, \
6969
battleShip, cruiser, lander, sub])
7070

71+
# Aerial and naval units take some fuel each turn, and they die if they run out of fuel
72+
for unitType in aerialUnits + navalUnits:
73+
unitType.needsFuelToStayAlive = True
74+
75+
fighter.fuelCostPerTurn = 5
76+
bomber.fuelCostPerTurn = 5
77+
battleCopter.fuelCostPerTurn = 2
78+
transportCopter.fuelCostPerTurn = 2
79+
80+
battleShip.fuelCostPerTurn = 1
81+
cruiser.fuelCostPerTurn = 1
82+
lander.fuelCostPerTurn = 1
83+
sub.fuelCostPerTurn = 1
84+
sub.fuelCostPerTurnWhileHiding = 5
85+
7186
# Infantery can look further when standing on tall mountains
7287
infantery.overrideVisionForTerrainType(mountain, 4)
7388
mech.overrideVisionForTerrainType(mountain, 4)

source/core/gameDatabase.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def fromStream(self, stream):
119119

120120
self.unitTypes = []
121121
for i in xrange(unitTypesCount):
122-
unitType = UnitType('', 0, 0, 0, 0)
122+
unitType = UnitType('', 0, 0, 0, 0, 0)
123123
unitType.gameDatabase = self
124124
readBytesCount += unitType.fromStream(stream[readBytesCount:])
125125
self.unitTypes.append(unitType)

source/core/level.py

-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ def fromStream(self, stream):
146146

147147
self.buildings = []
148148
for i in xrange(buildingsCount):
149-
# TODO: Instead of having to create a Building instance with some default type, construct one directly from a stream?
150149
building = Building(self.game, self.gameDatabase.getBuildingType(0), 0, Point(0, 0), None)
151150
readBytesCount += building.fromStream(stream[readBytesCount:])
152151
self.buildings.append(building)

source/core/levelLoaders.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def loadFromAwsFile(level, filename):
7676
playerData.addBuilding(level.getBuildingAtPosition(Point(x, y)))
7777

7878

79-
# TODO: Load units!
79+
# Load units
8080
for x in xrange(width):
8181
for y in xrange(height):
8282
awsUnit = ord(data[readpos]) + 256 * ord(data[readpos + 1])

source/core/pathfinding.py

+13-5
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,10 @@ def getReachableTiles(level, start, unitType):
4848
return [node.position for node in closedList]
4949
#
5050

51-
# Checks if the specified unit type can follow the given route within one turn. This fu nction only checks the terrain, it does not look for obstructing units.
52-
def isRouteValid(level, unitType, route):
53-
movementPointsLeft = unitType.movementPoints
51+
# Checks if the specified unit type can follow the given route within one turn. This function only checks the terrain and available fuel,
52+
# it does not look for obstructing units.
53+
def isRouteValid(level, unit, route):
54+
movementPointsLeft = min(unit.type.movementPoints, unit.fuel)
5455
for tile in route:
5556
movementPointsLeft -= unitType.movementCostFor(level.getTerrainType(tile))
5657
if movementPointsLeft < 0:
@@ -63,14 +64,21 @@ def isRouteValid(level, unitType, route):
6364
# Returns (success, route, obstructing unit). If there is no obstructing unit, (True, original route, None) is returned.
6465
# Otherwise, (False, route up to the obstructing unit, obstructing unit) is returned.
6566
def isRouteUnobstructed(players, route):
66-
for i in xrange(len(route)):
67+
for i, tile in enumerate(route):
6768
for player in players:
68-
obstructingUnit = player.getUnitAtPosition(route[i])
69+
obstructingUnit = player.getUnitAtPosition(tile)
6970
if obstructingUnit != None:
7071
return (False, route[:i], obstructingUnit)
7172
return (True, route, None)
7273
#
7374

75+
def fuelCostForRoute(level, unit, route):
76+
cost = 0
77+
for tile in route:
78+
cost += unit.type.movementCostFor(level.getTerrainType(tile))
79+
return cost
80+
#
81+
7482

7583
def __tileInNodeList(tile, nodeList):
7684
for node in nodeList:

source/core/player.py

+44-14
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,11 @@ def removeUnit(self, unit):
131131
if unit in self.__finishedUnits:
132132
self.__finishedUnits.remove(unit)
133133

134-
self.__visibilityMap.removeVision(unit.position, unit.currentVision(), unit.currentStealthDetectionRange())
134+
if not unit.isLoaded():
135+
self.__visibilityMap.removeVision(unit.position, unit.currentVision(), unit.currentStealthDetectionRange())
136+
137+
for loadedUnit in unit.loadedUnits:
138+
self.removeUnit(loadedUnit)
135139
#
136140

137141
# NOTE: Not used? Current approach is to let each command function return a situation update, which is then handled by the caller.
@@ -151,7 +155,19 @@ def startTurn(self):
151155
for building in self.buildings:
152156
self.money += building.type.income
153157

154-
# Check all units that can be resupplied or repaired. Units standing on friendly buildings or next to supplying units will be supplied.
158+
# First, units that consume fuel per turn will do so.
159+
for unit in filter(lambda unit: unit.fuelConsumptionForTurn() > 0, self.units):
160+
unitUpdate = situationUpdate.addUnitUpdateForPlayer(self, unit)
161+
unit.consumeFuelForTurn()
162+
if unit.needsFuelToStayAlive() and unit.fuel <= 0:
163+
self.__addLoadedUnitRemovalsToSituationUpdate(unit, situationUpdate)
164+
self.removeUnit(unit)
165+
unitUpdate.newUnit = None
166+
167+
# Check all units that can be resupplied or repaired:
168+
# - units standing on friendly buildings
169+
# - units standing next to supplying units
170+
# - units that are carried by a supplying unit
155171
# Units standing on friendly buildings that can repair them will be repaired up to 2 hitpoints (less if not enough funds is available).
156172
# Units are checked in the order that they're built.
157173
supplyingUnits = filter(Unit.canSupply, self.units)
@@ -160,7 +176,9 @@ def startTurn(self):
160176

161177
resupply = False
162178
if unit.needsResupply():
163-
if building != None:
179+
if unit.isLoaded() and unit.carriedBy.canSupply():
180+
resupply = True
181+
elif building != None:
164182
resupply = True
165183
else:
166184
for supplyingUnit in supplyingUnits:
@@ -192,8 +210,6 @@ def startTurn(self):
192210
else:
193211
break
194212

195-
# TODO: Subtract fuel for units that use fuel each turn and remove any crashed units (units that ran out of fuel with the setting 'needs-fuel-to-survive')
196-
197213
self.__activeUnits = self.units[:]
198214
self.__movedUnits = []
199215
self.__finishedUnits = []
@@ -203,22 +219,21 @@ def startTurn(self):
203219

204220
situationUpdate.updateMoneyAmountForPlayer(self, self.money)
205221
return (ACTION_RESULT_SUCCESS, situationUpdate)
206-
# TODO: Send listeners a situationUpdate! resupplied units + money update!
207222
#
208223

209224
# Returns a (result, situationUpdate) tuple - the situation update is None if the result is invalid.
210-
# TODO: Add a utility function that updates the visibility/stealth detection maps and that extracts a list
211-
# of no-longer visible and newly visible units/buildings from other players!
212225
def moveUnit(self, unitID, route):
213226
unit = self.getUnitByID(unitID)
214227
if unit == None or not self.unitCanMove(unit):
215228
return (ACTION_RESULT_INVALID, None)
216229

217-
if not isRouteValid(self.game.level, unit.type, route):
230+
# If this unit doesn't have sufficient fuel and movement points for the specified route, it's an invalid move.
231+
if not isRouteValid(self.game.level, unit, route):
218232
return (ACTION_RESULT_INVALID, None)
219233

220-
# TODO: Also tell what unit obstructed the route, and check if that unit was previously visible - if visible, it's an INVALID move. If invisible, it's a TRAPPED situation!!!
234+
# Check up to what point the given route is unobstructed.
221235
(unobstructedRoute, route, obstructingUnit) = isRouteUnobstructed(self.game.getOtherPlayers(), route)
236+
fuelCost = fuelCostForRoute(self.game.level, unit, route)
222237

223238
# An empty route is invalid!
224239
if len(route) == 0:
@@ -233,7 +248,7 @@ def moveUnit(self, unitID, route):
233248

234249
# Otherwise, the unit has been trapped.
235250
self.__visibilityMap.removeVision(unit.position, unit.currentVision(), unit.currentStealthDetectionRange())
236-
unit.moveTo(route[-1])
251+
unit.moveTo(route[-1], fuelCost)
237252
self.unitIsFinished(unit)
238253
self.__visibilityMap.addVision(unit.position, unit.currentVision(), unit.currentStealthDetectionRange())
239254

@@ -245,8 +260,10 @@ def moveUnit(self, unitID, route):
245260

246261
if unitAtDestination != None:
247262
if unitAtDestination.canLoad(unit):
263+
unit.fuel -= fuelCost
248264
return self.__loadUnit(unit, unitAtDestination)
249265
elif unit.canCombineWithUnit(unitAtDestination):
266+
unit.fuel -= fuelCost
250267
return self.__combineUnits(unit, unitAtDestination)
251268
else:
252269
return (ACTION_RESULT_INVALID, None)
@@ -255,7 +272,7 @@ def moveUnit(self, unitID, route):
255272
situationUpdate.addUnitUpdateForPlayer(self, unit)
256273

257274
self.__visibilityMap.removeVision(unit.position, unit.currentVision(), unit.currentStealthDetectionRange())
258-
unit.moveTo(route[-1])
275+
unit.moveTo(route[-1], fuelCost)
259276
self.__visibilityMap.addVision(unit.position, unit.currentVision(), unit.currentStealthDetectionRange())
260277

261278
if unit.canActAfterMoving():
@@ -300,7 +317,11 @@ def __combineUnits(self, unit, unitAtDestination):
300317

301318
def unloadUnit(self, unitID, destination):
302319
unit = self.getUnitByID(unitID)
303-
if unit == None or unit.carriedBy == None or not self.unitCanMove(unit):
320+
if unit == None or not unit.isLoaded() or not self.unitCanMove(unit):
321+
return (ACTION_RESULT_INVALID, None)
322+
323+
# Units can only be unloaded onto tiles that they can move across, but they must also be able to cross the tile that the transporting unit is on.
324+
if not unit.canMoveAcross(self.game.level.getTerrainType(unit.position)) or not unit.canMoveAcross(self.game.level.getTerrainType(destination)):
304325
return (ACTION_RESULT_INVALID, None)
305326

306327
for player in self.game.getAllPlayers():
@@ -367,10 +388,12 @@ def attackUnit(self, unitID, targetID):
367388
unit.attackUnit(targetUnit)
368389

369390
if not targetUnit.alive():
391+
self.__addLoadedUnitRemovalsToSituationUpdate(targetUnit, situationUpdate)
370392
targetUnit.player.removeUnit(targetUnit)
371393
targetUnitUpdate.newUnit = None
372394

373395
if not unit.alive():
396+
self.__addLoadedUnitRemovalsToSituationUpdate(unit, situationUpdate)
374397
self.removeUnit(unit)
375398
unitUpdate.newUnit = None
376399
else:
@@ -456,6 +479,14 @@ def endTurn(self):
456479
#
457480

458481

482+
# Recursively adds a unit removal update to the situation update for all transported units (excluding the transporting unit).
483+
def __addLoadedUnitRemovalsToSituationUpdate(self, unit, situationUpdate):
484+
if unit.isTransportingUnits():
485+
for loadedUnit in unit.loadedUnits:
486+
situationUpdate.addUnitRemovalForPlayer(loadedUnit.player, loadedUnit)
487+
self.__addLoadedUnitRemovalsToSituationUpdate(loadedUnit, situationUpdate)
488+
#
489+
459490
def unitCanMove(self, unit):
460491
return unit in self.__activeUnits
461492
#
@@ -521,7 +552,6 @@ def getOldVisibilityMapForSituationUpdate(self, situationUpdate):
521552
def toStream(self, hideInformation):
522553
return toStream(self.id, \
523554
self.name)
524-
# TODO: Also send units and buildings along?
525555
#
526556

527557
# All but the game variable will be deserialized.

source/core/situationUpdate.py

-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ def addUnitRemovalForPlayer(self, player, unit):
6565
return existingUnitUpdate
6666
#
6767

68-
# TODO: Refactor this into the above unit design!!!
6968
def addBuildingCreationForPlayer(self, player, building):
7069
if building == None:
7170
return

0 commit comments

Comments
 (0)