Mancala: Multiway Analysis and Building a Game GUI in Wolfram Language

Robert B. Nachbar, Wolfram Solutions

(RowDefault[, (Spacer1), , (Spacer1), ])  #WolframTechConf

ABSTRACT

We studied the game Mancala using game graphs and multiway analysis to examine how game complexity and fairness change with different board set-ups and rule sets, as well as how early a win can be forced. We also developed an interactive dynamic interface for two players to play the game.

Outline

Introduction

A little background

A little history

What if we could

What if we could also

Mancala The Game

Setup

Objective

Plays or moves

Overview

Capture

Extra-turn

Game over

Computable Mancala

Game board

Implementation of game play

The Rules

Regular play rules

Simpler play rules

Random Games

All Possible Games

Game state

State space traversal

Game graph anatomy

Enumerating the games

Results

Game graphs

Game graph size

Number of games and number of plays

Games

Number of plays

Scores and wins

Fairness

Early win decidability

Random play

Perfect play

The curious case of regular play with 2 pits and 5 seeds

Multiway Analysis

Branchial graphs

SowOnlyPlay, 2 pits, 1 to 6 seeds

RegularPlay, 2 pits, 1 to 6 seeds

All plays, 3 pits, 2 seeds

All plays, 3, 4, 5, and 6 pits, 1 seed

Observations

Causal invariance

Summary

Complexity

Fairness

Early win

Interactive GUI

Construction

Game board display

Dynamic game board display

Let’s play!

Back Matter

Acknowledgements

References

Initialization

Introduction

A little background

Chemical reaction networks (CRNs) are multiway systems, and were the topic of my 2022 WTC presentation []

One of my 2021 WSS students worked on the multiway analysis of the peg solitaire puzzle []

There is the Game Blog [], which went much further

And, a 2022 WSRP project looked at the Mancala problem []

This was the tipping point that got me to examine the Mancala game from a computational perspective

A little history

This Kalah board was given to me when I was about 12

These are the instructions on the back of the board

I have no idea what happened to the booklet in the intervening years

Mancala is a two-player board game known from antiquity and has many variations of play worldwide. We studied the most common North American version known as Kalah.

The traditional board consists of six pits, each containing four seeds, and an empty store for each player, and the objective is to capture the most number of seeds (one’s own and the opponent’s) in one’s store by moving them from one of the pits and sowing them in succeeding pits and the player’s store.

The game involves both counting and strategy, as the rules allow extra turns and capturing the opponents seeds.

What if we could

the use the tools of multicomputation and learn something new about the game?

study the consequences of different numbers of pits and/or seeds?

determine if it’s possible to force a win for one player or the other?

What if we could also

play it interactively, especially long distance via the Wolfram Cloud?

change the rules?

Mancala The Game

Setup

The game board consists of a row of pits and a store (or Kalah) on the right for each player

\[KernelIcon];
closeCellGroup[]

In each pit are placed, usually, 4 seeds; the stores are initially empty

We’ll use numbers to represent the seeds

\[KernelIcon];
closeCellGroup[]

Objective

The objective is to capture the most number of seeds (one’s own and the opponent’s)

Plays or moves

Overview

A play consists of selecting a pit and sowing the seeds one at a time into the succeeding player’s pits to the right, the player’s store, and the opponent’s pits

the opponent’s store is always skipped

if there are enough seeds, the sowing continues into the player’s pits

For example, Blue chooses pit 3, takes the two seeds and sows them into pits 4 and 5, and then the board is turned for Yellow to play:

\[KernelIcon];board={{{6,0,2,2,1,0},20},{{1,0,2,1,3,1},9}};
animatePlay[board,1,3,{"Blue","Yellow"}]
closeCellGroup[]

Next, Yellow chooses pit 5, takes the three seeds and sows them into pit 6, Yellow’s store, and Blue’s pit 1, and then the board is turned for Blue to play:

\[KernelIcon];board=playRegular[{board,1}, 3]//First;
animatePlay[board,2,5,{"Blue","Yellow"}]
closeCellGroup[]

Capture

When a player’s last seed is sown in one of his/hers pits that is empty, that seed and any seeds in the opponent’s pit immediately across the board are captured and moved to the player’s store

For example, Blue chooses pit 4:

\[KernelIcon];board={{{6,0,2,2,1,0},20},{{1,0,2,1,3,1},9}};
animatePlay[board,1,4,{"Blue","Yellow"}]
closeCellGroup[]

Extra-turn

When a player’s last seed is sown into his/her own store, he/she gets another turn; there is no limit to the number of successive extra-turns

