Introducing the fumbbl_replays Python package for Blood Bowl

The fumbbl_replays package is a Python utility package for the board game Blood Bowl. It allows users to plot board positions, either from scratch, or from existing (FUMBBL) game logs. In addition, it has some functionality to analyze FUMBBL game logs.

On the FUMBBL website, a lot of high quality replay data is available as well as an API to conveniently fetch the data. In addition, the API provides up to date roster information. To do useful analyses (aka nufflytics) in Python with this data, we need a utility package / library. In R, a similar package exists to work with BB2 replays [https://github.com/nufflytics/nufflytics]. For BB3 replay files, work is ongoing to process them, with a proof-of-concept example C# Github repo available.

We also need a standard way to describe Blood Bowl games in a compact way, that is both human and machine readable. In chess, there is the Portable Game Notation (PGN). PGN has become the de facto standard of describing Chess games. For Blood Bowl, already in 2002 some work has been done towards this end. David Morgan-Mar developed a notation for the purpose of sharing Blood Bowl game logs over the internet. [https://www.dangermouse.net/games/bloodbowl/rules.html]

If we could converge on a standard Fantasy Football Game Notation (FFGN), it would serve many purposes, e.g.:

  • It would allow us to interchange data between software
  • it would help to train AI engines.

So the package is being developed with this end goal in mind. As Blood Bowl is a much complexer game than Chess, and I am an amateur programmer, we need some intermediate goals that bring us closer to the end goal. Thus, I started with plotting (new or extracted) board positions using a short hand notation for board pieces (players) and codifying player moves. While developing the package, I benefited greatly from discussions and feedback on the Bot bowl discord forum.

%pip install -e . --quiet

Plotting Blood Bowl board positions

If we want to describe (codify) a Blood Bowl board state, we need to describe the pieces (what type of player is it, what extra skills does it have), the location of the pieces as well as the “state” of the pieces. Player state in Blood Bowl can be either standing, prone, or stunned, and can be in various special states such as “Bone head”, “Rooted”, “Hypnotized” etc. (A full game state also contains additional information on rerolls, players on the bench etc. This is not yet implemented)

Let’s start with the location of the pieces. A grid reference system is needed. The game board of Blood Bowl has dimensions 15 x 26. It has cognitive benefit to use numbers for one dimension, and letters for the other dimensions. Fancy word: alphanumeric. Chess over the centuries has had various notations, and this notation is the one that became universally accepted. [https://en.wikipedia.org/wiki/Algebraic_notation_(chess)] The only choice left for us is then, which axis should have letters, and which axis should have the numbers.

A strong argument was made on the BotBowl discord that distance to the end zone is very important in BB. By using numbers for the long axis, we can easily deduce that a Gutter Runner at position c15 is in scoring position: It needs 11 movement to score a touchdown at c26. This notation is also used by Cow Daddy Gaming in his “What´s the play” puzzles.

I wrote a function show_boardpos() that displays the name of all the board positions.

import fumbbl_replays as fb

fb.show_boardpos(rotation = 'H')

Now we need a way to describe the playing pieces, and visualize them. In chess it is easy, there are only six different ones. In Blood Bowl, there are roughly 200 different playing pieces (30 teams, times 5 positionals, plus 50+ star players). Here the concept of a roster can help us out. I wrote a function fetch_roster() that fetches rosters from FUMBBL and displays the positions. It also contains links to icons that can represent the piece on the board.

Take for example the High Elf roster.

roster = fb.fetch_roster("High Elf")
roster
positionId positionName skillArray shorthand icon_path race
0 39330 Lineman [] L https://fumbbl.com/i/585638.png High Elf
1 39331 Thrower [Cloud Burster, Pass, Safe Pass] T https://fumbbl.com/i/436284.png High Elf
2 39332 Catcher [Catch] C https://fumbbl.com/i/585639.png High Elf
3 39333 Blitzer [Block] Z https://fumbbl.com/i/436286.png High Elf

It has four different pieces or “positionals”. It turns out that FUMBBL has already solved our problem of denoting them, introducing a shorthand text reference. So if we want to describe some action involving a High Elf Catcher, and there are four of them on the board, we could denote them by C1, C2, C3 and C4. This is compact, and has meaning within the context of the High Elf roster.

If we combine the descriptions of the pieces, and their location, we have enough to describe for example an initial setup formation before kick-off.

my_setup = ['setup', ['L1: g13', 'L2: h13', 'L3: i13', 'Z1: c11', 'Z2: m11', 'T1: h6', 'L4: e11', 
                      'L5: k11', 'C1: l10', 'C2: d10', 'L6: h11']]

I wrote a function create_position() that combines the roster and the setup annotation to create an object that contains all the information to make a nice plot of the board state. The function print_position() prints a nicely formatted summary of the position. As default, a position is created for the home team, denoted as “teamHome”.

positions = fb.create_position(roster, my_setup)
fb.print_position(positions)
home_away race short_name positionName boardpos PlayerState
5 teamHome High Elf T1 Thrower h6 Standing
9 teamHome High Elf C2 Catcher d10 Standing
8 teamHome High Elf C1 Catcher l10 Standing
3 teamHome High Elf Z1 Blitzer c11 Standing
6 teamHome High Elf L4 Lineman e11 Standing
10 teamHome High Elf L6 Lineman h11 Standing
7 teamHome High Elf L5 Lineman k11 Standing
4 teamHome High Elf Z2 Blitzer m11 Standing
0 teamHome High Elf L1 Lineman g13 Standing
1 teamHome High Elf L2 Lineman h13 Standing
2 teamHome High Elf L3 Lineman i13 Standing

Let’s suppose that the High Elf team is playing against a Gnome team. Let’s also fetch a Gnome roster and create a board position on the other half of the pitch. As we already have a home team, we refer to this team as “teamAway”.

roster = fb.fetch_roster("Gnome")
roster
positionId positionName skillArray shorthand icon_path race
0 57706 Altern Forest Treeman [Mighty Blow (+1), Stand Firm, Strong Arm, Thi… T https://fumbbl.com/i/733781.png Gnome
1 57707 Gnome Beastmaster [Jump Up, Wrestle, Guard, Stunty] B https://fumbbl.com/i/735006.png Gnome
2 57708 Gnome Illusionist [Jump Up, Wrestle, Stunty, Trickster] I https://fumbbl.com/i/735007.png Gnome
3 57709 Woodland Fox [Dodge, Side Step, Stunty, My Ball] F https://fumbbl.com/i/735008.png Gnome
4 57710 Gnome Lineman [Jump Up, Wrestle, Right Stuff, Stunty] L https://fumbbl.com/i/735009.png Gnome
my_setup = ['setup', ['T2: j14', 'T1: f14', 'F1: h20', 'I1: b14', 'I2: n14', 'L3: e14', 'L6: k14', 
                      'B2: m15', 'B1: c15', 'L4: g15', 'F2: i16']]

positions2 = fb.create_position(roster, my_setup, 'teamAway')

fb.print_position(positions2)
home_away race short_name positionName boardpos PlayerState
3 teamAway Gnome I1 Gnome Illusionist b14 Standing
5 teamAway Gnome L3 Gnome Lineman e14 Standing
1 teamAway Gnome T1 Altern Forest Treeman f14 Standing
0 teamAway Gnome T2 Altern Forest Treeman j14 Standing
6 teamAway Gnome L6 Gnome Lineman k14 Standing
4 teamAway Gnome I2 Gnome Illusionist n14 Standing
8 teamAway Gnome B1 Gnome Beastmaster c15 Standing
9 teamAway Gnome L4 Gnome Lineman g15 Standing
7 teamAway Gnome B2 Gnome Beastmaster m15 Standing
10 teamAway Gnome F2 Woodland Fox i16 Standing
2 teamAway Gnome F1 Woodland Fox h20 Standing

As a final step before plotting, we add both positions together. As both are pandas DataFrames, we use the concat() function from pandas to combine (“concatenate”) them.

import pandas as pd

positions = pd.concat([positions, positions2])

The function create_plot() plots the board position. By default, it plots a horizontal pitch, with the team denoted as “teamHome” in red, and the other team in blue.

fb.create_plot(positions)

The create_plot() function allows us the swap the color of the teams, to change the pitch orientation to vertical, and to add a layer of semi-transparant tacklezones.

fb.create_plot(positions, red_team = "teamAway", orientation = 'V', tackle_zones = True)

The library also support moving single pieces (players). It currently only works for pieces that already exist in a board position. In the plot above, suppose we want to move the Woodland Fox F1 to board position o26:

positions = fb.move_piece(positions, "teamAway", "F1", "o26")

fb.create_plot(positions, red_team = "teamAway")

Each player also has an associated PlayerState. This can either be Standing (the default), HasBall, Prone or Stunned. The function set_piece_state() allows to set this for individual players:

positions = fb.set_piece_state(positions, "teamAway", "F1", "HasBall")
positions = fb.set_piece_state(positions, "teamHome", "T1", "Prone")
positions = fb.set_piece_state(positions, "teamAway", "B1", "Stunned")

fb.create_plot(positions, red_team = "teamAway")

The compact setup description can also describe player states other than standing. / is Prone, X denotes stunned. o denotes a player that has the ball.

The compact description for the Gnome position plotted above can be obtained using get_position():

fb.get_position(positions, home_away = 'teamAway')
['setup', ['T2: j14', 'T1: f14', 'F1: o26o', 'I1: b14', 'I2: n14', 'L3: e14', 'L6: k14', 'B2: m15', 'B1: c15X', 'L4: g15', 'F2: i16']]

Note the notation for Fox F1 with the ball (F1: o26o) and the notation for the stunned Beastmaster B1 (B1: c15X).

As the positions object is a table of players, I added an extra argument to create_plot() to plot a free ball.

fb.create_plot(positions, red_team = "teamAway", ballpos = 'e5')

Star players can also be plotted. For this we need to fetch a separate star player “roster” and add it to the team roster. Lets add Rowana Forestfoot (“RF”) and Rodney Roachbait (“RR”) to the Gnome setup, replacing Gnome linemen L3 and L6.

positions = positions.query('home_away == "teamHome"')

roster = fb.fetch_roster("Gnome")
stars = fb.fetch_stars()
roster = pd.concat([roster, stars])

my_setup = ['setup', ['T2: j14', 'T1: f14', 'F1: o26o', \
                    'I1: b14', 'I2: n14', \
                    'RF1: e14', 'RR1: k14', \
                    'B2: m15', 'B1: c15X', 'L4: g15', 'F2: i16']]

positions2 = fb.create_position(roster, my_setup, 'teamAway')


positions = pd.concat([positions, positions2])

fb.create_plot(positions, red_team = "teamAway", ballpos = 'e5')

Plotting player skills aka “digital loom color bands”

Knowing what extra skills players have is often important in analyzing a given board position (what blocks are possible, can we use the Dodge skill or does the opponent has Tackle etc). In the FUMBBL client there is a function that allows automatic skill marking using text. When playing on tabletop there are various ways to mark / denote what extra skills players have. One popular way is to use colored elastic (“loom”) band. The most common skills have semi-standardized colors associated with them:

  • Guard is green
  • Block is blue
  • Wrestle is white
  • Dodge is yellow
  • Leader is purple
  • Mighty Blow is red
  • Tackle is orange

At a typical tournament (I checked this for Thrudball 2024) 80% of skills chosen are one of these seven skills. Here I decided to make a digital version of the colored elastic band, plotted below the player icon. If a player has more than one extra skill (“Skill stacking”), the colored bands are stacked on top of each other.

Here I will demonstrate by setting up the board for a Shambling Undead team with a set of 6 skills often chosen at tournaments.

import fumbbl_replays as fb
roster = fb.fetch_roster("Shambling Undead")

my_setup = ['setup', ['Z1: g14', 'Z2: h14', 'Z3: i14', 
                      'W1: e16', 'W2: k16', 'G1: h16', 'G2: h17', 
                      'M1: c16', 'M2: m16', 'Z4: b17', 'Z5: n17']]

positions = fb.create_position(roster, my_setup)
fb.add_skill_to_player(positions, "M1", "Guard")
fb.add_skill_to_player(positions, "M2", "Guard")
fb.add_skill_to_player(positions, "G1", "Block")
fb.add_skill_to_player(positions, "G2", "Block")
fb.add_skill_to_player(positions, "W1", "Tackle")
fb.add_skill_to_player(positions, "W1", "Mighty Blow")

fb.create_plot(positions, red_team = "teamAway", orientation = 'H', skill_bands = True)

It is also possible to remove a (Gained) skill from a player.

fb.remove_skill_from_player(positions, "W1", "Tackle")
(positions
 .filter(['short_name', 'positionName', 'skillArrayRoster', 'learned_skills', 'skill_colors', 'boardpos'])
)
short_name positionName skillArrayRoster learned_skills skill_colors boardpos
0 Z1 Zombie Lineman [Regeneration] [] [] g14
1 Z2 Zombie Lineman [Regeneration] [] [] h14
2 Z3 Zombie Lineman [Regeneration] [] [] i14
3 W1 Wight Blitzer [Block, Regeneration] [Mighty Blow] [red] e16
4 W2 Wight Blitzer [Block, Regeneration] [] [] k16
5 G1 Ghoul Runner [Dodge] [Block] [blue] h16
6 G2 Ghoul Runner [Dodge] [Block] [blue] h17
7 M1 Mummy [Mighty Blow (+1), Regeneration] [Guard] [lime] c16
8 M2 Mummy [Mighty Blow (+1), Regeneration] [Guard] [lime] m16
9 Z4 Zombie Lineman [Regeneration] [] [] b17
10 Z5 Zombie Lineman [Regeneration] [] [] n17

Plotting board positions from FUMBBL replays

Up until now, we created board positions from scratch, using rosters from FUMBBL and a simple way to describe a board position.

The package also allows us to plot board positions extracted from FUMBBL replay files. At this moment, only the board position right before kick-off can be plotted. Suppose we want to plot this position for match 4550284.

We first need to fetch the replay data. The fetch_data() function takes the match_id as argument and returns five objects: the match_id, replay_id, a positions object containing the board state right before first kick-off, which team is the receiving_team (i.e. playing offense), and a metadata list (coach names, race names, and match touchdown result).

match_id, replay_id, positions, receiving_team, metadata = fb.fetch_data(match_id = 4550284)

To plot the board state right before kick-off, we can use the create_plot() function in the same way as above. We plot the receiving team in red so we can see which team is playing offense and which team is playing defense.

fb.create_plot(positions, red_team = receiving_team)

(positions.filter(['race', 'home_away', 'short_name', 'positionName', 'playerName',  'skillArrayRoster', 'learned_skills', 'cost', 'recoveringInjury'])
                    )
race home_away short_name positionName playerName skillArrayRoster learned_skills cost recoveringInjury
0 Gnome teamHome T2 Altern Forest Treeman Caroline Rigol [Mighty Blow, Stand Firm, Strong Arm, Take Roo… [+MA] 120000 None
1 Gnome teamHome T1 Altern Forest Treeman Matthew Ir [Mighty Blow, Stand Firm, Strong Arm, Take Roo… [Pro] 120000 None
2 Gnome teamHome L2 Gnome Lineman Jaiden Netzigon [Jump Up, Right Stuff, Stunty, Wrestle] [] 40000 None
5 Gnome teamHome B1 Gnome Beastmaster Eloise Celorn [Guard, Jump Up, Stunty, Wrestle] [] 55000 None
6 Gnome teamHome L1 Gnome Lineman Andre Drumma [Jump Up, Right Stuff, Stunty, Wrestle] [] 40000 None
7 Gnome teamHome B2 Gnome Beastmaster Sienna Rime [Guard, Jump Up, Stunty, Wrestle] [] 55000 None
8 Gnome teamHome L4 Gnome Lineman Melanie Kayce [Jump Up, Right Stuff, Stunty, Wrestle] [] 40000 None
9 Gnome teamHome L5 Gnome Lineman Eliza Elora [Jump Up, Right Stuff, Stunty, Wrestle] [] 40000 None
10 Gnome teamHome L7 Gnome Lineman Gabriela Lacspor [Jump Up, Right Stuff, Stunty, Wrestle] [] 40000 None
11 Gnome teamHome I1 Gnome Illusionist Brynlee Tror [Jump Up, Stunty, Trickster, Wrestle] [] 50000 None
12 Gnome teamHome L3 Gnome Lineman Chase Kavelin [Jump Up, Right Stuff, Stunty, Wrestle] [Sneaky Git] 40000 None
14 Shambling Undead teamAway Z3 Zombie Lineman Rylee Dager [Regeneration] [] 40000 None
15 Shambling Undead teamAway Z2 Zombie Lineman Molly Harven [Regeneration] [] 40000 None
16 Shambling Undead teamAway Z1 Zombie Lineman Asher Meridan [Regeneration] [] 40000 None
17 Shambling Undead teamAway M1 Mummy Brooklyn Biel [Mighty Blow, Regeneration] [] 125000 None
18 Shambling Undead teamAway G1 Ghoul Runner Noelle Vythethi [Dodge] [] 75000 None
19 Shambling Undead teamAway Z5 Zombie Lineman Asher Meegosh [Regeneration] [] 40000 None
20 Shambling Undead teamAway M2 Mummy Bailey Fer [Mighty Blow, Regeneration] [] 125000 None
21 Shambling Undead teamAway Z4 Zombie Lineman Haley Guilomar [Regeneration] [] 40000 None
22 Shambling Undead teamAway W1 Wight Blitzer Corbin Aleemy [Block, Regeneration] [] 90000 None
23 Shambling Undead teamAway W2 Wight Blitzer Lucas Mickal [Block, Regeneration] [] 90000 None
24 Shambling Undead teamAway G2 Ghoul Runner Ivy Farate [Dodge] [] 75000 None

Adjusting this board position by moving players one-by-one works also in the same way as above.

positions = fb.move_piece(positions, "teamAway", "Z1", "b26")
positions = fb.move_piece(positions, "teamAway", "Z2", "o26")

fb.create_plot(positions, red_team = receiving_team)

Suppose we think that this Gnome defensive setup is awesome, and we wish to share this setup with other coaches. Here the compact way to describe a setup using player abbreviations and the alphanumeric grid system comes in handy:

fb.get_position(positions, home_away = 'teamHome')
['setup', ['T2: g13', 'T1: i13', 'L2: h13', 'B1: f10', 'L1: e11', 'B2: j10', 'L4: k11', 'L5: h11', 'L7: i10', 'I1: g10', 'L3: h10']]

However, suppose we think this setup is nice, but it would be even better if the illusionist in row g would instead be a Woodland Fox. We can take the setup (copy-paste), change the setup slightly, and create a new position, with F1: g10. As we now only have a single team, we can rotate the pitch and crop to show only the upper part of it.

roster = fb.fetch_roster("Gnome")

my_setup = ['setup', ['T2: g13', 'T1: i13', 'L2: h13', 'B1: f10', \
                      'L1: e11', 'B2: j10', 'L4: k11', 'L5: h11', \
                        'L7: i10', 'F1: g10', 'L3: h10']]

positions = fb.create_position(roster, my_setup)

fb.create_plot(positions, orientation= "V", crop = "upper")

Working with raw replays directly

It is also possible to work with the raw FUMBBL replay files directly. I made a start with describing the replay file format in doc/fumbbl_replay_file_format.md. We can use fetch_replay() to retrieve a replay in JSON format.

import fumbbl_replays as fb

my_replay = fb.fetch_replay(match_id = 4447439)

JSON consists of key-value pairs. We can for example query the value of the key gameStatus:

my_replay['gameStatus']
'uploaded'

Or query the rosterName:

my_replay['game']['teamHome']['roster']['rosterName']
'Necromantic Horror'

The replay contains both a game log, as well as full roster information on both teams. We can extract the roster information from the replay using the function extract_rosters_from_replay().

import pandas as pd
pd.set_option('display.max_colwidth', None)

df_positions = fb.extract_rosters_from_replay(my_replay)
(df_positions
 .query("home_away == 'teamAway'")
 .filter(['short_name', 'positionName', 'skillArrayRoster', 'learned_skills', 'skill_colors'])
)
short_name positionName skillArrayRoster learned_skills skill_colors
14 Tr1 Loren Forest Treeman [Loner, Mighty Blow, Stand Firm, Strong Arm, Take Root, Thick Skull, Throw Team-Mate] [] []
15 W1 Wardancer [Block, Dodge, Leap] [] []
16 W2 Wardancer [Block, Dodge, Leap] [Strip Ball] [deeppink]
17 T1 Thrower [Pass] [Leader] [purple]
18 C1 Catcher [Catch, Dodge] [] []
19 C2 Catcher [Catch, Dodge] [] []
20 L1 Wood Elf Lineman [] [Dodge] [yellow]
21 L2 Wood Elf Lineman [] [Dodge] [yellow]
22 L3 Wood Elf Lineman [] [] []
23 L4 Wood Elf Lineman [] [Wrestle] [floralwhite]
24 L5 Wood Elf Lineman [] [Wrestle] [floralwhite]

I wrote a replay parser that parses the gameLog section of a replay and transforms this into a pandas DataFrame object, i.e. a flat 2D table with rows and columns.

df = fb.parse_replay(my_replay)
(df[0:4]
 .filter(['commandNr', 'turnNr', 'turnMode', 'Half', 'modelChangeId', 'modelChangeValue'])
)
commandNr turnNr turnMode Half modelChangeId modelChangeValue
0 1 0 startGame 0 fieldModelAddPlayerMarker {‘playerId’: ‘15440786’, ‘homeText’: ‘B’, ‘awayText’: ‘B’}
1 1 0 startGame 0 fieldModelAddPlayerMarker {‘playerId’: ‘15440787’, ‘homeText’: ‘G’, ‘awayText’: ‘G’}
2 1 0 startGame 0 fieldModelAddPlayerMarker {‘playerId’: ‘15440788’, ‘homeText’: ‘G’, ‘awayText’: ‘G’}
3 1 0 startGame 0 fieldModelAddPlayerMarker {‘playerId’: ‘15440790’, ‘homeText’: ‘G’, ‘awayText’: ‘G’}

We can use the pandas query() function to select rows based on conditions. This query selects all “fieldModelSetPlayerCoordinate” commands during setup before turn 1.

positions = (df.query('turnNr == 0 & turnMode == "setup" & Half == 1 & \
                     modelChangeId == "fieldModelSetPlayerCoordinate"')
                     .groupby('modelChangeKey')
                     .tail(1))

(positions[0:4]
 .filter(['commandNr', 'turnNr', 'turnMode', 'Half', 'modelChangeId', 'modelChangeValue'])
)
commandNr turnNr turnMode Half modelChangeId modelChangeValue
77 20 0 setup 1 fieldModelSetPlayerCoordinate [12, 6]
79 21 0 setup 1 fieldModelSetPlayerCoordinate [12, 7]
81 22 0 setup 1 fieldModelSetPlayerCoordinate [12, 8]
84 24 0 setup 1 fieldModelSetPlayerCoordinate [10, 4]

As I was interested in defensive setup formations, I wrote a function determine_receiving_team_at_start() that does exactly what you’d expect given its name :)

fb.determine_receiving_team_at_start(df)
'teamAway'

Towards FFGN

Finally, there is a function fumbbl2ffgn() that is very much a work in progress. The idea is to take a FUMBBL game log, and systematically strip away all information that is redundant regarding the actual logging of what happened during the game. A minimal game description would consist of all actions taken, all decisions that were made (i.e. to use the dodge skill) and all dice results. After we have such a description, we can transform it to a compact annotation that is readable both by humans and machines, and is still a complete description of the game, in that the full game can be reproduced. The compact annotation would then be candidate to become the official “Fantasy Football Game Notation”, or FFGN for short.

my_game_log = fb.fumbbl2ffgn(match_id = 4447439)
len(my_game_log)
866

This is where it currently stands. A single gamelog is now roughly 1000 lines of text. The table below describes the first turn of a Wood Elf team against Necromantic.

pd.set_option('display.max_colwidth', None)

# Turn 1 for the offensive
(my_game_log
 .query("Half == 1 & turnNr == 1 & commandNr > 88 & commandNr < 211")
 .filter(['modelChangeKey', 'modelChangeValue'])
)
modelChangeKey modelChangeValue
29 [‘T1’] [j17, i17, h17, g17, f17, e17]
30 [‘C1’] [g17, f17, e18, d17, c17, b17]
31 [‘L1’] [d14, e13, f13]
32 [‘L4’] Block roll:[‘!’, ‘!’] | block result: ! (POW/PUSH)
33 [‘L5’] [g12]
34 [‘L4’] [g13]
35 0 Armour roll: [3, 5] | Armour of [‘L5’] is not broken
36 [‘Tr1’] Confusion roll: 2 | [‘Tr1’] acts normally
37 [‘Tr1’] Block roll:[‘>’, ‘%’, ’*’] | block result: * (POW)
38 [‘L2’] [i12]
39 [‘Tr1’] [h13]
40 0 Armour roll: [4, 2] | Armour of [‘L2’] is not broken
41 [‘L5’] Block roll:[‘>’, ‘!’] | block result: ! (POW/PUSH)
42 [‘L4’] [h12]
43 0 Armour roll: [2, 5] | Armour of [‘L4’] is not broken
44 [‘L3’] [m16, l16, k16, j16, i16, h16]
45 [‘L2’] [l15, k15, j15, i15, h15, g15, f15]
46 [‘C2’] [c15]
47 [‘W1’] [d16, c16, b16]
48 [‘W2’] [g18, f18, e18, d18, c18, b18]
49 [‘W2’] {‘reportId’: ‘pickUpRoll’, ‘playerId’: ‘[’W2’]’, ‘successful’: True, ‘roll’: 4, ‘minimumRoll’: 2, ‘reRolled’: False}
50 [‘W2’] [c18, d18]
51 ___ End of Turn

Thrower 1 moves. Catcher 1 moves. Lineman 1 moves. Lineman 4 blocks, chooses pow/push, pows Zombie lineman L5 into square g12, follows up, does not break armor. Treeman does not take root, does a 3D block on Zombie lineman 2, chooses pow into square i12, follows up to square h13, does not break armor. Lineman 5 blocks zombie lineman L4, pows into h12, does not follow up, does not break armor. Then linemen L3, L2, catcher C2 and wardancer W1 all do a move action. Finally Wardancer W2 moves, picks up the ball and moves a bit more. End turn.

This blog post describes the basic functionality of the fumbbl_replays Python package. I have a second blog post coming up with three applications that use the fumbbl_replays package to accomplish some Nufflytics goal.

Avatar
Gertjan Verhoeven
Data Scientist / Policy Advisor

Gertjan Verhoeven is a research scientist currently at the Dutch Healthcare Authority, working on health policy and statistical methods. Follow me on Twitter or Mastodon to receive updates on new blog posts. Statistics posts using R are featured on R-Bloggers.

Related