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.