For example, Yellow chooses pit 1:

\[KernelIcon];board={{{1,0,2,1,3,1},9},{{6,0,2,2,1,0},20}};
animatePlay[board,2,1,{"Blue","Yellow"}]
closeCellGroup[]

In fact, with careful choices Yellow can have quite a streak:

\[KernelIcon];

The last play is a capture, which ends Yellow’s streak

Game over

The game is over when all the pits on one side are empty, at which point all the seeds in the pits on the opposite side are moved to that player’s store, and the player with the most seeds wins

For example, yellow chooses pit 2

\[KernelIcon];animatePlay[{{{0,0,0,2,0,0},14},{{0,1,0,1,1,0},29}},2,2,{"Blue","Yellow"}]
closeCellGroup[]

Computable Mancala

Game board

All the code is in the Mancala.wl package (available in the Wolfram Cloud here)

The code for presentation features is in the Initialization section at the end

First, load the package

\[KernelIcon];Needs["Mancala`"]

Here are the functions in the package:

\[KernelIcon];Names["Mancala`*"]//

The function initializeBoard returns the game board expression

\[KernelIcon];(board=initializeBoard[])//

\[KernelIcon];?initializeBoard

And that can be displayed pictorially with displayBoard

The board is displayed with the next player’s pits at the bottom and his/her store on the right

\[KernelIcon];displayBoard@board

\[KernelIcon];?displayBoard

The game board is configurable

\[KernelIcon];Table[Rasterize[displayBoard[initializeBoard[p,s]],ImageSize->(p+2) 30],{p,6},{s,4}]//TableForm
closeCellGroup[]

Finally, like most Wolfram Language object domains, we have randomBoard

\[KernelIcon];displayBoard@randomBoard[]

\[KernelIcon];?randomBoard

Implementation of game play

Computationally, this action is accomplished by using a game state and the play function

A game state is just a list of a board expression and the index of the next player (1 or 2)

\[KernelIcon];(state={{{{6,0,2,2,1,0},20},{{1,0,2,1,3,1},9}},2})//

The play functions are: playRegular, playCapture, playExtraTurn, and playSowOnly; each takes a game state and a number for the pit to sow and returns a new game state. For example:

\[KernelIcon];playRegular[state,5]

\[KernelIcon];?playRegular

The Rules

Regular play rules

Regular play employs all the plays:

Sowing

Extra-turn

Capture

End-of-game

Simpler play rules

Simpler play rules are derived from the regular play rules by omitting the extra-turn rule, the capture rule, or both

For example, compare:

