battleman.py: Broke program into multiple components - data classes and input functions are now in separate modules.

This commit is contained in:
Anna Rose 2012-03-29 18:28:21 -04:00
parent 109e967987
commit cdb245df99
3 changed files with 662 additions and 650 deletions

View File

@ -7,551 +7,22 @@
# to do anything fancier... may develop that at some point. # to do anything fancier... may develop that at some point.
# #
# future features: # future features:
# * keep a pickled file or a shelve db of the current state, and add a --resume option
# for resuming a battle later
# * an option for passing in multiple files that contain combatant definitions # * an option for passing in multiple files that contain combatant definitions
# * down combatants go into a separate list
import sys
sys.path.append('lib/')
from dice import Dice
import cPickle as pickle import cPickle as pickle
import argparse import argparse
import os.path import os.path
import sys import battle
from battle import CombatGroup
from battle import Combatant
class CombatGroup(): import easyinput
# What we're mostly getting here is a definition of the *members*
# of the group... then we build them all and stick them in
# the group
@classmethod
def from_input(cls):
name = input_str("name")
hp = input_int('hp')
init_mod = input_int('init mod', 0)
ap = input_int('action points', 0)
surges = input_int('healing surges', 0)
recharges = []
while True:
data = []
data = input_str("recharge", default='').split(',')
if len(data) == 2:
recharge = {}
recharge['name'] = data[0]
recharge['value'] = int(data[1])
recharge['used'] = False
recharges.append(recharge)
else:
break
count = input_int('count', 1)
# Now make the combatants...
members = []
for i in range(count):
members.append(Combatant(name, hp, pc=False, surges=surges, ap=ap, sw=0, recharges=recharges))
if count > 1:
name = name + 's'
return CombatGroup(name, members, init_mod)
def __init__(self, name, members, init_mod=0):
self.name = name
self.members = members
self.init_mod = init_mod
self.init = 0
def roll_init(self):
d = Dice.from_str('1d20+{}'.format(self.init_mod))
self.set_init(d.roll()['total'])
def set_init(self, init):
self.init = init
def add_member(self, c):
self.members.append(c)
def is_solo_group(self):
return len(self.members) == 1
def begin_turn(self):
print '{} {} initiative.'.format(self.name, ['has', 'have'][len(self.members) != 1])
for c in self.members:
c.begin_turn()
def end_turn(self):
for c in self.members:
c.end_turn()
class Combatant():
next_index = 0
def __init__(self, name, hp, pc=False, init_mod=0, surges=0, ap=0, sw=0, recharges=[]):
self.name = name
self.max_hp = hp
self.hp = self.max_hp
self.temp_hp = 0
self.pc = pc
self.surges = surges
self.ap = ap
self.sw = sw
self.conditions = {}
self.index = Combatant.next_index
self.next_condition_index = 0
Combatant.next_index += 1
self.recharges = {}
recharge_index = 0
for r in recharges:
r['index'] = recharge_index
r['just_used'] = False
self.recharges[recharge_index] = r
recharge_index += 1
def __str__(self):
return "{}: {} ({})".format(self.index, self.name, self.format_health_summary())
# cond_type can be 's' or 't', for 'save' or 'timed'. If it is 't', condition expires at the end of the players turn
# 'duration' rounds from now
def add_condition(self, name, cond_type, duration=None, end_type='e'):
condition = {}
condition['name'] = name
condition['cond_type'] = cond_type
condition['duration'] = duration
condition['end_type'] = end_type
condition['index'] = self.next_condition_index
if cond_type == 'timed' and duration == None:
print 'Error: specified a timed condition with no duration.'
return
self.conditions[self.next_condition_index] = condition
self.next_condition_index += 1
# Removes a condition, prints a message about it, and returns a copy of the condition
def remove_condition(self, index):
if index not in self.conditions:
print "Error: invalid condition index."
return None
c = self.conditions.pop(index)
print '{} is no longer affected by {}.'.format(self, c['name'])
return c
def choose_condition(self):
if not len(self.conditions):
print '{} has no conditions.'.format(self)
return None
print self.format_condition_summary()
index = input_int('choice')
if index not in self.conditions:
print 'Error: {} is not a valid index'.format(index)
return self.choose_condition()
return self.conditions[index]
def tick_conditions(self):
for c in self.conditions.values():
if c['cond_type'] == 't':
c['duration'] -= 1
def begin_turn(self):
for c in self.conditions.values():
if c['cond_type'] == 't' and c['end_type'] == 's':
if c['duration'] <= 0:
self.remove_condition(c['index'])
else:
print '{} is still affected by {} ({} round{} left).'.format(self, c['name'], c['duration'], 's'[c['duration']==1:])
def end_turn(self):
for c in self.conditions.values():
if c['cond_type'] == 's':
r = None
if self.pc:
print '{}: save against {}.'.format(self, c['name'])
r = input_int('saving throw')
else:
save_die = Dice.from_str('1d20')
r = save_die.roll()['total']
if r >= 10:
self.remove_condition(c['index'])
print '{} successfully saved against {}.'.format(self, c['name'])
else:
print '{} failed a save against {}.'.format(self, c['name'])
elif c['cond_type'] == 't' and c['end_type'] == 'e':
if c['duration'] <= 0:
self.remove_condition(c['index'])
else:
print '{} is still affected by {} ({} round{} left).'.format(self, c['name'], c['duration'], 's'[c['duration']==1:])
for r in self.recharges.values():
if r['used']:
if r['just_used']:
r['just_used'] = False
continue
# Roll to recharge
d = Dice.from_str('1d6')
n = d.roll()['total']
if n >= r['value']:
r['used'] = False
print '{} can use {} again!'.format(self, r['name'])
def damage(self, amount):
was_bloodied = self.is_bloodied()
if self.temp_hp > 0:
self.temp_hp -= amount
if self.temp_hp < 0:
amount = abs(self.temp_hp)
self.temp_hp = 0
self.hp -= amount
print '{} took {} points of damage.'.format(self, amount)
if self.is_down():
print '{} is down!'.format(self)
elif self.is_bloodied() and not was_bloodied:
print '{} is bloodied!'.format(self)
def heal(self, amount):
was_down = self.is_down()
was_bloodied = self.is_bloodied()
if self.hp < 0:
self.hp = 0
amount_healed = amount
if self.hp + amount_healed > self.max_hp:
amount_healed = self.max_hp - self.hp
self.hp += amount_healed
if was_down:
print '{} regains consciousness.'.format(self)
print '{} regained {} hit points.'.format(self, amount_healed)
if was_bloodied and not self.is_bloodied():
print '{} is no longer bloodied.'.format(self)
elif was_bloodied and self.is_bloodied():
print '{} is still bloodied.'.format(self)
def add_temp_hp(self, amount):
self.temp_hp += amount
print '{} gained {} temporary hit points.'.format(self, amount)
def use_surge(self, heal=True):
if self.surges <= 0:
print '{} has no healing surges.'.format(self)
return
self.surges -= 1
noheal = ''
if not heal:
noheal = " (but didn't regain hit points)"
print '{} spent a healing surge{}.'.format(self, noheal)
if heal:
self.heal(self.max_hp / 4)
def use_second_wind(self):
if self.sw <= 0:
print "{} doesn't have a second wind.".format(self)
return
self.surges -= 1
print '{} got their second wind!'.format(self)
# Now the actual effects of the SW
self.use_surge()
self.add_condition('Second Wind (+2 all def)', 't', 1)
def use_recharge_power(self, index):
if index not in self.recharges:
print "Error: Invalid recharge index"
return
self.recharges[index]['used'] = True
self.recharges[index]['just_used'] = True
def choose_recharge_power(self):
if not len(self.recharges):
print '{} has no rechargable powers.'.format(self)
return None
print self.format_recharge_summary()
index = input_int('choice')
if index not in self.recharges:
print 'Error: {} is not a valid index'.format(index)
return self.choose_recharge_power()
return self.recharges[index]
def is_bloodied(self):
return self.hp <= self.max_hp / 2
def is_down(self):
return self.hp <= 0
def format_full_info(self):
return """{name}
{separator}
index: {index}
hp: {hp}/{max_hp}
temp hp: {temp_hp}
surges: {surge}
ap: {ap}
sw: {sw}
conditions:
{conditions}
recharge powers:
{recharge}""".format(index=self.index, name=self.name, hp=self.hp, max_hp=self.max_hp, temp_hp=self.temp_hp, surge=self.surges, ap=self.ap, sw=self.sw, conditions=self.format_condition_summary(' '), recharge=self.format_recharge_summary(' '), separator='='*len(self.name))
def format_health_summary(self):
bloodied = ''
temp_info = ''
if self.is_bloodied():
bloodied = ', bloodied'
if len(self.conditions):
bloodied = bloodied + ', '
if self.temp_hp > 0:
temp_info = ', {} temp hp'.format(self.temp_hp)
return '{} hp{}{}{}'.format(self.hp, temp_info, bloodied, ', '.join([x['name'] for x in self.conditions.values()]))
def format_condition_summary(self, initial=''):
summary = ''
for (index, c) in self.conditions.items():
type_string = ''
if c['cond_type'] == 's':
type_string = 'Save Ends'
elif c['cond_type'] == 't':
type_string = '{} Round{}'.format(c['duration'], 's'[ c['duration']==1: ] )
summary = summary + '{}{}: {} ({})\n'.format(initial, index, c['name'], type_string)
return summary.rstrip()
def format_recharge_summary(self, initial):
summary = ''
for (index, r) in self.recharges.items():
summary = summary + '{}{}: {} (Recharge: {}, Available: {})\n'.format(initial, index, r['name'], r['value'], ['Yes', 'No'][ r['used'] ])
return summary.rstrip()
# data about the battle - includes combatant list, etc
class Battle():
def __init__(self):
self.combatant_hash = {}
self.groups = []
self.current = None
self.round = None
def __str__(self):
ret = ''
if self.is_started():
ret = 'Battle underway, currently on round {}\n\n'.format(self.round)
else:
ret = 'Battle not yet started\n\n'
ret = ret + 'Combatants\n==========\n'
ret = ret + self.format_combatants()
return ret
def is_started(self):
return self.current != None
def add_group(self, group):
# If battle is already going, need to know
# where in the init order the new mooks go
if self.is_started():
if group.is_solo_group() and group.members[0].pc:
group.set_init(input_int('Initiative for {}'.format(group.name)))
else:
group.roll_init()
added = False
for i in range(len(self.groups)):
if group.init > self.groups[i].init:
self.groups.insert(i, group)
added = True
break
if not added:
self.groups.append(group)
else:
self.groups.append(group)
for c in group.members:
self.combatant_hash[c.index] = c
def get_current_group(self):
if self.current != None:
return self.groups[self.current]
else:
return None
# If you're in a multi-member group, the first
# member of the group gets returned.
def get_current_combatant(self):
if self.current != None:
return self.groups[self.current].members[0]
else:
return None
def get_combatant(self, index):
if index in self.combatant_hash:
return self.combatant_hash[index]
else:
return None
def choose_combatant(self):
print self.format_combatants()
index = input_int('choose combatant')
if index not in self.combatant_hash:
print 'Error: {} is not a valid index'.format(index)
return self.choose_combatant()
return self.combatant_hash[index]
def begin(self):
if self.is_started():
print "Error: battle is already running"
for g in self.groups:
if g.is_solo_group() and g.members[0].pc:
g.set_init(input_int('Initiative for {}'.format(g.name)))
else:
g.roll_init()
self.groups.sort(reverse=True, key=lambda group: group.init)
self.current = 0
print '\nInitiative Roster:\n'
for g in self.groups:
print '{} ({})'.format(g.name, g.init)
print ''
self.next_round()
self.get_current_group().begin_turn()
# Returns a formatted string with all of the combatants
def format_combatants(self):
ret = ''
for g in self.groups:
if g.is_solo_group():
ret = ret + '{}\n'.format(g.members[0])
else:
ret = ret + '{}:\n'.format(g.name)
for c in g.members:
ret = ret + ' {}\n'.format(c)
return ret.rstrip()
# Returns a formatted string with just the current group
def format_current_group(self):
if self.validate_started():
return self.validate_started()
g = self.groups[self.current]
if g.is_solo_group():
return '{}'.format(g.members[0].format_full_info())
else:
ret = '{}\n'.format(g.name)
for c in g.members:
ret = ret + ' {}\n'.format(c)
return ret.rstrip()
def next_combatant(self):
if self.validate_started():
print self.validate_started()
return
g = self.get_current_group()
g.end_turn()
self.current += 1
if self.current >= len(self.groups):
self.current = 0
self.next_round()
g = self.get_current_group()
g.begin_turn()
def next_round(self):
if self.round == None:
self.round = 1
else:
self.round += 1
# Decrement all timed conditions
for c in self.combatant_hash.values():
c.tick_conditions()
print 'Beginning round {}'.format(self.round)
def validate_started(self):
if not self.is_started():
return 'Error: you can only run this command after starting the battle'
return None
def main(): def main():
battle = Battle() btl = battle.Battle()
### This is the pickling jar ### This is the pickling jar
battle_pickle = None battle_pickle = None
@ -573,33 +44,31 @@ def main():
if settings.resume: if settings.resume:
try: try:
with open(BP_FILE, 'r') as f: with open(BP_FILE, 'r') as f:
battle = pickle.load(f) btl = pickle.load(f)
battle_pickle = pickle.dumps(battle) battle_pickle = pickle.dumps(btl)
except: except:
print "Error: Couldn't resume. Quitting to preserve our pickle." print "Error: Couldn't resume. Quitting to preserve our pickle."
sys.exit(1) sys.exit(1)
print 'Resuming battle. We are in round: {}\n'.format(battle.round)
else: else:
# hard-coding test cases for now. # hard-coding test cases for now.
# Eventually, use a state-saving text file that's easy to edit, or at least copy... # Eventually, use a state-saving text file that's easy to edit, or at least copy...
battle.add_group(CombatGroup("Adele", [Combatant("Adele", hp=26, pc=True, surges=8, sw=1)], 2)) btl.add_group(CombatGroup("Adele", [Combatant("Adele", hp=26, pc=True, surges=8, sw=1)], 2))
battle.add_group(CombatGroup("Aristaire", [Combatant("Aristaire", hp=20, pc=True, surges=6, sw=1)], 0)) btl.add_group(CombatGroup("Aristaire", [Combatant("Aristaire", hp=20, pc=True, surges=6, sw=1)], 0))
battle.add_group(CombatGroup("Foobolds", [Combatant("Foobold", hp=50), Combatant("Foobold", hp=50), Combatant("Foobold", hp=50), Combatant("Foobold", hp=50), Combatant("Foobold", hp=50)], 20)) btl.add_group(CombatGroup("Foobolds", [Combatant("Foobold", hp=50), Combatant("Foobold", hp=50), Combatant("Foobold", hp=50), Combatant("Foobold", hp=50), Combatant("Foobold", hp=50)], 20))
battle.add_group(CombatGroup("Barglins", [Combatant("Barglin", hp=1), Combatant("Barglin", hp=1)], 3)) btl.add_group(CombatGroup("Barglins", [Combatant("Barglin", hp=1), Combatant("Barglin", hp=1)], 3))
battle.add_group(CombatGroup("Orcs of Baz", [Combatant("Orc", hp=32), Combatant("Orc", hp=32)], 1)) btl.add_group(CombatGroup("Orcs of Baz", [Combatant("Orc", hp=32), Combatant("Orc", hp=32)], 1))
print battle print btl
while True: while True:
do_prompt(battle, battle_pickle, bp_io_failed, BP_FILE) do_prompt(btl, battle_pickle, bp_io_failed, BP_FILE)
def do_prompt(battle, battle_pickle, bp_io_failed, BP_FILE): def do_prompt(btl, battle_pickle, bp_io_failed, BP_FILE):
print '' print ''
(comm, rdata) = input_str('', default='n', show_default=False, prompt_str='>').partition(' ')[::2] (comm, rdata) = easyinput.input_str('', default='n', show_default=False, prompt_str='>').partition(' ')[::2]
data = rdata.split(' ') data = rdata.split(' ')
if data == ['']: if data == ['']:
@ -608,39 +77,39 @@ def do_prompt(battle, battle_pickle, bp_io_failed, BP_FILE):
if comm == '?': if comm == '?':
do_help() # fixme - add ability to get command-specific help do_help() # fixme - add ability to get command-specific help
elif comm == 'a': elif comm == 'a':
do_add_combatants(battle, data) do_add_combatants(btl, data)
elif comm == 'p': elif comm == 'p':
do_print_combatant_info(battle, data) do_print_combatant_info(btl, data)
elif comm == 'l': elif comm == 'l':
print battle.format_combatants() print btl.format_combatants()
elif comm == 'b': elif comm == 'b':
battle.begin() btl.begin()
elif comm == 'd': elif comm == 'd':
do_damage(battle, data) do_damage(btl, data)
elif comm == 'h': elif comm == 'h':
do_heal(battle, data) do_heal(btl, data)
elif comm == 't': elif comm == 't':
do_add_temp_hp(battle, data) do_add_temp_hp(btl, data)
elif comm == 'T': elif comm == 'T':
do_remove_temp_hp(battle, data) do_remove_temp_hp(btl, data)
elif comm == 's': elif comm == 's':
do_surge(battle, data) do_surge(btl, data)
elif comm == 'so': elif comm == 'so':
do_surge(battle, data, heal=False) do_surge(btl, data, heal=False)
elif comm == 'sw': elif comm == 'sw':
do_second_wind(battle, data) do_second_wind(btl, data)
elif comm == 'c': elif comm == 'c':
do_add_condition(battle, data) do_add_condition(btl, data)
elif comm == 'C': elif comm == 'C':
do_remove_condition(battle, data) do_remove_condition(btl, data)
elif comm == 'n': elif comm == 'n':
battle.next_combatant() btl.next_combatant()
elif comm == 'r': elif comm == 'r':
do_use_recharge_power(battle, data) do_use_recharge_power(btl, data)
elif comm == 'w': elif comm == 'w':
do_wait(battle, data) do_wait(btl, data)
elif comm == 'W': elif comm == 'W':
do_unwait(battle, data) do_unwait(btl, data)
elif comm == 'x': elif comm == 'x':
do_stub() do_stub()
elif comm == 'q': elif comm == 'q':
@ -649,7 +118,7 @@ def do_prompt(battle, battle_pickle, bp_io_failed, BP_FILE):
# Re-pickle and write if changed after every query. It's cheap # Re-pickle and write if changed after every query. It's cheap
# and we only have to run at user-speed anyway # and we only have to run at user-speed anyway
old_bp = battle_pickle old_bp = battle_pickle
battle_pickle = pickle.dumps(battle) battle_pickle = pickle.dumps(btl)
if old_bp != battle_pickle: if old_bp != battle_pickle:
try: try:
@ -685,105 +154,105 @@ q - quit""")
# Core data parsing functions # Core data parsing functions
def do_add_combatants(battle, data): def do_add_combatants(btl, data):
if len(data) >= 1: if len(data) >= 1:
ngroups = int(data[0]) ngroups = int(data[0])
else: else:
ngroups = input_int('number of groups') ngroups = easyinput.input_int('number of groups')
for i in range(1, ngroups+1): for i in range(1, ngroups+1):
print "Adding group {}".format(i) print "Adding group {}".format(i)
battle.add_group(CombatGroup.from_input()) btl.add_group(CombatGroup.from_input())
def do_print_combatant_info(battle, data): def do_print_combatant_info(btl, data):
if len(data) >= 1: if len(data) >= 1:
c = battle.get_combatant(int(data[0])) c = btl.get_combatant(int(data[0]))
if not c: if not c:
print('Error: Invalid combatant index.') print('Error: Invalid combatant index.')
else: else:
print c.format_full_info() print c.format_full_info()
else: else:
print battle.format_current_group() print btl.format_current_group()
def do_damage(battle, data): def do_damage(btl, data):
c = do_combatant_select(battle, data) c = do_combatant_select(btl, data)
if not c: if not c:
return return
if len(data) >= 2: if len(data) >= 2:
amount = int(data[1]) amount = int(data[1])
else: else:
amount = input_int('damage') amount = easyinput.input_int('damage')
c.damage(amount) c.damage(amount)
def do_heal(battle, data): def do_heal(btl, data):
c = do_combatant_select(battle, data) c = do_combatant_select(btl, data)
if not c: if not c:
return return
if len(data) >= 2: if len(data) >= 2:
amount = int(data[1]) amount = int(data[1])
else: else:
amount = input_int('amount') amount = easyinput.input_int('amount')
c.heal(amount) c.heal(amount)
def do_add_temp_hp(battle, data): def do_add_temp_hp(btl, data):
c = do_combatant_select(battle, data) c = do_combatant_select(btl, data)
if not c: if not c:
return return
if len(data) >= 2: if len(data) >= 2:
amount = int(data[1]) amount = int(data[1])
else: else:
amount = input_int('amount') amount = easyinput.input_int('amount')
c.add_temp_hp(amount) c.add_temp_hp(amount)
def do_remove_temp_hp(battle, data): def do_remove_temp_hp(btl, data):
do_stub() do_stub()
def do_surge(battle, data, heal=True): def do_surge(btl, data, heal=True):
c = do_combatant_select(battle, data) c = do_combatant_select(btl, data)
if not c: if not c:
return return
c.use_surge(heal) c.use_surge(heal)
def do_second_wind(battle, data): def do_second_wind(btl, data):
c = do_combatant_select(battle, data) c = do_combatant_select(btl, data)
if not c: if not c:
return return
c.use_second_wind() c.use_second_wind()
def do_add_condition(battle, data): def do_add_condition(btl, data):
duration = None duration = None
end_type = 'e' end_type = 'e'
c = do_combatant_select(battle, data) c = do_combatant_select(btl, data)
if not c: if not c:
return return
name = do_data_input_str(data, 1, 'condition name') name = easyinput.do_data_input_str(data, 1, 'condition name')
ctype = do_data_input_str(data, 2, 'condition type', default='s', show_default=True) ctype = easyinput.do_data_input_str(data, 2, 'condition type', default='s', show_default=True)
if ctype == 't': if ctype == 't':
duration = do_data_input_int(data, 3, 'duration') duration = easyinput.do_data_input_int(data, 3, 'duration')
end_type = do_data_input_str(data, 4, '(s)tart|(e)nd', default='e', show_default=True) end_type = easyinput.do_data_input_str(data, 4, '(s)tart|(e)nd', default='e', show_default=True)
c.add_condition(name, ctype, duration, end_type) c.add_condition(name, ctype, duration, end_type)
def do_remove_condition(battle, data): def do_remove_condition(btl, data):
c = do_combatant_select(battle, data) c = do_combatant_select(btl, data)
if not c: if not c:
return return
@ -799,8 +268,8 @@ def do_remove_condition(battle, data):
c.remove_condition(index) c.remove_condition(index)
def do_use_recharge_power(battle, data): def do_use_recharge_power(btl, data):
c = do_combatant_select(battle, data) c = do_combatant_select(btl, data)
if not c: if not c:
return return
@ -816,11 +285,11 @@ def do_use_recharge_power(battle, data):
c.use_recharge_power(index) c.use_recharge_power(index)
def do_wait(battle, data): def do_wait(btl, data):
do_stub() do_stub()
def do_unwait(battle, data): def do_unwait(btl, data):
do_stub() do_stub()
@ -835,56 +304,5 @@ def parse_args():
return parser.parse_args() return parser.parse_args()
# Data parsing helper functions
def do_combatant_select(battle, data):
c = None
if len(data) >= 1:
c = battle.get_combatant(int(data[0]))
if not c:
print 'Error: Invalid combatant index.'
else:
c = battle.get_current_combatant()
if not c:
print 'Error: Battle not started and no combatant specified.'
return c
def do_data_input_str(battle, data, index, prompt, default=None, show_default=False, prompt_str=':'):
if len(data) >= index + 1:
return data[index]
else:
return input_str(prompt, default, show_default, prompt_str)
def do_data_input_int(battle, data, index, prompt, default=None, show_default=True, prompt_str=':'):
return int(do_data_select_str(battle, data, index, prompt, default, show_default, prompt_str))
# Input primitives
def input_str(prompt, default=None, show_default=False, prompt_str=':'):
full_prompt = prompt
if default != None and show_default:
full_prompt = full_prompt + ' [{}]'.format(default)
full_prompt = full_prompt + '{} '.format(prompt_str)
data = raw_input(full_prompt)
if not data:
if default == None:
print 'Error: you must provide a value!'
return input_str(prompt, default, show_default, prompt_str)
else:
return default
else:
return data
def input_int(prompt, default=None, show_default=True, prompt_str=':'):
return int(input_str(prompt, default, show_default))
if __name__ == '__main__': if __name__ == '__main__':
main() main()

558
lib/battle.py Normal file
View File

@ -0,0 +1,558 @@
# battle.py
#
# A set of classes and convenience functions
# for representing a D&D 4e battle
#
# future features:
# * down combatants go into a separate list
from dice import Dice
import easyinput
# data about the battle - includes combatant list, etc
class Battle():
def __init__(self):
self.combatant_hash = {}
self.groups = []
self.current = None
self.round = None
def __str__(self):
ret = ''
if self.is_started():
ret = 'Battle underway, currently on round {}\n\n'.format(self.round)
else:
ret = 'Battle not yet started\n\n'
ret = ret + 'Combatants\n==========\n'
ret = ret + self.format_combatants()
return ret
def is_started(self):
return self.current != None
def add_group(self, group):
# If battle is already going, need to know
# where in the init order the new mooks go
if self.is_started():
if group.is_solo_group() and group.members[0].pc:
group.set_init(easyinput.input_int('Initiative for {}'.format(group.name)))
else:
group.roll_init()
added = False
for i in range(len(self.groups)):
if group.init > self.groups[i].init:
self.groups.insert(i, group)
added = True
break
if not added:
self.groups.append(group)
else:
self.groups.append(group)
for c in group.members:
self.combatant_hash[c.index] = c
def get_current_group(self):
if self.current != None:
return self.groups[self.current]
else:
return None
# If you're in a multi-member group, the first
# member of the group gets returned.
def get_current_combatant(self):
if self.current != None:
return self.groups[self.current].members[0]
else:
return None
def get_combatant(self, index):
if index in self.combatant_hash:
return self.combatant_hash[index]
else:
return None
def choose_combatant(self):
print self.format_combatants()
index = easyinput.input_int('choose combatant')
if index not in self.combatant_hash:
print 'Error: {} is not a valid index'.format(index)
return self.choose_combatant()
return self.combatant_hash[index]
def begin(self):
if self.is_started():
print "Error: battle is already running"
for g in self.groups:
if g.is_solo_group() and g.members[0].pc:
g.set_init(easyinput.input_int('Initiative for {}'.format(g.name)))
else:
g.roll_init()
self.groups.sort(reverse=True, key=lambda group: group.init)
self.current = 0
print '\nInitiative Roster:\n'
for g in self.groups:
print '{} ({})'.format(g.name, g.init)
print ''
self.next_round()
self.get_current_group().begin_turn()
# Returns a formatted string with all of the combatants
def format_combatants(self):
ret = ''
for g in self.groups:
if g.is_solo_group():
ret = ret + '{}\n'.format(g.members[0])
else:
ret = ret + '{}:\n'.format(g.name)
for c in g.members:
ret = ret + ' {}\n'.format(c)
return ret.rstrip()
# Returns a formatted string with just the current group
def format_current_group(self):
if self.validate_started():
return self.validate_started()
g = self.groups[self.current]
if g.is_solo_group():
return '{}'.format(g.members[0].format_full_info())
else:
ret = '{}\n'.format(g.name)
for c in g.members:
ret = ret + ' {}\n'.format(c)
return ret.rstrip()
def next_combatant(self):
if self.validate_started():
print self.validate_started()
return
g = self.get_current_group()
g.end_turn()
self.current += 1
if self.current >= len(self.groups):
self.current = 0
self.next_round()
g = self.get_current_group()
g.begin_turn()
def next_round(self):
if self.round == None:
self.round = 1
else:
self.round += 1
# Decrement all timed conditions
for c in self.combatant_hash.values():
c.tick_conditions()
print 'Beginning round {}'.format(self.round)
def validate_started(self):
if not self.is_started():
return 'Error: you can only run this command after starting the battle'
return None
class CombatGroup():
# What we're mostly getting here is a definition of the *members*
# of the group... then we build them all and stick them in
# the group
@classmethod
def from_input(cls):
name = easyinput.input_str("name")
hp = easyinput.input_int('hp')
init_mod = easyinput.input_int('init mod', 0)
ap = easyinput.input_int('action points', 0)
surges = easyinput.input_int('healing surges', 0)
recharges = []
while True:
data = []
data = easyinput.input_str("recharge", default='').split(',')
if len(data) == 2:
recharge = {}
recharge['name'] = data[0]
recharge['value'] = int(data[1])
recharge['used'] = False
recharges.append(recharge)
else:
break
count = easyinput.input_int('count', 1)
# Now make the combatants...
members = []
for i in range(count):
members.append(Combatant(name, hp, pc=False, surges=surges, ap=ap, sw=0, recharges=recharges))
if count > 1:
name = name + 's'
return CombatGroup(name, members, init_mod)
def __init__(self, name, members, init_mod=0):
self.name = name
self.members = members
self.init_mod = init_mod
self.init = 0
def roll_init(self):
d = Dice.from_str('1d20+{}'.format(self.init_mod))
self.set_init(d.roll()['total'])
def set_init(self, init):
self.init = init
def add_member(self, c):
self.members.append(c)
def is_solo_group(self):
return len(self.members) == 1
def begin_turn(self):
print '{} {} initiative.'.format(self.name, ['has', 'have'][len(self.members) != 1])
for c in self.members:
c.begin_turn()
def end_turn(self):
for c in self.members:
c.end_turn()
class Combatant():
next_index = 0
def __init__(self, name, hp, pc=False, init_mod=0, surges=0, ap=0, sw=0, recharges=[]):
self.name = name
self.max_hp = hp
self.hp = self.max_hp
self.temp_hp = 0
self.pc = pc
self.surges = surges
self.ap = ap
self.sw = sw
self.conditions = {}
self.index = Combatant.next_index
self.next_condition_index = 0
Combatant.next_index += 1
self.recharges = {}
recharge_index = 0
for r in recharges:
r['index'] = recharge_index
r['just_used'] = False
self.recharges[recharge_index] = r
recharge_index += 1
def __str__(self):
return "{}: {} ({})".format(self.index, self.name, self.format_health_summary())
# cond_type can be 's' or 't', for 'save' or 'timed'. If it is 't', condition expires at the end of the players turn
# 'duration' rounds from now
def add_condition(self, name, cond_type, duration=None, end_type='e'):
condition = {}
condition['name'] = name
condition['cond_type'] = cond_type
condition['duration'] = duration
condition['end_type'] = end_type
condition['index'] = self.next_condition_index
if cond_type == 'timed' and duration == None:
print 'Error: specified a timed condition with no duration.'
return
self.conditions[self.next_condition_index] = condition
self.next_condition_index += 1
# Removes a condition, prints a message about it, and returns a copy of the condition
def remove_condition(self, index):
if index not in self.conditions:
print "Error: invalid condition index."
return None
c = self.conditions.pop(index)
print '{} is no longer affected by {}.'.format(self, c['name'])
return c
def choose_condition(self):
if not len(self.conditions):
print '{} has no conditions.'.format(self)
return None
print self.format_condition_summary()
index = easyinput.input_int('choice')
if index not in self.conditions:
print 'Error: {} is not a valid index'.format(index)
return self.choose_condition()
return self.conditions[index]
def tick_conditions(self):
for c in self.conditions.values():
if c['cond_type'] == 't':
c['duration'] -= 1
def begin_turn(self):
for c in self.conditions.values():
if c['cond_type'] == 't' and c['end_type'] == 's':
if c['duration'] <= 0:
self.remove_condition(c['index'])
else:
print '{} is still affected by {} ({} round{} left).'.format(self, c['name'], c['duration'], 's'[c['duration']==1:])
def end_turn(self):
for c in self.conditions.values():
if c['cond_type'] == 's':
r = None
if self.pc:
print '{}: save against {}.'.format(self, c['name'])
r = easyinput.input_int('saving throw')
else:
save_die = Dice.from_str('1d20')
r = save_die.roll()['total']
if r >= 10:
self.remove_condition(c['index'])
print '{} successfully saved against {}.'.format(self, c['name'])
else:
print '{} failed a save against {}.'.format(self, c['name'])
elif c['cond_type'] == 't' and c['end_type'] == 'e':
if c['duration'] <= 0:
self.remove_condition(c['index'])
else:
print '{} is still affected by {} ({} round{} left).'.format(self, c['name'], c['duration'], 's'[c['duration']==1:])
for r in self.recharges.values():
if r['used']:
if r['just_used']:
r['just_used'] = False
continue
# Roll to recharge
d = Dice.from_str('1d6')
n = d.roll()['total']
if n >= r['value']:
r['used'] = False
print '{} can use {} again!'.format(self, r['name'])
def damage(self, amount):
was_bloodied = self.is_bloodied()
if self.temp_hp > 0:
self.temp_hp -= amount
if self.temp_hp < 0:
amount = abs(self.temp_hp)
self.temp_hp = 0
self.hp -= amount
print '{} took {} points of damage.'.format(self, amount)
if self.is_down():
print '{} is down!'.format(self)
elif self.is_bloodied() and not was_bloodied:
print '{} is bloodied!'.format(self)
def heal(self, amount):
was_down = self.is_down()
was_bloodied = self.is_bloodied()
if self.hp < 0:
self.hp = 0
amount_healed = amount
if self.hp + amount_healed > self.max_hp:
amount_healed = self.max_hp - self.hp
self.hp += amount_healed
if was_down:
print '{} regains consciousness.'.format(self)
print '{} regained {} hit points.'.format(self, amount_healed)
if was_bloodied and not self.is_bloodied():
print '{} is no longer bloodied.'.format(self)
elif was_bloodied and self.is_bloodied():
print '{} is still bloodied.'.format(self)
def add_temp_hp(self, amount):
self.temp_hp += amount
print '{} gained {} temporary hit points.'.format(self, amount)
def use_surge(self, heal=True):
if self.surges <= 0:
print '{} has no healing surges.'.format(self)
return
self.surges -= 1
noheal = ''
if not heal:
noheal = " (but didn't regain hit points)"
print '{} spent a healing surge{}.'.format(self, noheal)
if heal:
self.heal(self.max_hp / 4)
def use_second_wind(self):
if self.sw <= 0:
print "{} doesn't have a second wind.".format(self)
return
self.surges -= 1
print '{} got their second wind!'.format(self)
# Now the actual effects of the SW
self.use_surge()
self.add_condition('Second Wind (+2 all def)', 't', 1)
def use_recharge_power(self, index):
if index not in self.recharges:
print "Error: Invalid recharge index"
return
self.recharges[index]['used'] = True
self.recharges[index]['just_used'] = True
def choose_recharge_power(self):
if not len(self.recharges):
print '{} has no rechargable powers.'.format(self)
return None
print self.format_recharge_summary()
index = easyinput.input_int('choice')
if index not in self.recharges:
print 'Error: {} is not a valid index'.format(index)
return self.choose_recharge_power()
return self.recharges[index]
def is_bloodied(self):
return self.hp <= self.max_hp / 2
def is_down(self):
return self.hp <= 0
def format_full_info(self):
return """{name}
{separator}
index: {index}
hp: {hp}/{max_hp}
temp hp: {temp_hp}
surges: {surge}
ap: {ap}
sw: {sw}
conditions:
{conditions}
recharge powers:
{recharge}""".format(index=self.index, name=self.name, hp=self.hp, max_hp=self.max_hp, temp_hp=self.temp_hp, surge=self.surges, ap=self.ap, sw=self.sw, conditions=self.format_condition_summary(' '), recharge=self.format_recharge_summary(' '), separator='='*len(self.name))
def format_health_summary(self):
bloodied = ''
temp_info = ''
if self.is_bloodied():
bloodied = ', bloodied'
if len(self.conditions):
bloodied = bloodied + ', '
if self.temp_hp > 0:
temp_info = ', {} temp hp'.format(self.temp_hp)
return '{} hp{}{}{}'.format(self.hp, temp_info, bloodied, ', '.join([x['name'] for x in self.conditions.values()]))
def format_condition_summary(self, initial=''):
summary = ''
for (index, c) in self.conditions.items():
type_string = ''
if c['cond_type'] == 's':
type_string = 'Save Ends'
elif c['cond_type'] == 't':
type_string = '{} Round{}'.format(c['duration'], 's'[ c['duration']==1: ] )
summary = summary + '{}{}: {} ({})\n'.format(initial, index, c['name'], type_string)
return summary.rstrip()
def format_recharge_summary(self, initial):
summary = ''
for (index, r) in self.recharges.items():
summary = summary + '{}{}: {} (Recharge: {}, Available: {})\n'.format(initial, index, r['name'], r['value'], ['Yes', 'No'][ r['used'] ])
return summary.rstrip()
# Data parsing helper function
# Modelled on the data_input functions in easyinput
def do_combatant_select(battle, data):
c = None
if len(data) >= 1:
c = battle.get_combatant(int(data[0]))
if not c:
print 'Error: Invalid combatant index.'
else:
c = battle.get_current_combatant()
if not c:
print 'Error: Battle not started and no combatant specified.'
return c

36
lib/easyinput.py Normal file
View File

@ -0,0 +1,36 @@
# easyinput.py
#
# These are just some simple helper functions for doing input. They produce a prompt, allow default values,
# and allow conditional prompting based on the presence or absence of data in a list
def input_str(prompt, default=None, show_default=False, prompt_str=':'):
full_prompt = prompt
if default != None and show_default:
full_prompt = full_prompt + ' [{}]'.format(default)
full_prompt = full_prompt + '{} '.format(prompt_str)
data = raw_input(full_prompt)
if not data:
if default == None:
print 'Error: you must provide a value!'
return input_str(prompt, default, show_default, prompt_str)
else:
return default
else:
return data
def input_int(prompt, default=None, show_default=True, prompt_str=':'):
return int(input_str(prompt, default, show_default))
def do_data_input_str(data, index, prompt, default=None, show_default=False, prompt_str=':'):
if len(data) >= index + 1:
return data[index]
else:
return input_str(prompt, default, show_default, prompt_str)
def do_data_input_int(battle, data, index, prompt, default=None, show_default=True, prompt_str=':'):
return int(do_data_select_str(battle, data, index, prompt, default, show_default, prompt_str))