Jump to content

Kings of War Combat Simulator


McNathanson

Recommended Posts

Hey folks I thought those of you interested in Kings of War might have fun playing around with the combat sim I wrote.  It's pretty basic and doesn't have a lot of special rules support (just Crushing Blow and Thunderous Charge for now, no Phalanx, etc.).  But it will show you a Unit 1 vs Unit 2 combat results distribution, including how many rounds before the loser routed, which is useful info.

 

I'm attaching the code in a .zip and also pasting below if you prefer to just copy/paste into a .py file yourself.

Enjoy!

Nathan

 

kow-combat-sim-v0.1.zip

# Kings of War Combat Simulator version 0.1
# (c) Nathan Heldt-Sheller July 2015
#
# Usage:
# 1) Download and install Python 2.7.x
# 2) From a command prompt execute the script:
#       python kow-combat-sim-v0.1.py
# 3) Choose the KoW Combat Sim option (enter for default)
# 4) Choose a number of Trials to run (up to 99999)
# 5) Look at the distribution printout to see how many Trials each
#    unit won, and which round they won in.
#
# NOTE: Before running a lot of trials, you will want to disable the printouts
# except the stats print.  Change the "debugmsgs = True" on line ~299 (top of
# main() function) to "debugmsgs = False" to disable all the debug prints.
#
# NOTE: There are a lot of special rules not yet supported... only Crushing Blow
# and Thunderous Charge are supported currently.
#
# NOTE: This may have bugs, I wrote it in just one sitting.  It's also my first use
# of Python so I'm sure it's ugly code to a real Python programmer... sorry ;)
#
# NOTE: To change the units that are fighting just edit the info in the script
# below (search for "example unit stats").
#
# There's also a tool for rolling sequential handfuls of D6 just in case you
# want to play around with it.
#
# Thanks,
# Nathan

import random
import os

STEADY = "steady"
DISORDERED = "disordered"
WAVERED = "wavered"
ROUTED = "routed"

def rolld6countsuccesses(num_rolls, min_success_val, debugmsgs):
    # debugmsgs = False

    if debugmsgs: print "\trolling %d dice looking for %d+..." % \
        (num_rolls, min_success_val)
    success_count = 0
    for i in range(num_rolls):
        die = random.randint(1, 6)
        if(die >= min_success_val) : success_count += 1
        # if debugmsgs: print "die = %d, success_count = %d" % (die, success_count)
    if debugmsgs: print "\t... %d successes." % success_count
    return success_count

def roll2d6countsuccesses(num_rolls, min_success_val, debugmsgs):
    # debugmsgs = False

    success_count = 0
    for i in range(num_rolls) :
        die1 = random.randint(1, 6)
        die2 = random.randint(1, 6)
        if(die1 + die2) >= min_success_val : success_count += 1
        if(debugmsgs):
            print "die1 = %d, die2 = %d, success_count = %d" % \
                (die1, die2, success_count)
    return success_count

def prompt_for_int(prompt, min_input, max_input, default, debugmsgs):
    # debugmsgs = True

    # check args
    if(default < min_input or default > max_input):
        raise ValueError(\
            "bad args, default %d not within min/max range of %d - %d" % \
                (default, min_input, max_input))

    # prompt user
    x = raw_input(prompt + " [%d]: " % default)
    # if user enters '' assign default
    if x == '':
        try:
            retval = int(default)
        except:
            raise ValueError("bad args, default not an int")
    # else assign the input to retval if its an int
    else:
        try:
            retval = int(x)
            # check range
            if(retval < min_input or retval > max_input):
                raise ValueError("bad input, value out of range")
        except:
            if x == "q":
                raise ValueError("'q' => quit...")
            else:
                raise ValueError("bad input: not 'q', and not an int")
    return retval

def d6rollersim(dice, debugmsgs):
    # debugmsgs = True

    trials = 1
    rolls = 10
    value_needed = 4
    successes = 2 # start with 2 so that the starting default rolls is 2

    if dice == 1:
        print "Rolling 1 die at a time."
    else:
        print "Rolling %d dice at a time." % dice
    while True :
        try:
            # dice = prompt_for_int("Roll 1 or 2 dice at a time?", 1, 2, dice, debugmsgs)
            rolls = prompt_for_int("Number of rolls to attempt?", 0, 9999, successes, debugmsgs)
            value_needed = prompt_for_int("Roll needed for success?", 1, 12, value_needed, debugmsgs)
            trials = prompt_for_int("Trials to average?", 1, 9999, trials, debugmsgs)
        except ValueError as e:
            print e #"prompt_for_int() raised an exception '%s'; breaking..." % (e)
            break
        print "Starting %d trials, rolling %d dice needing %d or better" \
        % (trials, rolls, value_needed)
        successes = 0
        for i in range(trials):
            # successes += rolld6countsuccesses(rolls, value_needed, debugmsgs)
            if(dice == 1):
                successes += rolld6countsuccesses(rolls, value_needed, debugmsgs)
            elif(dice == 2):
                successes += roll2d6countsuccesses(rolls, value_needed, debugmsgs)
            else:
                print("Unsupported number of dice, goodbye!")
                break
        successes /= trials;
        print "\tAverage successes in {0} trials = {1} out of {2}".format\
            (trials, successes, rolls)

