2012-03-20 19:53:06 +00:00
#!/usr/bin/python
#
# battleman.py - RPG Battle Manager
#
# A table-top RPG battle flow manager
# Tuned pretty specifically to D&D 4e for now... need a templatized system
# to do anything fancier... may develop that at some point.
2012-03-22 16:03:44 +00:00
#
# future features:
# * an option for passing in multiple files that contain combatant definitions
2012-03-23 22:34:57 +00:00
2012-03-29 22:28:21 +00:00
import sys
sys . path . append ( ' lib/ ' )
2012-03-23 16:14:00 +00:00
2012-04-03 05:17:52 +00:00
import shelve
2012-03-29 22:06:09 +00:00
import argparse
2012-04-05 21:06:17 +00:00
from cmd import Cmd
2012-03-29 21:07:18 +00:00
import os . path
2012-03-29 22:28:21 +00:00
import battle
from battle import CombatGroup
from battle import Combatant
import easyinput
2012-03-22 22:07:25 +00:00
2012-03-29 22:06:09 +00:00
def main ( ) :
2012-03-29 22:28:21 +00:00
btl = battle . Battle ( )
2012-03-29 21:07:18 +00:00
2012-04-03 05:17:52 +00:00
### Shelf data
session = None
SESSION_FILE = os . path . expanduser ( ' ~/.config/4etools/battleman/battleman-restore ' )
2012-03-29 22:06:09 +00:00
###
2012-03-29 21:07:18 +00:00
# Make sure config directory exists
2012-04-03 05:17:52 +00:00
if not os . path . exists ( os . path . dirname ( SESSION_FILE ) ) :
os . makedirs ( os . path . dirname ( SESSION_FILE ) )
2012-03-29 21:07:18 +00:00
2012-03-29 22:06:09 +00:00
# Get command-line args
settings = parse_args ( )
2012-03-21 22:31:11 +00:00
2012-03-20 19:53:06 +00:00
2012-03-22 17:25:02 +00:00
print " Welcome to 4e Battle Manager. \n "
2012-03-29 22:06:09 +00:00
# Resume battle if needed
2012-04-03 05:17:52 +00:00
session = shelve . open ( SESSION_FILE )
2012-03-29 22:06:09 +00:00
if settings . resume :
try :
2012-04-03 05:17:52 +00:00
btl = session [ ' battle ' ]
Combatant . next_index = session [ ' combatant_next_index ' ]
2012-03-29 22:06:09 +00:00
except :
2012-04-03 05:17:52 +00:00
print " Error: Couldn ' t resume. Quitting to preserve our old session. "
2012-03-29 22:06:09 +00:00
sys . exit ( 1 )
else :
2012-05-04 15:27:04 +00:00
for f in settings . files :
for g in battle . combatgroups_from_file ( f ) :
btl . add_group ( g )
2012-05-04 15:27:37 +00:00
2012-03-22 16:03:44 +00:00
2012-03-29 22:28:21 +00:00
print btl
2012-03-30 04:55:34 +00:00
2012-04-03 05:17:52 +00:00
cmd_parser = CommandParser ( btl , session )
2012-03-30 04:55:34 +00:00
cmd_parser . cmdloop ( )
2012-04-03 05:17:52 +00:00
session . close ( )
2012-03-30 04:55:34 +00:00
2012-04-05 21:06:17 +00:00
class CommandParser ( Cmd ) :
2012-03-30 04:55:34 +00:00
""" Parse the commands from the command-line. """
2012-04-03 05:17:52 +00:00
def __init__ ( self , btl , session ) :
2012-04-05 21:06:17 +00:00
Cmd . __init__ ( self )
2012-03-30 04:55:34 +00:00
self . btl = btl
2012-04-03 05:17:52 +00:00
self . session = session
self . session_io_failed = False
2012-04-01 21:09:37 +00:00
self . doc_header = ' Available commands (type help <command> for more help) '
2012-03-30 04:55:34 +00:00
self . prompt = ' \n > '
def postloop ( self ) :
2012-04-03 05:17:52 +00:00
# Just dumbly dump to the shelf DB
try :
self . session [ ' battle ' ] = self . btl
self . session [ ' combatant_next_index ' ] = Combatant . next_index
except Exception :
if not self . session_io_failed :
print ( " Warning: can ' t write to the session database. Resuming later will fail. " )
self . session_io_failed = True
2012-03-30 04:55:34 +00:00
2012-03-31 21:38:01 +00:00
# This allows us to do partial command completion without <tab>,
# as long as
def default ( self , line ) :
2012-04-05 21:06:17 +00:00
comm , data , line = self . parseline ( line )
cmds = self . completenames ( comm )
2012-03-31 21:38:01 +00:00
num_cmds = len ( cmds )
if num_cmds == 1 :
2012-04-05 21:16:21 +00:00
return getattr ( self , ' do_ ' + cmds [ 0 ] ) ( data )
2012-03-31 21:38:01 +00:00
elif num_cmds > 1 :
2012-04-06 06:04:04 +00:00
print ' Error: Ambiguous command: {} ' . format ( comm )
2012-03-31 21:38:01 +00:00
else :
2012-04-06 06:04:04 +00:00
print ' Error: Unrecognized command: {} ' . format ( comm )
2012-03-31 21:38:01 +00:00
2012-04-01 21:09:37 +00:00
# We are overriding do_help to avoid printing info about
# undocumented commands
def do_help ( self , arg ) :
if arg :
2012-04-05 21:06:17 +00:00
Cmd . do_help ( self , arg )
2012-04-01 21:09:37 +00:00
else :
# Everything from here to the end is lifted straight
2012-04-05 21:06:17 +00:00
# out of cmd.Cmd.do_help()
2012-04-01 21:09:37 +00:00
names = self . get_names ( )
cmds_doc = [ ]
cmds_undoc = [ ]
help = { }
for name in names :
if name [ : 5 ] == ' help_ ' :
help [ name [ 5 : ] ] = 1
names . sort ( )
# There can be duplicates if routines overridden
prevname = ' '
for name in names :
if name [ : 3 ] == ' do_ ' :
if name == prevname :
continue
prevname = name
cmd = name [ 3 : ]
if cmd in help :
cmds_doc . append ( cmd )
del help [ cmd ]
elif getattr ( self , name ) . __doc__ :
cmds_doc . append ( cmd )
else :
cmds_undoc . append ( cmd )
self . stdout . write ( " %s \n " % str ( self . doc_leader ) )
self . print_topics ( self . doc_header , cmds_doc , 15 , 80 )
self . print_topics ( self . misc_header , help . keys ( ) , 15 , 80 )
# self.print_topics(self.undoc_header, cmds_undoc, 15,80)
2012-03-30 04:55:34 +00:00
def do_add ( self , line ) :
""" add [N]
Add the specified number of groups """
2012-03-31 20:20:44 +00:00
data = parse_data ( line )
2012-03-30 04:55:34 +00:00
if len ( data ) > = 1 :
num_groups = int ( data [ 0 ] )
else :
num_groups = easyinput . input_int ( ' number of groups ' )
for i in range ( 1 , num_groups + 1 ) :
print " Adding group {} " . format ( i )
self . btl . add_group ( CombatGroup . from_input ( ) )
def do_begin ( self , line ) :
""" begin
Begins the battle . Rolls initiative for NPCs and prompts for PCs """
self . btl . begin ( )
def do_print ( self , line ) :
""" print [index]
Print detailed info for combatant with index , or combatant or group with initiative """
2012-03-31 20:20:44 +00:00
data = parse_data ( line )
2012-03-30 04:55:34 +00:00
if len ( data ) > = 1 :
c = self . btl . get_combatant ( int ( data [ 0 ] ) )
if not c :
print ' Error: Invalid combatant index. '
else :
print c . format_full_info ( )
else :
print self . btl . format_current_group ( )
def do_list ( self , line ) :
""" list
Lists a summary of all of the combat groups and their members """
2012-03-29 21:07:18 +00:00
2012-03-30 04:55:34 +00:00
print self . btl . format_combatants ( )
2012-03-21 17:58:59 +00:00
2012-03-29 21:07:18 +00:00
2012-03-30 04:55:34 +00:00
def do_damage ( self , line ) :
""" damage [index] [amount]
Deals damage to the specified combatant """
2012-03-26 21:38:08 +00:00
2012-03-31 20:20:44 +00:00
data = parse_data ( line )
2012-03-24 01:46:53 +00:00
2012-03-30 04:55:34 +00:00
c = battle . do_combatant_select ( self . btl , data )
if not c :
return
2012-03-24 01:46:53 +00:00
2012-03-30 04:55:34 +00:00
if len ( data ) > = 2 :
amount = int ( data [ 1 ] )
else :
amount = easyinput . input_int ( ' damage ' )
2012-03-31 20:20:44 +00:00
c . damage ( amount )
2012-03-30 04:55:34 +00:00
2012-07-07 02:02:02 +00:00
def do_minionkill ( self , line ) :
""" damage [index list]
Deals 1 damage to each of the specified combatants . Used to indicate minion death """
data = parse_data ( line )
for index in data :
self . btl . get_combatant ( int ( index ) ) . damage ( 1 )
2012-03-30 04:55:34 +00:00
def do_heal ( self , line ) :
""" heal [index] [amount]
Heal hit points for the specified combatant """
2012-03-31 20:20:44 +00:00
data = parse_data ( line )
2012-03-30 04:55:34 +00:00
c = battle . do_combatant_select ( self . btl , data )
2012-03-25 18:35:20 +00:00
if not c :
2012-03-30 04:55:34 +00:00
return
if len ( data ) > = 2 :
amount = int ( data [ 1 ] )
2012-03-25 18:35:20 +00:00
else :
2012-03-30 04:55:34 +00:00
amount = easyinput . input_int ( ' amount ' )
c . heal ( amount )
2012-03-25 18:35:20 +00:00
2012-03-30 04:55:34 +00:00
def do_temp ( self , line ) :
""" temp [index] [amount]
Add temporary hit points to the specified combatant """
2012-03-23 20:54:27 +00:00
2012-03-31 20:20:44 +00:00
data = parse_data ( line )
2012-03-23 20:54:27 +00:00
2012-03-30 04:55:34 +00:00
c = battle . do_combatant_select ( self . btl , data )
if not c :
return
if len ( data ) > = 2 :
amount = int ( data [ 1 ] )
else :
amount = easyinput . input_int ( ' amount ' )
2012-03-22 03:27:45 +00:00
2012-03-30 04:55:34 +00:00
c . add_temp_hp ( amount )
2012-03-22 03:27:45 +00:00
2012-03-23 20:54:27 +00:00
2012-03-30 04:55:34 +00:00
def do_rmtemp ( self , line ) :
""" rmtemp [index] [amount]
Remove temporary hit points from the specified combatant """
2012-03-23 20:54:27 +00:00
2012-03-30 04:55:34 +00:00
do_stub ( )
2012-03-23 16:14:00 +00:00
2012-03-30 04:55:34 +00:00
def do_surge ( self , line ) :
""" surge [index] [heal]
Combatant with index uses a healing surge . If heal is 0 , don ' t heal the combatant " " "
2012-03-23 21:09:23 +00:00
2012-03-31 20:20:44 +00:00
data = parse_data ( line )
2012-03-30 04:55:34 +00:00
c = battle . do_combatant_select ( self . btl , data )
if not c :
return
heal = True
if len ( data ) > = 2 and data [ 1 ] == ' 0 ' :
heal = False
c . use_surge ( heal )
2012-03-23 21:09:23 +00:00
2012-03-30 04:55:34 +00:00
def do_wind ( self , line ) :
""" wind [index]
Use Second Wind for combatant """
2012-03-23 21:09:23 +00:00
2012-03-31 20:20:44 +00:00
data = parse_data ( line )
2012-03-24 01:57:08 +00:00
2012-03-30 04:55:34 +00:00
c = battle . do_combatant_select ( self . btl , data )
if not c :
return
c . use_second_wind ( )
2012-03-24 01:57:08 +00:00
2012-03-22 22:21:17 +00:00
2012-03-30 04:55:34 +00:00
def do_cond ( self , line ) :
""" cond [index] [name] [type] [duration] [start|end]
Add a temporary condition to a combatant , optionally specifying the condition name , type ( s or t ) , duration and what phase of the combatant ' s turn it expires on " " "
2012-03-22 22:21:17 +00:00
2012-03-31 20:20:44 +00:00
data = parse_data ( line )
2012-03-23 21:09:23 +00:00
2012-03-30 04:55:34 +00:00
duration = None
end_type = ' e '
2012-03-23 21:09:23 +00:00
2012-03-30 04:55:34 +00:00
c = battle . do_combatant_select ( self . btl , data )
if not c :
return
2012-03-23 20:54:27 +00:00
2012-03-30 04:55:34 +00:00
name = easyinput . do_data_input_str ( data , 1 , ' condition name ' )
ctype = easyinput . do_data_input_str ( data , 2 , ' condition type ' , default = ' s ' , show_default = True )
2012-03-24 01:06:46 +00:00
2012-03-30 04:55:34 +00:00
if ctype == ' t ' :
duration = easyinput . do_data_input_int ( data , 3 , ' duration ' )
end_type = easyinput . do_data_input_str ( data , 4 , ' (s)tart|(e)nd ' , default = ' e ' , show_default = True )
2012-03-24 01:06:46 +00:00
2012-03-30 04:55:34 +00:00
c . add_condition ( name , ctype , duration , end_type )
2012-03-25 05:08:56 +00:00
2012-03-23 19:06:13 +00:00
2012-03-30 04:55:34 +00:00
def do_rmcond ( self , line ) :
""" rmcond [index] [condition_index]
Remove a condition from a combatant early . """
2012-03-23 19:06:13 +00:00
2012-03-31 20:20:44 +00:00
data = parse_data ( line )
2012-03-23 21:44:06 +00:00
2012-03-30 04:55:34 +00:00
c = battle . do_combatant_select ( self . btl , data )
if not c :
return
2012-03-23 21:44:06 +00:00
2012-03-30 04:55:34 +00:00
if len ( data ) > = 2 :
index = int ( data [ 1 ] )
else :
cond = c . choose_condition ( )
index = None
if cond :
index = cond [ ' index ' ]
2012-03-23 21:44:06 +00:00
2012-03-30 04:55:34 +00:00
if index != None :
c . remove_condition ( index )
2012-03-23 21:44:06 +00:00
2012-03-25 05:53:32 +00:00
2012-03-30 04:55:34 +00:00
def do_recharge ( self , line ) :
""" recharge [index] [recharge_index]
Use a rechargable power """
2012-03-25 05:53:32 +00:00
2012-03-31 20:20:44 +00:00
data = parse_data ( line )
2012-03-25 05:53:32 +00:00
2012-03-30 04:55:34 +00:00
c = battle . do_combatant_select ( self . btl , data )
if not c :
return
2012-03-25 05:53:32 +00:00
2012-03-30 04:55:34 +00:00
if len ( data ) > = 2 :
index = int ( data [ 1 ] )
else :
r = c . choose_recharge_power ( )
index = None
if r :
index = r [ ' index ' ]
2012-03-24 01:57:08 +00:00
2012-03-30 04:55:34 +00:00
if index != None :
c . use_recharge_power ( index )
2012-03-24 01:57:08 +00:00
2012-03-30 04:55:34 +00:00
def do_wait ( self , line ) :
""" wait
2012-04-03 03:42:31 +00:00
Removes the specified combatant from the initiative roster and add them to the wait list . """
2012-03-30 04:55:34 +00:00
2012-04-03 03:42:31 +00:00
data = parse_data ( line )
c = battle . do_combatant_select ( self . btl , data )
if not c :
return
self . btl . wait ( c . index )
2012-03-30 04:55:34 +00:00
def do_unwait ( self , line ) :
""" unwait
2012-04-03 03:42:31 +00:00
Removes the specified combatant from the wait list and adds them back into the initiative roster . """
2012-03-30 04:55:34 +00:00
2012-04-03 03:42:31 +00:00
data = parse_data ( line )
c = battle . do_combatant_select ( self . btl , data )
if not c :
return
self . btl . unwait ( c . index )
2012-03-30 04:55:34 +00:00
2012-03-31 21:41:10 +00:00
def do_next ( self , line ) :
""" next
Steps to the next combatant in initiative order . This handles saving throws , effects that end at beginning and ends of turns , and round incrementing . """
self . btl . next_combatant ( )
2012-03-30 04:55:34 +00:00
def do_sync ( self , line ) :
""" sync
2012-04-03 05:17:52 +00:00
Save the current battle data to the session database . Really not necessary , but nice to have for peace of mind . """
2012-03-31 21:41:10 +00:00
2012-04-03 05:17:52 +00:00
# Since the postloop does this, we don't actually need to
2012-03-31 21:41:10 +00:00
# do a damn thing here - just let it be an 'empty' command.
pass
2012-03-30 04:55:34 +00:00
def do_EOF ( self , line ) :
2012-04-05 21:16:21 +00:00
return self . do_quit ( line )
2012-03-30 04:55:34 +00:00
def do_quit ( self , line ) :
""" quit
Exits the program . If a battle is in progress , it is temporarily saved and can be resumed by running the program with - - resume next time . """
2012-04-01 21:09:37 +00:00
return True
2012-03-24 01:57:08 +00:00
2012-03-31 20:20:44 +00:00
def parse_data ( line ) :
data = line . split ( ' ' )
if data == [ ' ' ] :
data = [ ]
return data
2012-03-24 01:57:08 +00:00
def do_stub ( ) :
2012-03-25 18:20:09 +00:00
print " Sorry, this is a stub function "
2012-03-24 01:57:08 +00:00
2012-03-29 22:06:09 +00:00
def parse_args ( ) :
parser = argparse . ArgumentParser ( description = ' Command-line interface to manage battle data for D&D 4e ' , formatter_class = argparse . RawTextHelpFormatter )
parser . add_argument ( ' --resume ' , ' -r ' , action = ' store_true ' , help = ' Resume the battle from the last run of the program ' )
2012-05-04 15:27:04 +00:00
parser . add_argument ( ' files ' , nargs = argparse . REMAINDER , help = " A list of files containing combat groups to add to the initial battle. Ignored if --resume is specified. " )
2012-03-29 22:06:09 +00:00
return parser . parse_args ( )
2012-03-20 19:53:06 +00:00
if __name__ == ' __main__ ' :
main ( )