This repository contains a calculator for probabilities in the game risk.

Zelphir Kaltstahl 51b9cec401 move todo list items 11 months ago
calculator.scm 70e524544c correct format fraction usage 11 months ago
display-utils.scm b065955821 improve display-summary, taking fraction formatter now 11 months ago
helpers.scm 15f2672d0d remove obsolete #:key for proc 11 months ago
model.scm 974db417c1 adapt att-lost? and def-lost? procedures to take rules 11 months ago
random.scm 4434931266 split up into modules 1 year ago
readme.org 51b9cec401 move todo list items 11 months ago

# Intro

This is a simulation tool for moves in the board game risk. Its approach is to run a simulation as many times as specified and then calculate probabilities by counting results of those simulations. It offers generic procedures to facilitate running simulations.

The rules of a risk game are usually as follows:

1. attacker may use up to 3 dice, but may choose to use fewer dice
2. defender may use up to 2 dice and cannot choose to use fewer than that
3. on equal eye count on the dice, the defender wins

However, this program allows modification of the rules. Some procedures allow specifying non-standard rules, which they give to other procedures in the process of running the simulations, so that the whole calculation is based on those modified rules.

The program makes use of a random number generator (RNG), which outputs uniformly distributed random numbers. It is possible to roll dice with more than 6 sides, but currently this is not used by any simulation procedure.

# Terminology

die
n-sided die
dice
more than one n-sided die
round
rolling dice for attackers and defenders once
fight
rolling dice for as many times as it takes for the attacker or defender to be unable to roll dice
battle
multiple subsequent fights, each with remaining attackers and defenders after the previous fight
annihilation
rolling dice, playing rounds, until the attacker cannot roll any die any longer or won the fight.

# Code conventions

• The attacker count of dice should always be first and the defender count of dice behind in arguments to procedures.

# Usage

## Creating the random number generator

You can create a random number generator with an optionally given seed as follows:

``````
(make-random-integer-generator #:seed 12345)
``````

Setting a seed will make results reproducible.

## Creating rules

You can create game rules as follows:

``````
(make-risk-rules att-dice def-dice eq-eyes-win)
``````

Where `eq-eyes-win` is a symbol, one of the following:

• ~'att~: meaning the attacker wins, if eye count is equal
• ~'def~: meaning the defender wins, if eye count is equal
• ~'none~: meaning no one loses units, if the eye count is equal
• ~'both~: meaning both players lose units, if the eye count is equal

## Calculating rounds

There is a procedure named `simulate-round`, which rolls dice for a single round of risk. Here is an example for running the simulation once using default risk game rules:

``````
(simulate-round
(make-init-fight-situation 4 3)
(make-random-integer-generator #:seed 12345)
#:rules *default-risk-rules*)
``````

You can run this simulation an arbitrary number of times, collecting a result table, which counts the outcomes as follows:

``````
(display-summary
(try-n-times (make-init-fight-situation 4 3)
10000
simulate-round
(make-random-integer-generator)
#:rules *default-risk-rules*)
#:fraction-formatter (λ (frac)
(format-fraction frac #:precision 3)))
``````

The procedure `try-n-times` will run the simulation `10000` times and the results will be counted in a resulting table. That table is summarized and by `display-summary`, which counts occurences of unique round results, calculates their probability and displays that.

## Calculating fights (annihilation)

Simulation of annihilation works the same way as simulating single rounds:

``````
(simulate-fight
(make-init-fight-situation 10 7)
(make-random-integer-generator #:seed 12345)
#:rules *default-risk-rules*)
``````

As does simulation of multiple annihilations:

``````
(display-summary
(try-n-times (make-init-fight-situation 10 7)
10000
simulate-fight
(make-random-integer-generator #:seed 12345)
#:rules *default-risk-rules*))
``````

## Calculating battle

Simulating battles can be done as follows:

``````
(displayln
(format-fraction
(calc-att-winning-prob
(let ([rand-int-gen (make-random-integer-generator #:seed 12345)])
(try-n-times-general
(lambda ()
(simulate-battle 8
'(3 2 1)
rand-int-gen
#:rules *default-risk-rules*))
100000)))
#:precision 5))
``````

There is also another procedure, which results in a battle win probability instead of a large table of all occurring courses of the battle and internally repeats fights a given number of times, instead of repeating the battle a given number of times:

``````
(displayln
"Probability of winning with 9 against 1, 1, 10:"
(format-fraction
(calc-battle-att-win-prob 9
'(1 1 10)
10000
(make-random-integer-generator #:seed 12345)
#:rules *default-risk-rules*)
#:precision 5))
``````

If you want to make assumptions about outcomes of fights of a battle, you can use another procedure as follows:

``````
(displayln
(format-fraction
(calc-consecutive-att-win-prob '(9 8 7)
'(1 1 10)
10000
(make-random-integer-generator #:seed 12345)
#:rules *default-risk-rules*)
#:precision 5))
``````

## Calculating win probability for attacker or defender

Given a result table containing fight results, you can quickly calculate the winning probability for attacker or defender as follows:

``````
(displayln
(format-fraction
(calc-att-winning-prob
(try-n-times (make-init-fight-situation 10 7)
10000
simulate-fight
(make-random-integer-generator #:seed 12345)
#:rules *default-risk-rules*))
#:precision 5))
``````

There is a convenience procedure for calculating defender winning probability as well:

``````
(displayln
(format-fraction
(calc-def-win-prob
(try-n-times (make-init-fight-situation 10 7)
10000
simulate-fight
(make-random-integer-generator #:seed 12345)
#:rules *default-risk-rules*))
#:precision 5))
``````

# To do [6/8]

• [X] `calc-battle-att-win-prob` is strange in the sense, that it internally performs `n` simulations, instead of being wrapped like the other procedures with `try-n-times`. It should be refactored to simulate the battle only once and then be simulated `n` times via the `try-n-times` procedure. – Or should it not?
• [X] rename: `annihilate` -> `simulate-fight`
• [X] rename: `try-single-round` -> `simulate-round`
• [X] make `att-lost?` take rules as an argument as well
• [X] make `def-lost?` take rules as an argument as well
• [X] `display-summary` should take a formatter procedure for formatting the fractions, instead of a precision and assuming, that the user wants to see decimal numbers. Giving a formatter procedure makes no assumptions about what the user wants.
• [ ] Add more rules and the logic to calculate according to them.
• [ ] Add number of sides of a die to the rules struct, so that different dice can be used.
• [ ] Rules and rules interpretation in the program could be modified, so that the attacker needs to have not only 1 more eye on a die, but any number of eyes more than the defender, to win.
• [ ] improve `try-n-times`
• ] use only `try-n-times-general` instead of `try-n-times`
• ] remove `try-n-times`
• ] rename: `try-n-times-general` -> `try-n-times`