def kowfightcombat(tohit, towound, attacks, debugmsgs):
    # debugmsgs = True

    if debugmsgs: print "\tattacking %d times needing %d to hit and %d to wound" \
        % (attacks, tohit, towound)
    hits = rolld6countsuccesses(attacks, tohit, debugmsgs)
    wounds = rolld6countsuccesses(hits, towound, debugmsgs)
    return wounds

def kowcalculatewounds(attacker, defender, melee, defense, attacks, cb, tc, nerve_state, debugmsgs):
    # debugmsgs = True

    tohit = max(1, melee[attacker])
    if debugmsgs: print "Attacker = Unit %d; Defender = Unit %d" % \
        (attacker + 1, defender + 1)
    if nerve_state[attacker] == STEADY:
        if debugmsgs: print "\tapplying thunderous_charge[%d]" % tc[attacker]
        towound = max(1, defense[defender] - cb[attacker] - tc[attacker])
    else:
        towound = max(1, defense[defender] - cb[attacker])
    wounds_dealt = kowfightcombat(tohit, towound, attacks[attacker], debugmsgs)
    if debugmsgs: print "\tAttacker's charge dealt %d wounds" % wounds_dealt
    return wounds_dealt

def kownervetest(waver, route, dmg, debugmsgs):
    # debugmsgs = True

    nerve_roll = random.randint(1, 6) + random.randint(1, 6)

    if nerve_roll == 12:
        if debugmsgs: print "\tnerve roll %d (%d total): we are doomed!" % \
            (nerve_roll, nerve_roll + dmg)
        return WAVERED
    if nerve_roll == 2:
        if debugmsgs: print "\tnerve roll %d (%d total): hold your ground!" % \
            (nerve_roll, nerve_roll + dmg)
        return STEADY
    if (dmg + nerve_roll) >= route:
        if debugmsgs: print "\tnerve roll %d (%d total) => " % \
            (nerve_roll, nerve_roll + dmg) + ROUTED
        return ROUTED
    if (dmg + nerve_roll) >= waver:
        if debugmsgs: print "\tnerve roll %d (%d total) => " % \
            (nerve_roll, nerve_roll + dmg) + WAVERED
        return WAVERED
    if debugmsgs: print "\tnerve roll %d (%d total) => " % \
        (nerve_roll, nerve_roll + dmg) + STEADY
    return STEADY

def kowdocharge(attacker, defender, melee, defense, attacks, cb, tc,
    dmg, nerve_state, waver, route, debugmsgs):
    # debugmsgs = True

    if debugmsgs: print # blank line to separate each charge
    wounds_dealt = kowcalculatewounds(attacker, defender, melee, defense,
        attacks, cb, tc, nerve_state, debugmsgs)
    dmg[defender] += wounds_dealt
    if wounds_dealt > 0:
        nerve_test = kownervetest(waver[defender], route[defender], dmg[defender], debugmsgs)
        if nerve_test == STEADY:
            nerve_state[defender] = DISORDERED
        elif nerve_test == WAVERED:
            nerve_state[defender] = WAVERED
        elif nerve_test == ROUTED:
            nerve_state[defender] = ROUTED
    else:
        nerve_state[defender] = STEADY

    if debugmsgs: print "Unit %d: dmg = %d; nerve state = " % (defender + 1,
        dmg[defender]) + nerve_state[defender]

def kowprintsimstats(trials, stats, max_rounds, debugmsgs):
    debugmsgs = False # the debugmsgs for this function are confusing; disable

    unit_wins = []
    for i in range(max_rounds +1):
        unit_wins.append([0,0])
    for i in range(trials):
        if debugmsgs: print stats[i]
        final_combat_round = stats[i][0]
        if debugmsgs: print "\nTrial %d went %d rounds" % (i, final_combat_round)
        if(stats[i][1] == ROUTED):
            unit_wins[final_combat_round][1] += 1
        else:
            unit_wins[final_combat_round][0] += 1

    print "\nKoW Combat Simulation Results"
    print "(Distribution of Wins by Combat Round and Unit):"
    print ' {0:<15s}  {1:^15s}  {2:<15s}'.format("Rounds\n Fought", "Unit 1 win",
        "Unit 2 win")
    for i in range(1,max_rounds+1):
        combat_round = i
        unitAwins = unit_wins[i][0]
        unitBwins = unit_wins[i][1]
        print '  {0:<15n}  {1:<15n}  {2:<15n}'.format(combat_round, unitAwins, unitBwins)
    print

