Home Forums Fantasy General Fantasy Frostgrave Combat Simulation

Viewing 10 posts - 1 through 10 (of 10 total)
  • Author
    Posts
  • #45657
    Jonathan Gingerich
    Participant

    A question that soon arises when looking at Frostgrave – How much better is a knight than a thug? To answer the question, I whipped up a little Python program and ran a million trials of a knight vs thug fight to the death. The results were pretty convincing, about a one in eight chance for the thug with the basic rules.

    Knight vs thug to the death, no optional rules.

    Knight wins:87.3% Draws:0.7% Thug wins:12.0%

    As might be expected, the critical hit rule gives a big boost to the thug as it’s an equalizer. The wound rule had much less effect. Together, the odds changed to about one in six – roll a die and the thug wins on a boxcar;-)

    Knight vs thug to the death, optional rules.

    Knight wins:81.5% Draws:0.9% Thug wins:17.6%

    That was fun, but I wanted more. How about two thugs against a knight? The mechanism was all worked out, but choices needed to be made. The first question that occured to me to ask – is it better for the knight to attack the healthiest thug, or the most damaged? The former will be more economical in that the coup de grace will not waste as many damage points; The latter will remove the thugs support advantage more quickly. Running the simulations made it clear. The best strategy is to attack the weakest.
    I then realized the thugs had a choice too. Which thug to attack the knight first? If the weaker thug attacked and died, the survivor would lose the support bonus. But if the stronger attacked first, then is a small chance they would survive where the weaker thug would not. And so the second thug would retain the support bonus for one more round. So a simple answer. In a million tries, it makes a small but measurable difference.
    But one of the characteristic things about Frostgrave is that combat is entirely symmetrical. It doesn’t matter if the knight attacks a thug or the thug attacks the knight, it’s all the same. So should you attack at all? Well, since the knight is always going to attack the weakest thug, there is no reason for the weakest thug to attack the knight, as that simply gives the knight two bites of the apple.
    So things became clear: the knight would always attack the weakest thug, only the strongest thug would attack the knight. Which might well mean both thugs attack the knight if the first combat caused the strongest thug to take enough damage to become the weaker!-)
    Well almost everything was clear. What if both thugs had equal health? Since the knight had a slim overall advantage, I first set it up so that the knight attacked when the thugs were equal, and the thugs did not. But then I realized the advantage should be calculated from the current state, rather than overall. Which was a hard problem given there were 1200 states of the three figures health. However, there are only 10 ways the thugs can have equal health, and for each number, there is a single point where the health of knight where the likely winner changes. So I could set up the state by changing the initial health of the thugs, and see when the results crossed 50% as I changed the health of the knight. Since it will be monotonic, it was fairly quick to work out. This final tweak squeezed about a tenth of a percent out.
    Which brings us to the conclusion: if you are freaking out over the math and madly protesting that context is far more likely to determine if you want to speed up the combat or flee – you are absolutely correct. Running a million trials and using the best and worst strategies for the knight (by both players) makes a different of about 1.2% And, remarkably, the optimum approach by both players is in between, 2 to 1. Which is not surprising at the thugs have two choices and the knight only one.
    Anyway here are the results – very close to a coin flip:

    Knight attacks strongest thug, strongest thug attacks knight.

    Knight wins:51.9% Draws:0.7% Thug wins:47.4%

    Knight attacks weakest thug, strongest thug attacks knight.

    Knight wins:52.3% Draws:0.7% Thug wins:47.0%

    One thug fights to death, then second.

    Knight wins:53.1% Draws:0.8% Thug wins:46.1%

    If you would like to play around and see what a templar or a barbarian can do, be my guest. In fact, if someone wants to repost this on Lead Adventure, please do (but attach my name!-). I haven’t got around to registering myself.

    import sys
    import random
    
    trials = 1000000
    crit_opt = True
    wound_opt = True
    random.seed(100)
    thug = {
          'fight' : 2,
          'weapon' : 0,
          'armor' : 10
          }
    thug2 = {
          'fight' : 2,
          'weapon' : 0,
          'armor' : 10
          }
    knight = {
          'fight' : 5,
          'weapon' : 0,
          'armor' : 13
          }
    thug_wins = 0
    knight_wins = 0
    
    def combat(Attacker,ADM,Target,TDM) :   
       Attacker_crit = False
       Target_crit = False
    
       if dead(Attacker) or dead(Target) : return
       die = random.randrange(1,21);
       if crit_opt :
          if die == 20 : Attacker_crit = True
       if wound_opt :
          if wounded(Attacker) : die -= 2
       Attacker_combat = die + Attacker['fight'] + ADM
    
       die = random.randrange(1,21);
       if crit_opt :
          if die == 20 : Target_crit = True
       if wound_opt :
          if wounded(Target) : die -= 2
       Target_combat = die + Target['fight'] + TDM
    
       if (Attacker_combat >= Target_combat and not Target_crit) or Attacker_crit :
          damage = Attacker_combat + Attacker['weapon'] - Target['armor']
          if Attacker_crit : damage *= 2
          if damage > 0 : Target['health'] -= damage
       if (Target_combat >= Attacker_combat and not Attacker_crit) or Target_crit:
          damage = Target_combat + Target['weapon'] - Attacker['armor']
          if Target_crit : damage *= 2
          if damage > 0 : Attacker['health'] -= damage
    
    favored = {
          1 : 4, 
          2 : 4,
          3 : 4,
          4 : 4,
          5 : 7,
          6 : 7,
          7 : 8,
          8 : 8,
          9 : 9,
          10:10
          }
    
    def thug_favored(T,K) :
       return K <= favored[T]
    
    def dead(figure) :
       return figure['health'] < 1
    
    def wounded(figure) :
       return figure['health'] <= 4
    
    for sample in range(trials) :
       thug['health'] = 10
       thug2['health'] = 10
       knight['health'] = 12
    
       while not (dead(thug) and dead(thug2)) and not dead(knight) :
    
          if thug['health'] > thug2['health'] :
             combat(thug,0 if dead(thug2) else 2, knight, 0)
             if thug['health'] < thug2['health'] :
                combat(thug2,0 if dead(thug) else 2, knight, 0)
          elif thug['health'] == thug2['health'] :
             if thug_favored(thug['health'],knight['health']) :
                combat(thug,0 if dead(thug2) else 2, knight, 0)
                if thug['health'] < thug2['health'] :
                   combat(thug2,0 if dead(thug) else 2, knight, 0)
          else :
             combat(thug2,0 if dead(thug) else 2, knight, 0)
             if thug['health'] > thug2['health'] :
                combat(thug,0 if dead(thug2) else 2, knight, 0)
    
          if thug['health'] < thug2['health'] or dead(thug2) :
             combat(thug,0 if dead(thug2) else 2, knight, 0)
          elif thug['health'] == thug2['health'] :
             if not thug_favored(thug['health'],knight['health']) :
                combat(thug,0 if dead(thug2) else 2, knight, 0)
          else :
             combat(thug2,0 if dead(thug) else 2, knight, 0)
    
       if not (dead(thug) and dead(thug2)) : thug_wins += 1
       if not dead(knight) : knight_wins += 1
    
    print('Knight wins:',round(knight_wins*100/trials,1),'% Draws:',round((trials-knight_wins-thug_wins)*100/trials,1),'% Thug wins:',round(thug_wins*100/trials,1),'%', sep='')
    
    

     

    #45660
    Jonathan Gingerich
    Participant

    And just a post script. The program is set up assuming the thugs attack the knight first, collectively. If the knight attacks a single thug first, the results are:

    Knight wins:56.1% Draws:0.8% Thug wins:43.2%

    So getting in the first blow is a lot more important than always attacking the weaker thug.

    #45673
    John D Salt
    Participant

    This is jolly interesting.

    I am still using Python 2, but it was a matter of minutes to re-jig the print statement to work on my machine. An unexpected difference is that, to work correctly under Python 2 with a plain print statement, the 100s used to convert to percentages in the print statement need to be floats (100.0s) to avoid truncation.

    You have a very different coding style from mine, but I think it is fairly clear what is going on. However, I do not have a copy of Frostgrave, and have never played it, so some of the procedures are a bit unclear.

    As I read the code, players take turns attacking. The winner is the one who gets the higher fighting score, this being the sum of 1d20, the figure’s fighting value, and a modifier. The winner inflicts damage on the opponent equal to the winner’s fightng score plus weapon rating, less the loser’s armour rating. In the event of a tie, both combatants suffer damage. Damage is deducted from the victom’s health rating. If the rule is in force, health ratings of 4 and below count as wounded, and incur a -2 penalty on the fighting score. If the rule is in force, critical hits on natural 20s inflict double damage, regardless of who got the higher fighting score. If an attack is supported by a friend it receives a die roll modifier of +2. As far as I can make out, this modifier is received even if the supporting figure makes its own attack; thus two figures against one can make two attacks, each with a +2 bonus.

    Iff the foregoing is correct, then I have understood the code; if not, then I haven’t. What is still not clear to me, though, is this business of how you calculate whether a thug is ‘favored’ or not. If I’m following correctly, the lookup table for ‘favoured’ status is used only to decide whether the second thug should fight in the case where the first thug has fought and the second thug has equal health. But where did the numbers in the lookup table come from? What, in words, is the definition of ‘favored’ status?

    If you can spare the time, I’d also like to know how larger multi-figure combats are adjudicated in Frostgrave. Can anyone hit anyone else in a melee, or are people limited to 2 against 1?

    All the best,

    John.

    #45674
    Jonathan Gingerich
    Participant

    First, oops, when I started looking at combat with Templar instead of a Knight, I realized I have confused the Knight’s “fighting” stat with the “move” stat. They should be a +4 rather than a +5. With the correction, the observations remain the same, but the correct stats are:

    Knight versus thug, basic rules
    Knight wins:83.5% Draws:0.8% Thug wins:15.7%

    Knight versus thug, optional rules
    Knight wins:78.1% Draws:1.0% Thug wins:20.9%

    Knight versus 2 thugs, optimal choices, Knight first strike
    Knight wins:49.1% Draws:0.8% Thug wins:50.1%

    Knight versus 2 thugs, optimal choices, Thugs first strike
    Knight wins:45.2% Draws:0.8% Thug wins:54.0%

    #45675
    Jonathan Gingerich
    Participant

    I guess I can count my coding style a success, as you got it in one, John!-)

    Do players take turns? – busted! it’s a fudge. Actually they roll a straight coin flip each turn to determine the first player. Each will have opportunities to activate every figure. I could randomize the order of play. But as the winner can “push back” i.e.disengage the opponent that would have to be considered. It would occasionally help the knight by temporarily disrupting “support”. (Unengaged figures can move and then fight, in case you are wonder why it’s not an issue when turns are always interleaved.)

    Combat occurs between figures in base to base contact. Any figure in contact with only one enemy can support attacks on that enemy. So with standard bases it would be possible to attack 1 figure with 6, for a +10 support bonus…

    The favors table was hand created. I realized that sometimes the best decision was not to attack. Since combat is symmetrical you are as likely to hurt or be hurt whether you or your enemy is attacking. The only way to influence this is to choose which thug to target or to attack with first. When their health is equal, there is no choice. So attacking in this situation is liable to give a choice to your opponent. But if both players avoid combat the program goes into an infinite loop:-) Sort of like soccer. Therefore I hand calculated who had the upper hand when the thugs were equal in health (by initializing their health and running the simulation) and found the crossover (50%) point given the knight’s health; And made them attack.

    (And the table needed to be recalculated when I realized the Knight’s “fighting” should be +4 not +5.)

    favored = {
    1 : 4,
    2 : 4,
    3 : 4,
    4 : 5,
    5 : 8,
    6 : 9,
    7 : 10,
    8 : 11,
    9 : 12,
    10: 12
    }

    #45685
    Mike
    Keymaster

    Such geekery.
    Excellent. I will have a look into this.

    #45687
    John D Salt
    Participant

    I can’t help wondering when you learnt to code, and if the people who taught you were functional programming enthusiasts!

    Certainly, the style is clear enough to quickly pick up what’s going on. A customary grumble by almost everyone is “needs more comments”, but in fact there is little value in comments that tell you what you can see from the code. It *is* possible to obfuscate Python, but you’d have to try harder than in a lot of other languages.

    Another great thing about Python is that you can be as oject-oriented as you want to be. If you don’t want to use classes, don’t bother. I always want to use classes, so my version of your program starts like this:

    from random import choice
    
    crit_opt = True
    wound_opt = True
    
    def roll1d20():
        ''' Roll one 20-sided die '''
        return choice([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20])
    
    class fig:
    
        def __init__(self, f=2, w=0, a=10, h=10):
            ''' Create figure with fight, weapon, armour, and health values given '''
            self.fight = f
            self.weapon = w
            self.armour = a
            self.health = h
    
        def dead(self):
            ''' Return True if figure dead '''
            return (self.health <= 0)
    
        def wounded(self):
            ''' Return True if figure wounded '''
            return (wound_opt == True and self.health <=4)
    
        def howhealthy(self):
            return self.health
    
        def combat(self, opponent, ownmod=0, enmod=0):
            ''' Fight one round between self and opponent, applying modifier '''
            if self.dead():
                print 'Dead men can\'t fight'
                return
            # end if
            ownroll = roll1d20()
            enroll = roll1d20()
            ownscore = ownroll + self.fight + ownmod
            enscore = enroll + opponent.fight + enmod
            if self.wounded():
                ownscore -= 2
            # end if
            if opponent.wounded():
                enscore -= 2
            # end if
            ownDamage = max(0, (enscore + opponent.weapon) - self.armour)
            enDamage = max(0, (ownscore + self.weapon) - opponent.armour)
            if enroll == 20 and crit_opt:
                self.health -= ownDamage * 2
                print 'Attacker Crit!', ownDamage * 2,
            else:
                if enscore >= ownscore:
                    self.health -= ownDamage
                    print 'Attacker  hit!', ownDamage,
                else:
                    print 'Attacker missed',
                # end if
            # end if
            if ownroll == 20 and crit_opt:
                opponent.health -= enDamage * 2
                print 'Defender Crit!', enDamage * 2
            else:
                if ownscore >= enscore:
                    opponent.health -= enDamage
                    print 'Defender hit!', enDamage
                else:
                    print 'Defender missed'
                # end if
            # end if
    

    There are a lot of print statements to allow me to trace what’s going on, and my own personal tic of showing block terminations with a comment wastes a few lines, but I think it does substantially the same thing as the relevant bits of your code, while being distinctly different in style. The advantage of wrapping things up in a class representing a figure is that, when crazed by excessive ingestion of Lapsang Souchong, I can visualise keeping collections of these by anonymous reference and fighting arbitrary forces of mixed figure types against each other in a Frostgrave big battle simulator.

    If I can prod a little deeper on the Frostgrave sequence of play — would I be right in thinking that it’s two actions per figure, players alternate activating single figures, figures that fight in their first action may not subsequently move? If not, what is it?

    A while ago I tried writing a small Python program to implement the gunnery procedures for a naval game a friend of mine designed years ago called “Surface Action” (you won’t have heard of it, and someone else has nicked the title since then). After two months and 4620 lines of Python, I found myself with an almost complete version of the game, using the Python turtles package to plot the ship’s moves on the tactical chart, and inclusding scenarios for a couple of actions involving the Goeben, the Battle of the River Plate, and the sinking of the Haguro. So, be warned — you migth end up a few months from now having implemented “Computer Frostgrave” (not that that would be a bad thing).

    What I found most interesting about going through the exercise was how the discipline of having to explain the wargame rules to a computer demanded real clarity on exactly what the rules said. Although “Surface Action” had been playtested fairly mercilessly by a bunch of people including some quite fierce rules lawyers over a period stretching back to 1978, I still found a few points where clarification was required for absolute certainty — and as I am still in touch with the author (a close friend and fellow wargamer since 1971) I hope they will help to improve the third edition, which may still see the light of day one of these years.

    Since Python is free and programming is a skill everyone should aspire to, I hope that in future wargame designers might acquire the habit of writing computer implementations of parts of their rules to check how the procedures work, to conduct statistical experiments (I seem to recall a set of medieval rules where peasants massacred knights every time at 2:1 odds, which seemed nonsense to me then as now) and to improve the clarity of the rules when written. This isn’t at all the same thing as writing a computer game; but it should help to make for better rules for manual games.

    All the best,

    John.

    #45696
    Jonathan Gingerich
    Participant

    John, forgive a geeky rant – I loathe  the # end if’s. The totally brilliant and original contribution of Python to language design is to force the indention to reflect the syntax. And you killed it, John, you killed it!  (Personally I prefer a one branch conditional on a single line if it fits – emphasizes the one branch.)

    Okay, I’m an old dog and learned my coding before OOP. I tend to write functionally then port it into a class. dead(thug) vs. thug.dead() just doesn’t do that much for me. (And really I should have wrote is_dead()) There are 15 figure types for adventures in Frostgrave, so, yeah, the next step would be a fig class initialized from a selection out of the table of literals.

    Frostgrave – think of a cross between a skirmish game and “rogue”. You paint up your band (10 or so figures) and gain treasure and experience in a specified manner. So it really needs league play for the constant competition to maintain a narrative. Thus I’ve not played yet, which explains my blind spots.

    Each turn is 4 phases: Wizard; Apprentice; Soldier; Creature. Each activated figure can take two actions, but not two non-move actions. Wizards and Apprentices can activate 3 nearby soldiers and all can more all first, then act with each – i.e. you could move everybody adjacent to an enemy then attack with each benefiting from the support bonuses. The Soldiers activate one at a time (but all of one player’s then the other’s), so the first soldier would have to attack unsupported in the example. The phases are interleaved, with the initiative player going first in each.

    Anyway, discussing this all made me realize I completely blew it by failing to account for disengagement. I’m really straining to see the optimal approach.  For example, if the knight plays second, and the thugs are equally healthy, is it better to attack, hoping to get a second attack next turn on an unsupported thug, or is better to pass so the thugs don’t get an opportunity to attack with a healthier thug?

    #45697
    John D Salt
    Participant

    John, forgive a geeky rant – I loathe the # end if’s. The totally brilliant and original contribution of Python to language design is to force the indention to reflect the syntax. And you killed it, John, you killed it! (Personally I prefer a one branch conditional on a single line if it fits – emphasizes the one branch.)

    I know it’s not Pythonic, and I *did* describe it as a personal tic. However, having had my first executable program punched (sure you’re an old dog?) in Algol-60, and having since then been accustomed to block-structured languages such as BCPL, C, Pascal, Simula, Modula-2, Ada, MODSIM II, Eiffel and Java, I get itchy palms if I don’t see something to close the block. You see, Python does not force the indentation to match the syntax, it forces the syntax to match the indentation. An innovation, certainly, but I remain to be convinced that doing away with free format is altogeter a good idea. I could get myself out of structure troubles in Pascal using a prettyprinter a good deal quicker than I can by tabbing and untabbing on those rare occasions when my Python indentation goes squirrelly. But, yeah, Guido’s style guidelines say my tic is a sheer waste of lines and toner.

    Okay, I’m an old dog and learned my coding before OOP. I tend to write functionally then port it into a class. dead(thug) vs. thug.dead() just doesn’t do that much for me

    “Before OOP” would be before 1967, strictly, it’s just that nobody heard of it for about 25 years because it was invented in Norway. Still, it seems to me that your coding style is pretty object-oriented — one of the things that makes it readily understandable — it’s just that you are using dictionaries instead of instances of classes. At the bottom level Python implements classes (as it implements so many things) using dictionaries, so to a great extent it’s six of one and half a dozen of the other. If you wanted to bind methods to a thing represented by a dictionary, you could do that too, as Python supports function variables (or thunks, as we old Algol hands would say). And if you can’t use prefixing classes for inheritance, you can use a prototype-based approach and add fields to suit in derived classes (“duck-punching”). The fig.combat() method I wrote is arguably a multi-method, and so you could argue need not belong to a specific class (though I’d say that was a better argument in a statically-typed language were pparameters to the procedure could be given type signatures).

    Thanks for the information on the Frostgrave sequence of play. It seems to be considerably more intricate than I was imagining, and might require a formal specification for me to understand properly. One of the things I discovered in the computerisation of “Surface Action” is that it’s hugely easier to write computer implemetations of the game procedures, and let players take the game decisions, than it is to attempt any sort of overall game-playing logic.

    Your mention of “Rogue” is a reminder that the cross-over between tabletop and computer games has a long and respectable history. I really do hope to see more along these lines in the future, while avoiding the trap of the “game assistance program”, a chimerical monster in my opinion.

    All the best,

    John.

    #46442
    Katie L
    Participant

    “Python supports function variables (or thunks, as we old Algol hands would say)”

    The modern term is that functions are first-class objects…

     

     

    In Python they really are just “any object which has a __call method”.

     

Viewing 10 posts - 1 through 10 (of 10 total)
  • You must be logged in to reply to this topic.