\[KernelIcon];With[{playList={"RegularPlay","CapturePlay","ExtraTurnPlay","SowOnlyPlay"}},
Table[SeedRandom[(*5102468*)477487];
randomGame[3,1,Method->play]//,{play,playList}]//
]
closeCellGroup[]

Random Games

Random game play can be useful for studying systems too large for full game graph generation

\[KernelIcon];?randomGame

For example:

\[KernelIcon];SeedRandom[477487];
boardList=randomGame[6,4,Method->"RegularPlay"]//Echo[#,"",Length]&;

\[KernelIcon];boardList////gameGraph[#,]&//
closeCellGroup[]

All Possible Games

Game state

The game state consists of the pair of the board configuration and the identity of the next player

The number of game states is given by

2 (Binomial[2 (m+1)+2 m n-1, 2 m n])

where m is the number of pits and n is the number of seeds in each pit []

(Labeled[({{"", 1, 2, 3, 4, 5, 6}, {1, "20", "70", "168", "330", "572", "910"}, {2, "252", "2,574", "12,376", "40,698", "106,260", "237,510"}, {3, "3,432", "100,776", "961,400", "5,259,150", "20,590,944", "64,448,228"}, {4, "48,620", "4,085,950", "77,134,200", "700,687,130", "4,108,911,268", "17,992,924,950"}, {5, "705,432", "169,344,630", "6,318,923,936", "95,252,033,940", "836,188,305,732", "5,121,094,767,152"}, {6, "10,400,600", "7,124,934,600", "525,193,567,528", "13,132,444,545,150", "172,541,500,616,352", "1,477,599,366,921,300"}}), "# Seeds", "# Pits"])

The nature of the game play does not allow all game states to be reached

State space traversal

We used a recursive, depth-first search strategy

A DataStructure[] was used to accumulate visited game states

Each play was saved as a tagged DirectedEdge

The before and after game states are the vertices, the selected pit is the edge tag

In our analysis, a “play” is the single act of selecting a pit, sowing the seed, and possibly making a capture; successive extra turns by the same player are considered separate plays

A player’s “turn” may comprise several plays

A Graph was constructed from the list of edges returned by the search function

\[KernelIcon];?multiway

For example:

\[KernelIcon];state={initializeBoard[2,2],1}

\[KernelIcon];edges=multiway[state,"labelEdgeWithMove"->True]//Echo[#,"|edges|",Length]&;

The tag for each edge is the number of the pit that was sown

\[KernelIcon];First@edges

The edge list returned by the multiway function is converted to a Graph with the the gameGraph function

\[KernelIcon];?gameGraph

\[KernelIcon];graph=gameGraph[edges,]

We can also display the game boards on the game graph with the VertexShapeFunction option:

\[KernelIcon];Graph[graph,VertexShapeFunction->(Inset[Rasterize[displayBoard[Sequence@@#2,{"Blue","Yellow"},"FontSize"->18],ImageSize->2 72],#1]&),]

Game graph anatomy

Each path from the root vertex (in-degree = 0) to each “leaf” vertex (out-degree = 0) represents a game

\[KernelIcon];root=SelectFirst[VertexList[graph],VertexInDegree[graph,#]==0&]

\[KernelIcon];leaves=Select[VertexList[graph],VertexOutDegree[graph,#]==0&];
Length@%

There are four distinct final game states

\[KernelIcon];paths=FindPath[graph,root,#,∞,∞]&/@leaves;
Length/@%
Total@%

There are seven distinct games

\[KernelIcon];Catenate[paths]//Map[Length]//MinMax

The shortest game has 2 moves and the longest 7 (a path is a list of vertices, not edges)

Here are the game paths laid out on the game graph:

\[KernelIcon];HighlightGraph[graph,#,ImageSize->3.75 72]&/@Map[Style[pathGraph[graph,#],Red,AbsoluteThickness[3],Arrowheads[0.05]]&,paths,{2}]//Row[#,Spacer[24]]&
closeCellGroup[]

Here are the final scores for each game {blueScore, yellowScore}:

\[KernelIcon];scores=Catenate[paths]//Map[Map[Last]@*First@*Last]

And we can compute wins and ties:

\[KernelIcon];Subtract@@@scores//Map[Sign]//Counts//KeyMap[Replace[{_?Negative->"Yellow wins",_?Positive->"Blue wins",_->"Tie"}]]//ReverseSort

Yellow wins 3 games, Blue wins 2 games, and there are 2 ties

Enumerating the games

We examined all board set-ups for 1 to 6 pits and 1 to 6 seeds per pit

Not all set-ups were attainable with the computing resources available

We also examined four sets of rules:

RegularPlay: sow, capture, and extra-turn

CapturePlay: sow and capture

ExtraTurnPlay: sow and extra-turn

SowOnlyPlay: sow

We did not employ any strategies to winnow the possibilities to only advantageous plays

For each game graph the following properties were computed:

number of vertices, number of edges, number of leaves

number of paths (games), path length distribution (plays per game distribution), shortest path length, longest path length

score distribution, win-tie-loss distribution

Results

Game graphs

Let’s take a look at the game graphs for the four rule sets with the 2-pits 2-seeds set-up

({{(Labeled[, "SowOnlyPlay"]), (Labeled[, "ExtraTurnPlay"]), (Labeled[, "CapturePlay"]), (Labeled[, "RegularPlay"])}})

Here is what happens when another seed is added:

({{(Labeled[, "SowOnlyPlay"]), (Labeled[, "ExtraTurnPlay"]), (Labeled[, "CapturePlay"]), (Labeled[, "RegularPlay"])}})

More seeds means longer games

And here is what happens when, alternatively, another pit is added:

({{(Labeled[, "SowOnlyPlay"]), (Labeled[, "ExtraTurnPlay"]), (Labeled[, "CapturePlay"]), (Labeled[, "RegularPlay"])}})

More pits means a dramatically greater variety of game play

We can already get some intuition about the rules

The capture play leads to shorter games, which makes sense because it removes seeds from play faster

The extra-turn play has an unclear subtle effect

Game graph size

The table below presents some statistics on the generated game graphs

Notice that only a small portion of the state space is actually visited

This is largely due to the method of sowing all the seeds from a given pit

The capture rule further reduces the coverage

Plotting these data by row or column gives a better feel for how quickly the number of game states grows

The growth is polynomial in the number of seeds and exponential in the number of pits

Here is another view of the same data

({{(Labeled[, "SowOnlyPlay"]), (Labeled[, "ExtraTurnPlay"]), (Labeled[, "CapturePlay"]), (Labeled[, "RegularPlay"])}})

And another

(Legended[({{(Labeled[, "SowOnlyPlay"]), (Labeled[, "ExtraTurnPlay"])}, {(Labeled[, "CapturePlay"]), (Labeled[, "RegularPlay"])}}), "* = not computed"])

Number of games and number of plays

Games

The table below presents some statistics on the number of games (paths in the game graph)

The set of paths for the larger game graphs could not be computed with the hardware available (the computation was memory bound, (Quantity[251.7130241394043`, "GiB", gibibytes, "Gibibytes"]))

Plotting these data by row or column gives a better feel for how quickly the number of games grows

The growth is exponential in the number of seeds and super-exponential in the number of pits

Here is another view of the same data

({{(Labeled[, "SowOnlyPlay"]), (Labeled[, "ExtraTurnPlay"]), (Labeled[, "CapturePlay"]), (Labeled[, "RegularPlay"])}})

And another

(Legended[({{(Labeled[, "SowOnlyPlay"]), (Labeled[, "ExtraTurnPlay"])}, {(Labeled[, "CapturePlay"]), (Labeled[, "RegularPlay"])}}), "* = not computed"])

Number of plays

Here are the distributions of the number of plays (path length)

The distributions are roughly bell-shaped and broad, COV 15-85%

The capture rule shortens games substantially, by about (1/3)

The average length of a game grows linearly in both the number of seeds and the number of pits

Scores and wins

Here are the distributions of the game scores

The capture rule gives a decided advantage to the first player (Blue) when there are 5 seeds

Here is the breakdown for wins, ties, and losses

Fairness

Fairness under random play can be computed at the game state level, beginning at the leaf vertices of the game graph, and then propagating upwards to the root vertex (the initial game state)

A value of +1 indicates a game state entirely in favor of player 1, a value of -1 fully in favor of play 2, and 0 a tie

Here are the results using the function vertexFairness, on the 2 pit, 2 seed game with the four types of play

Red indicates a vertex in player 2’s favor, green a vertex in player 1’s favor, and white a tie

The vertex fairness of the root vertex is the fairness of the game set-up and play

({{(Labeled[, ({{"SowOnlyPlay"}, {-0.125`}})]), (Labeled[, ({{"ExtraTurnPlay"}, {-0.5`}})]), (Labeled[, ({{"CapturePlay"}, {0.125`}})]), (Labeled[, ({{"RegularPlay"}, {-0.5`}})])}})

Here are the result for all the game set-ups

A better understanding can be had from histograms

Most of the extreme values come from game set-ups with only 1 pit

If we exclude those game set-ups, the average fairness under random play is -0.0186, that is, a very slight favor for player 2

Early win decidability

We looked at two kinds of strategy:

Random play—the players can choose any pit; this is effectively 0-depth search

Perfect play—the players choose any pit that is advantageous; this is effectively full-depth search

Random play

We did not do an exhaustive analysis of early win decidability for arbitrary play

We can get a qualitative assessment using the colorByOutcome function

The gameGraph function already assigns a color to a vertex if one of the stores in the corresponding game state has more than half the seeds

The function colorByOutcome will propagate that coloring up the graph until an undecidable vertex is reached

Not very early for this game set-up, but we’ll see later that this is not always the case

Assuming the games with 3 pits and 2 seeds are representative of larger game set-ups, we have these results:

({{(Labeled[, ({{"SowOnlyPlay", , , }, {->(QuantityPostfix[63, "%", percent, "Percent"]), ->(QuantityPostfix[6, "%", percent, "Percent"]), ->(QuantityPostfix[16, "%", percent, "Percent"]), ->(QuantityPostfix[15, "%", percent, "Percent"])}})]), (Labeled[, ({{"ExtraTurnPlay", , , }, {->(QuantityPostfix[62, "%", percent, "Percent"]), ->(QuantityPostfix[6, "%", percent, "Percent"]), ->(QuantityPostfix[14, "%", percent, "Percent"]), ->(QuantityPostfix[18, "%", percent, "Percent"])}})])}, {(Labeled[, ({{"CapturePlay", , , }, {->(QuantityPostfix[49, "%", percent, "Percent"]), ->(QuantityPostfix[8, "%", percent, "Percent"]), ->(QuantityPostfix[21, "%", percent, "Percent"]), ->(QuantityPostfix[22, "%", percent, "Percent"])}})]), (Labeled[, ({{"RegularPlay", , , }, {->(QuantityPostfix[45, "%", percent, "Percent"]), ->(QuantityPostfix[10, "%", percent, "Percent"]), ->(QuantityPostfix[21, "%", percent, "Percent"]), ->(QuantityPostfix[24, "%", percent, "Percent"])}})])}})

About half or more of the game states are undecided

Perfect play

The vertexWinStatus function with Method->"PerfectPlay" assigns a win status to every vertex of the game graph based on the win status of the immediately succeeding vertices and who’s play it is

Blue will choose a blue over everything else, a tie over yellow, and yellow if it’s the only choice

Yellow will choose yellow over everything else, a tie over blue, and blue if it’s the only choice

({{, "\!\(\*UnderoverscriptBox[\(→\), \(Method -> \"PerfectPlay\"\), \(vertexWinStatus\)]\)", }})

Here are the results for the smaller game set-ups that can be legibly displayed

These results are consistent with those reported by Donkers et al. []

The curious case of regular play with 2 pits and 5 seeds

Recall that the first player (Blue) won all but one game

\[KernelIcon];graph=multiway[{initializeBoard[2,5],1},"labelEdgeWithMove"->True]//gameGraph[#,]&

Not only that, but the majority of the games were won well before the last play by capturing at least half the seeds (note all the blue internal nodes)

One can push the frontier of these blue nodes further up the game graph to nodes whose out-edges all lead to blue nodes

\[KernelIcon];colorByOutcome[graph]

Blue can ensure a win by always choosing the first pit on the first move

Taking away the capture rule radically changes the game

\[KernelIcon];multiway[{initializeBoard[2,5],1},Method->"ExtraTurnPlay"]//gameGraph[#,]&//colorByOutcome//

Multiway Analysis

Branchial graphs

The edges of a branchial graph represent states in the multiway graph that immediately follow a common predecessor state

They can provide insight to “entanglement” of “nearby paths”

Another way of describing this phenomenon is “mixing” or “unpredictability”

The brachial graphs below are limited to up to 20 or 10 time steps for brevity

SowOnlyPlay, 2 pits, 1 to 6 seeds

Here are the game graphs for the 2-pit game board and 1 to 6 seeds under SowOnlyPlay:

({{, , , , , }})

And, here are their branchial graphs:

RegularPlay, 2 pits, 1 to 6 seeds

For RegularPlay we have

({{, , , , , }})

And, their branchial graphs:

All plays, 3 pits, 2 seeds

For the 3-pit, 2-seed board and all plays we have

({{, , , }})

And, their branchial graphs:

All plays, 3, 4, 5, and 6 pits, 1 seed

Increasing the number of pits should allow more opportunity for mixing

To keep the computations feasible, only 1 seed was used

3 pits

4 pits

5 pits

6 pits

The depth of the multiway search was restricted to 7 levels

Observations

The branchial graphs of the smaller game set-ups are rather simple, suggesting little entanglement of nearby paths, or mixing

Increasing the number of seeds does not increase the mixing

Increasing the number of pits above 3 is needed to achieve non-trivial mixing

The extra-turn play leads to substantially more mixing above 4 pits

Causal invariance

A system that is causally invariant will always give the same output for a given input regardless of the internal evaluation order

This is a good thing for algebra []

And, is often a desirable property for a puzzle

But, perhaps is not so desirable for a game

The function causalInvariantGraphQ can be used to test for causal invariance of a game multiway graph

\[KernelIcon];?causalInvariantGraphQ

All of the games with 1 pit are causally invariant

The players have only one pit from which to choose

All of the larger game set-ups are not causally invariant; for example here are the graphs for 2 pits and 1 to 6 seeds:

I was originally expecting sow-only play to be causally invariant, but it’s not

The end-of-game rule is the likely reason

Summary

Complexity

A typical measure of complexity is the size of the game graph

Only a small portion of the full state space is actually visited

The capture rule further reduces the coverage

The growth is polynomial in the number of seeds and exponential in the number of pits

The number of games can also measure complexity

The growth is exponential in the number of seeds and super-exponential in the number of pits

A third measure is the length of a game

The capture rule shortens games substantially, by about (1/3)

The distributions are roughly bell-shaped and broad, COV 15-85%

The average length of a game grows linearly in both the number of seeds and the number of pits

Yet another measure, although qualitative, is the character of the branchial graphs

Increasing the number of seeds does not increase the mixing/unpredictability

Increasing the number of pits above 3 is needed to achieve substantial mixing/unpredictability

Including the extra-turn rule greatly increases the mixing/unpredictability for 5 or more pits

Fairness

Only one game set-up (1 pit, 3 seeds, regular play) permitted a player to capture all the seeds

Under random play, the distribution of scores is symmetric, with the number of ties usually being the most frequent

For sow-only and extra-turn rule play, the number of tied games usually exceeded the number of games won by either player

For capture and regular rule play, the number of games won by either player usually exceeded the number of tied games

Under random play, the first player wins more often than the second player overall

There is a good deal of variability in the ratio

There are some decidedly pathologic cases giving nearly complete dominance to the first payer

Under random play, a fairness evaluation of the game graphs suggests a slight advantage to the second player

Early win

Perfect play does not necessarily allow the first player to win or draw

Interactive GUI

Construction

Game board display

Interestingly, the display of the game board is done entirely with the textual elements:

Row

Grid

Framed

Here is the code for making a pit, the code for a store is similar

makePit[seeds_, fontSize_, color_, seedColorThreshold_] := 
	With[{background=If[seeds > 0, color, White]}, 
		{grayLevel=First[ColorConvert[background, "GrayScale"]]}, 
		{fontColor=If[grayLevel < seedColorThreshold, White, Black]}, 
		Framed[
			Style[seeds, 
				FontSize -> fontSize, 
				FontColor -> fontColor
				], 
			RoundingRadius -> 15, 
			ImageSize -> {30, 30}, 
			Alignment -> Center, 
			Background -> background
			]
		]

The board part of the game state is the foundation of the displayed board, and the styling functions are mapped over the pits and stores with MapAt

For the player’s pits:

(* color & frame the player's pits *)
board = MapAt[
	makePit[#1, fontSize, colors[[1]], seedColorThreshold] & , 
	board, 
	{1, $pits, All}
	];

For the player’s store:

(* color & frame the player's store *)
board = MapAt[
	makeStore[#1, fontSize, colors[[1]], seedColorThreshold] & , 
	board, 
	{1, $store}
	];

The opponent’s list of pits is then reversed, and each list of pits is placed in a Row

The whole board is reversed, placed in a Grid, and then placed in a Framed

Lastly, the players’ names and the board are placed in a Row

Dynamic game board display

The function MancalaGUI displays the game board along with other dynamic elements  for playing the game

It uses a finite state automaton to control the overall display and dynamic elements available to the user

\[KernelIcon];Graph[{"●"\[DirectedEdge]"setup","setup"\[DirectedEdge]"start","start"\[DirectedEdge]"play","play"\[DirectedEdge]"play","play"\[DirectedEdge]"gameOver","gameOver"\[DirectedEdge]"recap","gameOver"\[DirectedEdge]"start","gameOver"\[DirectedEdge]"setup","recap"\[DirectedEdge]"start","recap"\[DirectedEdge]"setup"},]
closeCellGroup[]

The FSA is implemented with a Switch

$state = "setup"; 

Dynamic[
	Switch[$state, 
		"setup", 
			(* dynamic UI to set the game board set-up, type of play, 
				the players' names and colors, and a button to start the play *), 
			
		"start", 
			(* initialize the game state, clear the history list, and set 
				the FSA state to "play" *), 
			
		"play", 
			(* display dynamic game board with buttons *), 
			
		"gameOver", 
			(* display final game board and buttons to recap the game, 
				play again, or play with new players *), 
			
		"recap", 
			(* display the history of the game and buttons to play again or 
				play with new players *)
		], 
	TrackedSymbols :> {$state, $gameBoard}
	]

To make the displayed board dynamic, a player’s pit with seeds is wrapped with a Button whose main action is to compute the next game state with the $playFunction function

$playFunction is one of playRegular, playCapture, playExtraTurn, or playSowOnly

board[[1, $pits]] = MapIndexed[
	Function[{seeds, pit}, 
		With[{glyph=makePit[seeds, fontSize, colors[[1]], seedColorThreshold]}, 
			If[makeButtons && seeds > 0,
				Button[glyph, 
					Dynamic[
						AppendTo[$history, {$gameBoard, $currentPlayer, First@pit}]; 
						{$gameBoard, $currentPlayer} = 
							$playFunction[$gameBoard, $currentPlayer, First@pit];
						If[gameOverBoardQ[$gameBoard],
							$state = "gameOver";
							AppendTo[$history, {$gameBoard, $currentPlayer, 0}];
							],
						TrackedSymbols :> {$gameBoard, $currentPlayer}
						], 
				Appearance -> None],
			  (* else *)
				Button[glyph, Appearance -> None, Enabled -> False]
				]
			]
		], 
	board[[1, $pits]]
	];

Otherwise the rest of the board construction is the same

Let’s play!

\[KernelIcon];mancalaGUI[]

Thank You!

Back Matter

Acknowledgements

Brad Klee was particularly helpful with discussions about causal invariance, branchial graphs, modeling perfect play, and computing fairness

David Reiss is a Dynamic maven and provided a number of suggestions that improved the performance of the interactive GUI

The Wolfram Solutions Department provided lots of direct and indirect feedback during an early presentation

References

Amritesh Anand (2021) “[WSS 2021] Multi-way system analysis of puzzles​” Wolfram Community. 
https://community.wolfram.com/groups/-/m/t/2313755

J. Donkers, J. Uiterwijk, and A. de Voogt (2001). “Mancala games-Topics in mathematics and artificial intelligence.” In Step by Step. Proceedings of the 4th Colloquium ‘Board Games in Academia’eds. Retschitzki, J. and Haddad-Zubel, R. Fribourg, Switzerland: Universsity of Fribourg (pp. 133-146).
https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=2745494c6e96cdbb6636e941fa2a6f3dc4605ea0

G. Irving, J. Donkers, and J. Uiterwijk (2000), “Solving Kalah,” ICGA Journal, 23(3), 139-147. 
https://content.iospress.com/articles/icga-journal/icg23303

Robert B. Nachbar (2022) “Modeling Chemical Reaction Networks With Token-Event Graphs” Wolfram Technology Conference 2022. 
https://www.wolfram.com/broadcast/video.php?sx=arduino&c=104&p=6&disp=list&v=3812

Stephen Wolfram (2022), “Games and Puzzles as Multicomputational Systems,” Stephen Wolfram Writings. 
https://writings.stephenwolfram.com/2022/06/games-and-puzzles-as-multicomputational-systems.

Stephen Wolfram (2023), “Expression Evaluation and Fundamental Physics,” Stephen Wolfram Writings. 
https://writings.stephenwolfram.com/2023/09/expression-evaluation-and-fundamental-physics.

Bowei Zhang (2022), “[WSC22] Analyze the multiway graph of Mancala,” Wolfram Community. 
https://community.wolfram.com/groups/-/m/t/2580430

Initialization

Quit[]

AppendTo[$Path, NotebookDirectory[]];

<<Mancala`

SetSystemOptions["GraphOptions" -> 
    {"VertexCountThreshold" -> 2000, 
     "EdgeCountThreshold" -> 10000}];

$blue = ColorData[{"Indexed", "BoldColor"}][1]; 
$yellow = ColorData[{"Indexed", "BoldColor"}][2];

closeCellGroup, centerAndCloseCellGroup

closeCellGroup[] := (SelectionMove[EvaluationCell[], Next,Cell]; 
FrontEndExecute[FrontEndToken["SelectionCloseUnselectedCells"]])

centerAndCloseCellGroup[] := (SelectionMove[EvaluationCell[], Next,Cell]; 
SetOptions[NotebookSelection[],{TextAlignment->Center,ShowCellLabel->False}];
FrontEndExecute[FrontEndToken["SelectionCloseUnselectedCells"]])

animatePlay

italic[str_String]:=Style[str,Italic]

animatePlay[boardIn_?validBoardQ,player:1|2,pit_Integer?Positive,names:{_String,_String}]:=Module[{board=boardIn,playList={},$pits=Mancala`Private`$pits,$store=Mancala`Private`$store,nPits,opponent,seeds,p,state},
$animate=True;
nPits=Length@board[[player,$pits]];
opponent=Mod[player+1,2,1];
seeds=board[[player,$pits,pit]];
AppendTo[playList,Labeled[displayBoard[board,player,names],italic["initial board"],Bottom]];
AppendTo[playList,Labeled[displayBoard[board=MapAt[0&,board,{player,$pits,pit}],player,names,"Highlight"->{player,pit}],StringTemplate["take seeds from pit ``"][pit]//italic,Bottom]];
p=pit+1;
state="playerPits";
While[seeds>0,
Switch[state,
"playerPits",
Do[AppendTo[playList,Labeled[displayBoard[board=MapAt[#+1&,board,{player,$pits,p}],player,names,"Highlight"->{player,p}],StringTemplate["sow player's pit ``"][p]//italic,Bottom]];seeds--;p++,Min[seeds,nPits-p+1]];
If[seeds>0,state="playerStore"],

"playerStore",
AppendTo[playList,Labeled[displayBoard[board=MapAt[#+1&,board,{player,$store}],player,names,"Highlight"->{player,0}],italic["sow player's store"],Bottom]];seeds--;
If[seeds>0,p=1;state="opponentPits"],

"opponentPits",
Do[AppendTo[playList,Labeled[displayBoard[board=MapAt[#+1&,board,{opponent,$pits,p}],player,names,"Highlight"->{opponent,p}],StringTemplate["sow opponent's pit ``"][p]//italic,Bottom]];seeds--;p++,Min[seeds,nPits-p+1]];
If[seeds>0,p=1;state="playerPits"]
]
];
If[state=="playerPits" && board[[player,$pits,p-1]]==1,
AppendTo[playList,Labeled[displayBoard[board,player,names,"Highlight"->{opponent,nPits-(p-1)+1}],italic["capture"],Bottom]];
board=MapAt[#+board[[opponent,$pits,nPits-(p-1)+1]]+1&,board,{player,$store}];
board=MapAt[0&,board,{player,$pits,p-1}];
board=MapAt[0&,board,{opponent,$pits,nPits-(p-1)+1}];
AppendTo[playList,Labeled[displayBoard[board,player,names,"Highlight"->{player,0}],italic["move to player's store"],Bottom]]
];
Switch[board,
{{{(0)..},_},_},
(* sweep player 2 *)
AppendTo[playList,Labeled[displayBoard[board,player,names],italic["game over"],Bottom]];
board=MapAt[#+Total@board[[2,$pits,All]]&,board,{2,$store}];
board=MapAt[0&,board,{2,$pits,All}];
AppendTo[playList,Labeled[displayBoard[board,player,names],StringTemplate["sweep seeds to ``'s store"][If[player==2,"player",opponent]]//italic,Bottom]];
AppendTo[playList,Labeled[displayBoard[board,player,names],italic["game over"],Bottom]],
{_,{{(0)..},_}},
(* sweep player 1 *)
AppendTo[playList,Labeled[displayBoard[board,player,names],italic["game over"],Bottom]];
board=MapAt[#+Total@board[[1,$pits,All]]&,board,{1,$store}];
board=MapAt[0&,board,{1,$pits,All}];
AppendTo[playList,Labeled[displayBoard[board,player,names],StringTemplate["sweep seeds to ``'s store"][If[player==1,"player",opponent]]//italic,Bottom]];AppendTo[playList,Labeled[displayBoard[board,player,names],italic["game over"],Bottom]],
_,
Switch[state,
"playerPits",
AppendTo[playList,Labeled[displayBoard[board,player,names],italic["end of turn"],Bottom]];
AppendTo[playList,Labeled[displayBoard[board,opponent,names],italic["rotate the board"],Bottom]],

"playerStore",
AppendTo[playList,Labeled[displayBoard[board,player,names],italic["extra turn"],Bottom]],

"opponentPits",
AppendTo[playList,Labeled[displayBoard[board,player,names],italic["end of turn"],Bottom]];
AppendTo[playList,Labeled[displayBoard[board,opponent,names],italic["rotate the board"],Bottom]]
]
];
$animate=False;

FlipView[playList]
]

unresolvedBranchPairQ

General::notdag = "The input graph `` must be a directed \
acyclic graph.";

unresolvedPairQ::usage = "unresolvedPairQ[g, {u, v}] gives True if \
vertices u and v do not share a common descendant vertix in graph g."; 
unresolvedPairQ[g_Graph, pair:{_, _}] /; 
		(DirectedGraphQ[g] && AcyclicGraphQ[g]) || 
		Message[unresolvedPairQ::notdag, g] := 
	Reap[DepthFirstScan[g, #1, 
			{"DiscoverVertex" -> (Sow[#1] & )}]
		][[2,1]]& /@ pair // Apply[Intersection] // MatchQ[{}]
SetAttributes[unresolvedPairQ, ReadProtected];

causalInvariantGraphQ

causalInvariantGraphQ::usage = "causalInvariantGraphQ[g] give True if \
any unresolved branch pairs are present in graph g."; 
causalInvariantGraphQ[g_Graph] /; 
		(DirectedGraphQ[g] && AcyclicGraphQ[g]) || 
		Message[causalInvariantGraphQ::notdag, g] := 
	Module[{branchPairs}, 
		branchPairs = ResourceFunction["BranchialGraphs"][g] // 
			(Map[EdgeList /* Map[Take[#1, 2]&] /* MapApply[List]]) // 
			Catenate; 
		!AnyTrue[branchPairs, unresolvedPairQ[g, #1]&]
		]
SetAttributes[causalInvariantGraphQ, ReadProtected];