Donkey-Pong

Friday after work we socialize and have some drinks. It is after couple of drinks we qualify to play Donkey-Pong. It can only be played after couple of drinks as the sober mind rejects the notion of such a game. The rules are simple, you need six people and a ping-pong table. The players move clockwise or anti-clockwise hitting the ball and then the players on the other side do the same. The player who hits the ball on one side has to hit the ball from other side until there are only 2 players left in the game, at which point they stop running around the table. Donkey-pong allows the rules to be bent.

The player who faults gets “D” added to the score and on next fault gets “O” added to the score and the total score becomes “DO”. This continues till the score is “DONKEY”, at which point the player stops playing and can continue drinking. This continues until 5 players score “DONKEY”, the last one wins the game.

We will implement a scoreboard to track the scores for players as it is hard to do that after drinking. We will use Fabel-Elmish-React template to create this scoreboard.

The first thing to do is to install the Fable-Elmish-React template by following instructions on the page.

We then create a new app in folder called DonkeyPong using following command

dotnet new fable-elmish-react -n DonkeyPong -lang F#

Go to folder DonkeyPong and run following commands

yarn install
dotnet restore

Go to src folder and run following command

dotnet fable yarn-start

I am using VSCode with Ionide plugin. We will leave all the other files in the project. Add Game folder under src folder and following three files to project.

1. State.fs
2. Types.fs
3. View.fs

Make sure you add these files to DonkeyPong.fsproj file.

Let’s start by adding types to Types.fs. First we add

type Player = {
Name : string
Score : string option
IsPlaying : bool
}

Name is the name of the player, we will initialize it in the init()function later in State.fs . Score stores the current score of the player and IsPlaying is true while the user is still playing and the score is not DONKEY.

Add following type to indicate current state of the Game.

type GameState =
| New
| Active
| Over

We also declare following values.

let FinalScore = "DONKEY"
let FinalScoreLength = FinalScore.Length

Let’s declare Game type as

type Game = {
GameState : GameState
Players : Player seq
Winner : Player option
}
type Model = Game

GameState stores the current state of the Game. Players is the sequence of players paying the game. Winner is optional and will store the winner of the game. We declare Model as a type ofGame. This is the model in our Elmish app.

Add Msg type for the messages in our app.

type Msg =
| StartNew
| AddScore of Player

StartNew is used to start a new game. AddScore adds score to the player passed in the message.

Let’s add functions to State.fs to manipulate the model. The init() function is as shown below

let private getPlayers count =
seq { for i in 1..count do
yield {
Name = "Player " + i.ToString()
Score = None
IsPlaying = true
}
}
let init () : Model * Cmd<Msg> =
{
GameState = New
Players = getPlayers 6
Winner = None
}, []

Here getPlayers returns a sequence of players initializing the name to Player 1,Player 2,... and Score to None and IsPlaying to true. In the init function Game type is returned as shown above.

Add following code to update the model

let incPlayerScore player =
let curScoreLength, score =
match player.Score with
| None -> 0, ""
| Some score -> score.Length, score
if curScoreLength < FinalScoreLength then
{ player with
Score = Some (score + FinalScore.[curScoreLength].ToString())
IsPlaying = (curScoreLength + 1) < FinalScoreLength
}
else
player
let update msg model : Model * Cmd<Msg> =
match msg with
| StartNew ->
init()
| AddScore player ->
if model.GameState = Over then
model, []
else
let players =
model.Players
|> Seq.choose (fun x -> if x = player then Some (incPlayerScore x) else Some x)
let playersPlaying =
players
|> Seq.filter (fun x -> x.IsPlaying)
|> Seq.length
{ model with
GameState = if playersPlaying = 1 then Over else Active
Players = players
Winner = if playersPlaying = 1 then players |> Seq.tryFind(fun x -> x.IsPlaying) else None
}, []

Function incPlayerScore keeps track of player’s score by adding one char at a time to make the word DONKEY.

In the update function for StartNew init function is called to reset the game. For message AddScore we return model and no message if the game is over, otherwise a new player list is created with score incremented for player passed in the message. playersPlaying stores the number of players currently playing the game. If playersPlaying is 1 then GameState is set to Over otherwise it is still set as Active . If playersPlaying is 1 then the game is over and we set the last player with IsPlaying as the winner.

Let’s add following code to View.fs file.

let simpleButton txt action dispatch isActive =
a
[ ClassName ("button btn-primary " + (if isActive then "active" else ""))
Style[CSSProp.Width "170px"]
OnClick (fun _ -> action |> dispatch) ]
[ str txt ]
let root model dispatch =
div[][
yield simpleButton "Reset Game" StartNew dispatch true
if model.GameState = Over then
yield div[][str (sprintf "Game Over - Winner is %s" model.Winner.Value.Name)]

for player in model.Players do
yield div[][
div[ClassName "column is-narrow"][
div[ClassName "column is-narrow"][
strong[][str (player.Name)]
strong[][str (" Score: " + (player.Score |> Option.defaultValue ""))]
]
simpleButton "+" (AddScore player) dispatch player.IsPlaying
]
]
]

Here we list all players with their name current score and a button to increase the score for the player. The view looks as shown in the following screenshot.

Clicking the button adds one character at a time to the players score until it becomes DONKEY , and when 5 players have become DONKEY the last one is declared as the winner as show below.

Reset Game button can be used to reset the scoreboard.

The source code for this app is here.