The short version is that I've coded you a Battle Simulator in Python. This doesn't have a nice user interface - it's mostly a series of functions that are linked together. I'll polish it up a little over the course of tomorrow and add in some more units and so on. It's set up with one battle pre-loaded. Just load it up in the Python IDE and type fightBattle( Albia, Bulmeria ) to activate the test battle.
A lot of the first bit is just comments and me trying to think things through. Scroll past it.
import random
WOUNDMARGIN = 20
## Code is tentatively copyrighted Jack Roberts 2011. It -may- get released to open license later, when I'm comfortable about its completed state.
## Combat: 3d100/3 + mods for each combatant. Highest wins.
## If difference <= 20, units damaged for one turn, not destroyed.
## Losing side's damaged units destroyed at end of combat.
## Units may have bonuses vs other unit types in combat.
## Known types; Infantry, Tank, Air, Sea, Turret
## STATBLOCK
## off = # , def = # ,
## modifiers = [('infantry' , #),('armour' , -#),('sea' , #)]
## special = ['alwaysdestroy']
## MOCK BATTLE
## Albia arrives with 3 Regulars, 2 Tanks and a Destroyer.
## Bulmeria defends with 4 Regulars, 1 Tank and an Art Platform.
## Most capable units are ranked against each other first.
##
## Would this happen?
##
## Albia fields a Tank (22 attack).
## Bulmeria uses the Art Platform (16 defence, +16 v tanks = 32).
## Albia rolls 45, Bulmeria rolls 56. Bulmeria wins, Albian tank destroyed.
## Additionally, Bulmerian Art Platform remains in play.
##
## No. Because Art Platform outranked by Regulars, this happens.
##
## Albia tries to field a Tank (22 attack) but the Art Platform (32) outranks by 10.
## Albia fields a Regular (21 attack) which outranks the Art Platform (16) by 5.
## In response, Bulmeria fields a Tank (22 defence, +6 vs infantry = 28) which
## outranks Regulars by 7.
## Albia cannot field a Tank to balance out their Tank (and the Destroyer would
## be insufficient anyway) because the Art platform would win again. Bulmeria
## has thus fielded the force that provides them the greatest bonus, whilst
## Albia has worked to reduce that bonus as much as possible.
##
## Tank v ArtPlat = +10
## Reg v ArtPlat = -5
## Reg v Tank = +7
## Tank v Tank = 0 ...but Alb using a tank results in the first scenario.
## Reg v Reg = 0 ...but Bul is not prevented from using the tank.
##
##
## Battle Starts - Input Faction Armies
##
## Albia (A): Regulars (3), Tanks (2), Destroyer
## Bulmeria (D): Regulars (4), Tanks (1), Art Platform
##
## Regulars = Off21, Def25
## Tanks = Off22, Def22, +6 vs Inf
## Art Platform = Def16, +16 vs Tanks/Sea
## Destroyer = Off10, Def10, +2 vs Inf
##
## Neither side has native advantages/disadvantages. No situational modifiers.
##
## Pair relative advantages/disadvantages between unit types. Bonus is to Def.
##
## Attackers v Defenders
##
## Regulars v Regulars = -4
## Regulars v Tanks = -7
## Regulars v Art Platform = +5
##
## Tanks v Regulars = +3
## Tanks v Tanks = 0
## Tanks v Art Platform = -10
##
## Destroyer v Regulars = -13
## Destroyer v Tank = -12
## Destroyer v Art Platform = -22
##
##
## Regulars have highest possible malus (vs Art Plat), so attempt to field?
## If Regulars fielded, Tanks fielded to defend.
## Is it possible to use a different unit vs Tanks?
## Tanks v Tanks = 0. Destroyer v Tanks = 12. Field Tanks.
## But fielding Tanks results in Art Platform defending.
## As Art Platform matches highest possible malus anyway, field Regs.
## End the loop.
##
##
##
##
##
##
def intCheck(digitString): #Just checks if an entry was an integer.
while digitString.isdigit() == False:
digitString = raw_input('Digit: ')
return eval(digitString)
# This class is for Army type objects. Separate from Factions (to be coded).
# Armies are just collections of units you send to fight one another.
class Army():
name = str
units = []
# Tuples within list.
# [ ( Unit, intact, damaged ) ]
# [( Regulars, 1, 0 )]
def __init__(self):
self.name = 'Generican'
return
def getUnit(self, unitName): # Gets a unit based on its name.
for item in self.units:
Unit = item[0]
if unitName == Unit.name:
return Unit
return None
def addUnit(self, unit): # Adds a unit to the units list.
if self.getUnit(unit.name) != None:
print 'Unit with that name already present.'
return
intact = intCheck(raw_input('Number of units intact:'))
damaged = intCheck(raw_input('Number of units damaged:'))
entry = [ unit, intact, damaged ]
unitList = list(self.units)
unitList.append(entry)
self.units = unitList
return
def addUnitDirect(self, unit, intact, damaged): # As addUnit, without user interface.
entry = [ unit, intact, damaged ]
unitList = list(self.units)
unitList.append(entry)
self.units = unitList
return
def editUnit(self, unitName): # Edits unit numbers: intact/wounded.
for item in self.units:
Unit = item[0]
if unitName == Unit.name:
print 'Editing ' + Unit.name
intact = intCheck(raw_input('Number of units intact:'))
damaged = intCheck(raw_input('Number of units damaged:'))
item[1] = intact
item[2] = damaged
return
y = raw_input('Unit does not exist. Add? (y/n):')
if y in ['y','Y','yes','Yes']:
unit = searchLibrary(unitName)
if unit == None:
return
self.addUnit(unit)
return
return
def killUnit(self, unitName): # Reduces intact unit number by 1.
for item in self.units:
Unit = item[0]
if unitName == Unit.name:
item[1] -= 1
return
return
def woundUnit(self, unitName): # Reduces intact unit number by 1, increases wounded unit number by 1.
for item in self.units:
Unit = item[0]
if unitName == Unit.name:
item[1] -= 1
item[2] += 1
if item[1] < 0:
print 'ERRROR'
print self.units
print fingles
return
return
def healUnit(self, unitName): # Restores wounded unit to intact service. Not really used in this sim yet.
for item in self.units:
Unit = item[0]
if unitName == Unit.name:
if item[2] > 0:
item[2] -= 1
item[1] += 1
return
return
def clearRanks(self): # Kills all intact and wounded units in the army.
for item in self.units:
item[1] = 0
item[2] = 0
return
def listForces(self): # Lists forces to user.
print '%s Forces' % (self.name)
for item in self.units:
print '%s: %s Intact, %s Damaged' % (item[0].name, item[1], item[2])
# Determine which units are available in a given army.
def battleAvailLists(armyAtt, armyDef): # Checks which units are available for battle to each side.
attackAvail = []
defendAvail = []
for unitDets in armyAtt.units:
if unitDets[1] > 0:
attackAvail.append(unitDets[0].name)
for unitDets in armyDef.units:
if unitDets[1] > 0:
defendAvail.append(unitDets[0].name)
return [attackAvail, defendAvail]
# Compare the inherent advantage between two different units.
def compareAdvantage(unitAtt, unitDef):
attack = unitAtt.offence
defend = unitDef.defence
# Read type modifiers.
attMods = unitAtt.modifiers
defMods = unitDef.modifiers
attType = unitAtt.unitType
defType = unitDef.unitType
for i in attMods:
if i[0] == defType:
attack += i[1]
for j in defMods:
if j[0] == attType:
defend += j[1]
advantage = attack - defend
# Will not presently take into account faction advantages/disadvantages.
return advantage
# Pick which unit to field. Attacker gets to choose.
def battlePick(armyAtt, armyDef):
# Get army details - unit types, number available.
battleAvail = battleAvailLists(armyAtt, armyDef)
attackAvail = battleAvail[0]
defendAvail = battleAvail[1]
if defendAvail == [] or attackAvail == []:
print 'battleOrder() Error: One army has no units.'
return
# These are the lists of unit types available for combat, e.g. militia, regulars, commando, light tank, heavy tank.
# All I really need to check are the best options the defender can field for
# each of the attacker's units and then take the best from there.
# Get comparisons for each.
chances = []
for unitAttName in attackAvail:
unitAtt = armyAtt.getUnit(unitAttName)
sortie = (0,0,0)
for unitDefName in defendAvail:
unitDef = armyDef.getUnit(unitDefName)
advantage = compareAdvantage(unitAtt, unitDef)
if sortie == (0,0,0) or sortie[2] > advantage:
sortie = (unitAtt, unitDef, advantage)
chances.append(sortie)
# Pick the most favourable combination for the attacker.
chosenFight = chances[0]
for exchange in chances:
if exchange[2] > chosenFight[2]:
chosenFight = exchange
# And now all you need to do is fight the battle.
return chosenFight
# Fight a battle consisting of rounds until one side is defeated.
def fightBattle(armyAtt, armyDef):
print '%s and %s forces engage.\n' % (armyAtt.name, armyDef.name)
battleAvail = battleAvailLists(armyAtt, armyDef)
attackAvail = battleAvail[0]
defendAvail = battleAvail[1]
if attackAvail == [] or defendAvail == []:
print "There is no contest here."
return
while attackAvail != [] and defendAvail != []:
winner = fightRound(armyAtt, armyDef)
battleAvail = battleAvailLists(armyAtt, armyDef)
attackAvail = battleAvail[0]
defendAvail = battleAvail[1]
armyAtt.listForces()
print ''
armyDef.listForces()
print ''
if attackAvail == [] and defendAvail == []:
print "Both armies have fought to bitter exhaustion."
if winner == armyAtt:
print "The last stroke was won by the attackers. They slay any wounded defenders."
armyDef.clearRanks()
else:
print "The last stroke was won by the defenders. They slay any wounded attackers."
armyAtt.clearRanks()
elif attackAvail == []:
print 'The defenders have won out!'
armyAtt.clearRanks()
elif defendAvail == []:
print 'The attackers have crushed all resistance.'
armyDef.clearRanks()
return
# Fight a single round of combat.
def fightRound(armyAtt, armyDef):
chosenFight = battlePick(armyAtt, armyDef)
unitAtt = chosenFight[0]
unitDef = chosenFight[1]
attack = unitAtt.offence
defend = unitDef.defence
attMods = unitAtt.modifiers
defMods = unitDef.modifiers
attType = unitAtt.unitType
defType = unitDef.unitType
# Apply 'type' modifiers (e.g. 12 vs infantry) to rolls.
for i in attMods:
if i[0] == defType:
attack += i[1]
for j in defMods:
if j[0] == attType:
defend += j[1]
attackRoll = roll3D100() + attack
defendRoll = roll3D100() + defend
difference = attackRoll - defendRoll
attName = unitAtt.name
defName = unitDef.name
# WOUNDMARGIN is a global variable that affects the range within which scores
# will wound both units rather than killing one and leaving the other intact.
if difference <= WOUNDMARGIN and difference >= -WOUNDMARGIN: # Wound both sides.
armyAtt.woundUnit(unitAtt.name)
armyDef.woundUnit(unitDef.name)
if difference > 0:
winner = armyAtt
name1 = armyAtt.name
name2 = attName
name3 = armyDef.name
name4 = defName
statA = attackRoll
statB = defendRoll
if difference <= 0:
winner = armyDef
name1 = armyDef.name
name2 = defName
name3 = armyAtt.name
name4 = attName
statA = defendRoll
statB = attackRoll
print 'A %s %s has defeated a %s %s, but sustained heavy damage. [%s vs %s]' % (name1, name2, name3, name4, statA, statB)
else:
if difference > WOUNDMARGIN: # Kill one side.
armyDef.killUnit(unitDef.name)
winner = armyAtt
name1 = armyAtt.name
name2 = attName
name3 = armyDef.name
name4 = defName
statA = attackRoll
statB = defendRoll
elif difference < -WOUNDMARGIN:
armyAtt.killUnit(unitAtt.name)
winner = armyDef
name1 = armyDef.name
name2 = defName
name3 = armyAtt.name
name4 = attName
statA = defendRoll
statB = attackRoll
print 'A %s %s has utterly destroyed a %s %s! [%s vs %s]' % (name1, name2, name3, name4, statA, statB)
print ''
return winner
def roll3D100(): # Roll 3d100, then divides it by 3.
roll = 0
for i in range(0,3):
roll += random.randint(1,100)
roll /= 3
return roll
# Template for a kind of unit, e.g. Regulars, Militia, Light Tanks, Heavy Tanks, Turrets or Bombers.
class Unit():
name = str
offence = int
defence = int
unitType = str # Important. Default is infantry, defines what you are weak or strong against.
modifiers = [] # [('infantry', 12) , ('armour', -20) , ('turret', -5)] Appliers modifiers to rolls against unit types.
special = []
def __init__(self):
self.name = 'Generic Unit'
self.offence = 0
self.defence = 0
self.unitType = 'infantry'
return
def showStats(self):
print self.name
print 'Off %s, Def %s' % (self.offence, self.defence)
print 'Type: %s' % (self.unitType)
if self.modifiers != []:
print 'MODS'
for i in self.modifiers:
print '%s vs %s' % (i[1], i[0])
return
# The Unit Library is actually terrible code, but I feel like cheating. It is
# just a store of all the unit designs created. This more than anything saves
# me time and lets me just dump a bunch of new units in through the code because
# remembering how to do i/o properly is a pain in the neck.
UNITLIBRARY = []
def searchLibrary(unitName):
global UNITLIBRARY
for unit in UNITLIBRARY:
if unit.name == unitName:
return unit
print 'Error: Not in library.'
return None
def addToLibrary(unit):
global UNITLIBRARY
for item in UNITLIBRARY:
if item.name == unit.name:
print 'Error: Item with that name already in Unit Library.'
return
UNITLIBRARY.append(unit)
return
def removeFromLibrary(unitName):
global UNITLIBRARY
for unit in UNITLIBRARY:
if unit.name == unitName:
UNITLIBRARY.remove(unit)
return
Regulars = Unit()
Regulars.offence = 21
Regulars.defence = 25
Regulars.name = 'Regulars'
addToLibrary(Regulars)
Tank = Unit()
Tank.name = 'Medium Tank'
Tank.unitType = 'armour'
Tank.offence = 22
Tank.defence = 22
Tank.modifiers = [('infantry',6)]
addToLibrary(Tank)
ArtPlatform = Unit()
ArtPlatform.name = 'Artillery Platform'
ArtPlatform.unitType = 'turret'
ArtPlatform.defence = 16
ArtPlatform.modifiers = [('armour',16)]
addToLibrary(ArtPlatform)
Albia = Army()
Albia.name = 'Albian'
Albia.addUnitDirect(Regulars, 4, 0)
Albia.addUnitDirect(Tank, 3, 0)
Bulmeria = Army()
Bulmeria.name = 'Bulmerian'
Bulmeria.addUnitDirect(Regulars,3,0)
Bulmeria.addUnitDirect(Tank,2,0)
Bulmeria.addUnitDirect(ArtPlatform,1,0)
A few notes. The first is the variable WOUNDMARGIN at the top. This is taken from the OP's battle rules which state that a winning unit will be damaged if his victory was less than 20 greater than his opponent's roll.
It is worth noting that the combat system has changed since this rule was established. I have also chosen to re-interpret that rule as 'if the victory is less than that of the wound margin, both units are injured instead of one being destroyed'. This makes things a little more interesting in that the winning side gets to keep all its cripplingly wounded units at the end of the battle, whilst the loser's wounded units are all destroyed.
This system makes it possible for both sides to end on a stalemate with the battle ending, Somme-like, out of exhaustion. My provision for this was to award victory in that situation to whoever won the last of the rounds of combat in that battle. The survivors basically just wander around the battlefield executing enemy wounded and cleaning up. There is a very slight bias towards the defender in determining the winner of a close round of combat (as in a 2.5% slant in their favour at best).
I am not sure exactly how balanced WOUNDMARGIN is at 20 currently. I was worried it was too high, but I just did a bunch of tank battles (completely even attack/defend odds) and there were rather less 'mutual wounding' scenarios than I expected so we can probably leave it as is for the moment.
Here is a mock battle between the dastardly forces of Albia and Bulmeria:
Albian Forces
Regulars: 4 Intact, 0 Damaged
Medium Tank: 3 Intact, 0 Damaged
Bulmerian Forces
Regulars: 3 Intact, 0 Damaged
Medium Tank: 2 Intact, 0 Damaged
Artillery Platform: 1 Intact, 0 Damaged
Albian and Bulmerian forces engage.
A Albian Regulars has utterly destroyed a Bulmerian Medium Tank! [80 vs 59]
A Bulmerian Medium Tank has utterly destroyed a Albian Regulars! [83 vs 48]
A Albian Regulars has defeated a Bulmerian Medium Tank, but sustained heavy damage. [97 vs 80]
A Bulmerian Regulars has utterly destroyed a Albian Regulars! [88 vs 60]
A Bulmerian Regulars has defeated a Albian Regulars, but sustained heavy damage. [79 vs 75]
A Bulmerian Artillery Platform has utterly destroyed a Albian Medium Tank! [77 vs 48]
A Bulmerian Artillery Platform has utterly destroyed a Albian Medium Tank! [73 vs 52]
A Albian Medium Tank has defeated a Bulmerian Artillery Platform, but sustained heavy damage. [77 vs 64]
Albian Forces
Regulars: 0 Intact, 2 Damaged
Medium Tank: 0 Intact, 1 Damaged
Bulmerian Forces
Regulars: 2 Intact, 1 Damaged
Medium Tank: 0 Intact, 1 Damaged
Artillery Platform: 0 Intact, 1 Damaged
The defenders have won out!
On a win, all the loser's units in the battlespace are destroyed. Only winners get to keep the wounded.
I'll update this more as we go on (i.e. try and make it user friendly!), and probably more once the Tech unit listings are through so I can fill in the gaps in the Unit Library. But essentially this has been put together to try and make your life a little easier, Sheb. It should cut down significantly on the amount of time spent trying to run battles.