def kowcombatsim(trials, debugmsgs):
    # debugmsgs = True

    try:
        trials = prompt_for_int("Trials", 1, 99999, trials, debugmsgs)
    except ValueError:
        trials = 1
    print "KoW combat using %d trials:" % trials

    # example unit stats for 10 Knights vs 40 Shield Wall
    # copy/paste these numbers to create a different matchup
    # note that the left value is always the first attacker, and the right
    # value is the first defender
    melee = [3,4] # melee value for [Unit 1, Unit 2]
    defense = [5,4] # etc
    attacks = [16,25]
    waver = [14,20]
    route = [16,22]
    crushing_blow = [0,0]
    thunderous_charge = [2,0]
    damage = [0,0] # for recording damage taken... leave at zero unless you
                    # want to start the Trial with a damaged unit

    # start the trials
    stats = []
    max_rounds = 1
    for i in range(trials):
        combat_round = 0
        nerve_state = [STEADY, STEADY]
        damage = [0,0]
        while True:
            # for human readability, round '1' is the first round
            combat_round += 1
            if debugmsgs: print "\nTRIAL %d - COMBAT ROUND %d" % (i+1, combat_round)

            # UNIT 1's TURN
            attacker = 0
            defender = 1
            # if not wavered, 0 charges 1
            if nerve_state[attacker] != WAVERED:
                kowdocharge(attacker, defender, melee, defense, attacks,
                    crushing_blow, thunderous_charge, damage, nerve_state,
                    waver, route, debugmsgs)
                if nerve_state[defender] == ROUTED:
                    break
            # if no charge, reset defender nerve
            else:
                nerve_state[defender] = STEADY

            # UNIT 2's TURN
            attacker = 1
            defender = 0
            # if not wavered, 1 countercharges 0
            if nerve_state[attacker] != WAVERED:
                kowdocharge(attacker, defender, melee, defense, attacks,
                    crushing_blow, thunderous_charge, damage, nerve_state,
                    waver, route, debugmsgs)
                if nerve_state[defender] == ROUTED:
                    break
            # if no charge, reset defender nerve
            else:
                nerve_state[defender] = STEADY

        # trial over, record stats
        if combat_round > max_rounds:
            max_rounds = combat_round
        trial_results = [combat_round, nerve_state[0], damage[0], nerve_state[1], damage[1]]
        if debugmsgs: print "Recording Trial Results:", trial_results
        stats.append(trial_results)

    # simulation complete, print stats
    kowprintsimstats(trials, stats, max_rounds, debugmsgs)

def main():

    # debugmsgs = False # use these "top level" vars to set for the whole script
    debugmsgs = True # use these "top level" vars to set for the whole script
    # NOTE: disable HERE to run a lot of Trials without printing forever!

    os.system('clear')
    print "Welcome to Nathan's KoW combat sim v0.1; enter 'q' at any prompt to quit."

    mode = 1
    while mode > 0:
        print "1) Simulate a KoW combat"
        print "2) Roll handfuls of D6"
        try:
            mode = prompt_for_int("Choose a mode, or 'q' to quit", 1, 2, 1, debugmsgs)
        except ValueError as e:
            print e
            mode = 0 # exit

        if mode == 1:
            trials = 1
            kowcombatsim(trials, debugmsgs)
        elif mode == 2:
            d6rollersim(1, debugmsgs)

    print "quitting, goodbye!"

if __name__ == "__main__":
    main()

Link to comment
Share on other sites

It's really just as easy as running the script (see the "# Usage:" comment at the top).  You can Google "installing Python 2.7" and "running a Python script on Windows" and you'll get what you need.

 

After starting the script (step #2 in the Usage instructions), press enter a bunch and you'll see a KoW combat get simulated :)

 

NtK

Link to comment
Share on other sites

The instructions are at the beginning, you'll need to download Python to run the code. It would be awesome if it were compiled into a sweet UI though, and you append functionality for additional ability variables as they become available. It looks neat though, thanks for putting it together!

Link to comment
Share on other sites

Thanks Sammy!  I think if and when I spend more time on it, it will be to add support for other abilities, and then maybe take .xml input for unit stats.

 

Honestly I'm not sure I'll continue in Python though, I'm not finding it significantly simpler than the same code would be in Java which I'm more familiar with (and which can easily be wrapped in an Andriod UI).  But if I instead take the time to write up an HTML interface, I'll keep the backing code in Python.  

All depends I guess on whether I get into KoW... I'm still waiting for my full copy of the rules to playtest!

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
×
×
  • Create New...