calculator.scm 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. ;; ===========
  2. ;; TERMINOLOGY
  3. ;; ===========
  4. ;; DIE: six sided die
  5. ;; DICE: more than one die
  6. ;; ROUND: rolling dice for attackers and defenders once
  7. ;; FIGHT: rolling dice for as many times as it takes for the attacker or
  8. ;; defender to be unable to roll dice
  9. ;; BATTLE: multiple subsequent fights, each with remaining attackers and
  10. ;; defenders after the previous fight
  11. ;; ANNIHILATION: rolling dice, playing rounds, until the attacker cannot roll
  12. ;; any die any longer or won the fight.
  13. (use-modules
  14. ;; SRFI 1 for additional list procedures
  15. (srfi srfi-1)
  16. ;; SRFI 8 for receive form
  17. (srfi srfi-8)
  18. ;; SRFI-27 for random number utilities
  19. (srfi srfi-27)
  20. ;; SRFI-69 for hash tables
  21. (srfi srfi-69)
  22. ;; format for formatting floats
  23. (ice-9 format)
  24. ;; for functional structs (not part of srfi-9 directly)
  25. (srfi srfi-9 gnu)
  26. (rnrs enums)
  27. (display-utils)
  28. (helpers)
  29. (model)
  30. (random))
  31. ;; ==========
  32. ;; RISK LOGIC
  33. ;; ==========
  34. (define roll-die
  35. (lambda* (rand-int-proc #:key (die-sides 6))
  36. (+ (rand-int-proc die-sides) 1)))
  37. (define roll-dice
  38. (λ (dice-count rand-int-proc)
  39. (let loop ([remaining-dice dice-count]
  40. [results '()])
  41. (cond
  42. [(= remaining-dice 0) results]
  43. [else
  44. (loop (- remaining-dice 1)
  45. (cons (roll-die rand-int-proc) results))]))))
  46. (define calculate-round-result
  47. (lambda* (fight-situation
  48. rolls-attacker
  49. rolls-defender
  50. #:key
  51. (rules *default-risk-rules*))
  52. ;; The defender has at maximum the same number of dice
  53. ;; the attack has, so we can iterate over the defender
  54. ;; rolls.
  55. (let loop ([remaining-rolls-att (sort rolls-attacker >)]
  56. [remaining-rolls-def (sort rolls-defender >)]
  57. [att-wins 0]
  58. [def-wins 0])
  59. (cond
  60. ;; Attacker rolls can be exhausted before defender
  61. ;; rolls, as it is possible to do a minority attack.
  62. [(or (null? remaining-rolls-def) (null? remaining-rolls-att))
  63. (make-fight-situation (+ (get-att-wins fight-situation) att-wins)
  64. (- (get-att-rems fight-situation) def-wins)
  65. (+ (get-def-wins fight-situation) def-wins)
  66. (- (get-def-rems fight-situation) att-wins))]
  67. [else
  68. (let ([max-att-roll (car remaining-rolls-att)]
  69. [max-def-roll (car remaining-rolls-def)])
  70. (cond
  71. [(< max-att-roll max-def-roll)
  72. (loop (cdr remaining-rolls-att)
  73. (cdr remaining-rolls-def)
  74. att-wins
  75. (+ def-wins 1))]
  76. [(> max-att-roll max-def-roll)
  77. (loop (cdr remaining-rolls-att)
  78. (cdr remaining-rolls-def)
  79. (+ att-wins 1)
  80. def-wins)]
  81. ;; The case of equal eyes on the dice needs to be
  82. ;; treated specially, according to the rules
  83. ;; give.
  84. [(= max-att-roll max-def-roll)
  85. (let ([eq-eyes-win-rule (get-eq-eyes-win rules)])
  86. (cond
  87. [(eq? eq-eyes-win-rule 'def)
  88. (loop (cdr remaining-rolls-att)
  89. (cdr remaining-rolls-def)
  90. att-wins
  91. (+ def-wins 1))]
  92. [(eq? eq-eyes-win-rule 'att)
  93. (loop (cdr remaining-rolls-att)
  94. (cdr remaining-rolls-def)
  95. (+ att-wins 1)
  96. def-wins)]
  97. [(eq? eq-eyes-win-rule 'both)
  98. (loop (cdr remaining-rolls-att)
  99. (cdr remaining-rolls-def)
  100. (+ att-wins 1)
  101. (+ def-wins 1))]
  102. [(eq? eq-eyes-win-rule 'none)
  103. (loop (cdr remaining-rolls-att)
  104. (cdr remaining-rolls-def)
  105. att-wins
  106. def-wins)]
  107. [else
  108. (error "unknown equal eyes win rule")]))]))]))))
  109. (define simulate-round
  110. (lambda* (fight-situation
  111. rand-int-proc
  112. #:key
  113. (rules *default-risk-rules*))
  114. "Roll dice and calculate the result of the round. Return a new fight
  115. situation."
  116. (define calc-max-att-count
  117. (λ (att-dice-count rules)
  118. (min
  119. ;; At least one unit needs to remain in the source
  120. ;; area.
  121. (- att-dice-count 1)
  122. ;; Only so many may attack in one round, accordung
  123. ;; to the rules.
  124. (get-att-dice rules))))
  125. (define calc-max-def-count
  126. (λ (def-dice-count rules)
  127. (min
  128. ;; The defender may use up all their units.
  129. def-dice-count
  130. ;; Only so many may defend in one round, according
  131. ;; to the rules.
  132. (get-def-dice rules))))
  133. (let ([rolls-attacker
  134. (roll-dice (calc-max-att-count (get-att-rems fight-situation) rules)
  135. rand-int-proc)]
  136. [rolls-defender
  137. (roll-dice (calc-max-def-count (get-def-rems fight-situation) rules)
  138. rand-int-proc)])
  139. (calculate-round-result fight-situation
  140. rolls-attacker
  141. rolls-defender
  142. #:rules rules))))
  143. (define calc-att-winning-prob
  144. (λ (count-table)
  145. "Calculate the winning probability of the attacker given
  146. a fight results table."
  147. (let ([att-wins-def-wins
  148. (hash-table-fold count-table
  149. (λ (fight-result-as-list count previous)
  150. ;; There are only 2 possible
  151. ;; outcomes of
  152. ;; annihilation. Either the
  153. ;; remaining attackers are 1
  154. ;; (NOTE/TODO: perhaps subject
  155. ;; to rules), or the remaining
  156. ;; defenders are 0. Any other
  157. ;; result is not a result of
  158. ;; annihilation.
  159. (cond
  160. ;; If the defender lost ...
  161. [(= (fight-sit-list-def-rems fight-result-as-list) 0)
  162. (cons (+ (car previous) count)
  163. (cdr previous))]
  164. ;; If the attacker lost ...
  165. [(> (fight-sit-list-def-rems fight-result-as-list) 0)
  166. (cons (car previous)
  167. (+ (cdr previous) count))]
  168. [else
  169. (error
  170. "calc-att-winning-prob given non-annihilation fight results count table")]))
  171. (cons 0 0))])
  172. (/ (car att-wins-def-wins)
  173. (+ (car att-wins-def-wins)
  174. (cdr att-wins-def-wins))))))
  175. (define calc-def-win-prob
  176. (λ (count-table)
  177. "Calculate the winning probability of the defender, given an annihilation
  178. fight result table."
  179. (- 1 (calc-att-winning-prob count-table))))
  180. (define calc-consecutive-att-win-prob
  181. (lambda* (att-counts
  182. def-counts
  183. n-times
  184. rand-int-proc
  185. #:key
  186. (rules *default-risk-rules*))
  187. "Calculate the probability for consecutively winning mutliple fights of the
  188. given counts of attackers against the given counts of defenders. Expects the
  189. number of counts of attackers to be equal to the number of counts of
  190. defenders (pairings)."
  191. (let loop ([rem-att-counts att-counts]
  192. [rem-def-counts def-counts]
  193. [prob 1])
  194. (cond
  195. [(null? rem-def-counts) prob]
  196. [else
  197. (loop
  198. (cdr rem-att-counts)
  199. (cdr rem-def-counts)
  200. (* prob
  201. (calc-att-winning-prob
  202. (try-n-times (make-init-fight-situation (car rem-att-counts)
  203. (car rem-def-counts))
  204. n-times
  205. simulate-fight
  206. rand-int-proc
  207. #:rules rules))))]))))
  208. (define try-n-times-general
  209. (lambda (try-proc n-times)
  210. "Try a try-proc n times and give back counted fight
  211. results. try-proc takes as arguments a fight situation and
  212. the random generator and the rules. Return a hash-table
  213. using fight results as keys and count of those fight results
  214. occurring as values."
  215. (let ([count-table (make-hash-table)])
  216. (let loop ([times-tried 0])
  217. (cond
  218. [(< times-tried n-times)
  219. (let* ([round-result (try-proc)]
  220. [hash-key (fight-situation->hash-key round-result)])
  221. (hash-table-set!
  222. count-table
  223. hash-key
  224. (+ (hash-table-ref count-table hash-key (λ () 0)) 1))
  225. (loop (+ times-tried 1)))]
  226. [else
  227. count-table])))))
  228. (define simulate-battle
  229. (lambda* (att-count
  230. def-counts
  231. rand-int-proc
  232. #:key
  233. (rules *default-risk-rules*)
  234. (verbose #f))
  235. "Simulate a battle exactly once given the attacker count and a list of
  236. defender counts."
  237. (let iter-fights ([available-att-count att-count]
  238. [def-counts def-counts]
  239. [battle-result
  240. (make-init-fight-situation att-count (apply + def-counts))]
  241. [cur-fight-num 0])
  242. (when verbose
  243. (debug "fight number:" cur-fight-num))
  244. (when verbose
  245. (debug "rem. att:" available-att-count))
  246. (cond
  247. ;; If there are no defenders left to defend or no attackers left to
  248. ;; attack, then the battle is over and we need to return a fight
  249. ;; situation (result).
  250. [(null? def-counts)
  251. (when verbose
  252. (debug "STOP: no more defender groups"))
  253. battle-result]
  254. [(< available-att-count 2)
  255. (when verbose
  256. (debug "STOP: insufficient attackers:" available-att-count))
  257. battle-result]
  258. ;; Otherwise ...
  259. [else
  260. (when verbose
  261. (debug "rem. def:" (car def-counts)))
  262. (when verbose
  263. (debug "battle result so far:" battle-result))
  264. (let* ([fight-result
  265. (simulate-fight
  266. (make-init-fight-situation available-att-count (car def-counts))
  267. rand-int-proc
  268. #:rules rules)]
  269. [updated-battle-result
  270. (make-fight-situation
  271. (+ (get-att-wins battle-result)
  272. (get-att-wins fight-result))
  273. ;; NOTE: FUTURE: Future rules may contain a requirement of leaving
  274. ;; more than 1 unit in the source area, which would require a change
  275. ;; of this code.
  276. (get-att-rems fight-result)
  277. (+ (get-def-wins battle-result)
  278. (get-def-wins fight-result))
  279. (+ (get-def-rems fight-result)
  280. (apply + (cdr def-counts))))])
  281. (when verbose
  282. (debug "fight gave following result:" fight-result))
  283. (when verbose
  284. (debug "updated battle result:" updated-battle-result))
  285. ;; Continue with the next battle, if there are defenders or attackers
  286. ;; left to fight.
  287. ;; NOTE: FUTURE: Future rules may contain a requirement of leaving
  288. ;; more than 1 unit in the source area, which would require a change
  289. ;; of this code.
  290. (iter-fights (- (get-att-rems updated-battle-result) 1)
  291. (cdr def-counts)
  292. updated-battle-result
  293. (+ cur-fight-num 1)))]))))
  294. (define calc-battle-att-win-prob
  295. (lambda* (att-count
  296. def-counts
  297. n-times
  298. rand-int-proc
  299. #:key
  300. (rules *default-risk-rules*)
  301. (verbose #f))
  302. "Calculate the probability for consecutively winning mutliple fights of the
  303. given count of attackers against the given counts of defenders."
  304. ;; TODO: remove after removing debug of depth
  305. (define max-depths (length def-counts))
  306. ;; (1) Number of attackers changes at each fight, by at least 1.
  307. ;; (2) At each fight, there is another list of remaining defender
  308. ;; counts.
  309. ;; (3) The initial previous probability is 1, because it is the neutral
  310. ;; element of multiplication.
  311. (define iter
  312. (λ (att-count def-counts prev-prob branch)
  313. ;; TOOD: remove debug
  314. (when verbose
  315. (debug "now at branch" branch "with branch selection probability" (format-fraction prev-prob #:precision 5)))
  316. (cond
  317. ;; If all defenders are gone, return simply the
  318. ;; previous probability, which is the probability
  319. ;; for reaching this fight result from the fight
  320. ;; result before the previous fight situation.
  321. [(null? def-counts)
  322. ;; TOOD: remove debug
  323. (when verbose
  324. (debug "no defenders left, bubbling up to branch" (take branch (- (length branch) 1))))
  325. prev-prob]
  326. [else
  327. ;; TOOD: remove debug
  328. (when (= (- max-depths (length def-counts)) 1)
  329. (when verbose (debug "----------------------------------------")))
  330. (let ([count-table
  331. ;; Calculate fight result probabilities for the current fight
  332. ;; situation.
  333. (try-n-times (make-init-fight-situation att-count (car def-counts))
  334. n-times
  335. simulate-fight
  336. rand-int-proc
  337. #:rules rules)])
  338. ;; Along the branches of a probability tree we multiply. Or in other
  339. ;; words probabilities for events associated with AND need to be
  340. ;; multiplied. The probabilities of battles after the previous fight
  341. ;; depend on that previous fight happening, so they are AND
  342. ;; associated.
  343. (* prev-prob
  344. ;; Look at each result using a fold over the entries of the hash
  345. ;; table of fight results.
  346. (hash-table-filter-fold count-table
  347. ;; Filter for fights won by the attacker.
  348. (λ (key val)
  349. (fight-res-list-att-win? key))
  350. ;; Sum all battles from won fight
  351. ;; results. Use addition as accumulation
  352. ;; procedure.
  353. ;; TOOD: remove debug logs, just use + instead of lambda
  354. (lambda (a b)
  355. (when verbose
  356. (debug "in branch" branch
  357. "accumulating"
  358. (format-fraction a #:precision 7) "+" (format-fraction b #:precision 7) "=" (format-fraction (+ a b) #:precision 7)))
  359. (+ a b))
  360. ;; For each fight result, which
  361. ;; represents a win for the attacker, we
  362. ;; need to calculate the battles after
  363. ;; it.
  364. (λ (key val)
  365. (iter
  366. ;; The attacker can only attack with
  367. ;; the remaining units. 1 attacker
  368. ;; must remain in the conquered area.
  369. ;; NOTE/TODO: If the rules allow empty
  370. ;; source areas, this part needs a
  371. ;; change.
  372. (- (fight-sit-list-att-rems key) 1)
  373. ;; The attacker annihilated one group
  374. ;; of defenders, so only give the
  375. ;; other defender counts for the next
  376. ;; battle calculation.
  377. (cdr def-counts)
  378. ;; Probability of the current fight
  379. ;; result, for which the subsequent
  380. ;; probability of winning the battle
  381. ;; is calculated.
  382. (/ val n-times)
  383. (append branch (list key))))
  384. ;; Add probabilities to 0, as 0 is the
  385. ;; neutral element of addition.
  386. 0)))])))
  387. (iter att-count def-counts 1 '())))
  388. (define try-n-times
  389. (lambda* (fight-situation
  390. n
  391. try-proc
  392. rand-int-proc
  393. #:key
  394. (rules *default-risk-rules*))
  395. "Try a try-proc n times and give back counted fight
  396. results. try-proc takes as arguments a fight situation and
  397. the random generator and the rules. Return a hash-table
  398. using fight results as keys and count of those fight results
  399. occurring as values."
  400. (let ([count-table (make-hash-table)])
  401. (let loop ([times-tried 0])
  402. (cond
  403. [(< times-tried n)
  404. (let* ([round-result
  405. (try-proc fight-situation rand-int-proc #:rules rules)]
  406. [hash-key (fight-situation->hash-key round-result)])
  407. (hash-table-set!
  408. count-table
  409. hash-key
  410. (+ (hash-table-ref count-table hash-key (λ () 0)) 1))
  411. (loop (+ times-tried 1)))]
  412. [else
  413. count-table])))))
  414. (define simulate-fight
  415. (lambda* (fight-situation
  416. rand-int-proc
  417. #:key
  418. (rules *default-risk-rules*))
  419. "Calculate a fight result for rolling dice as long as
  420. possible for either side, always rolling the maximum number
  421. of dice possible."
  422. (let loop ([prev-round-result
  423. (make-init-fight-situation (get-att-rems fight-situation)
  424. (get-def-rems fight-situation))])
  425. (let ([att-rems (get-att-rems prev-round-result)]
  426. [def-rems (get-def-rems prev-round-result)])
  427. (cond
  428. ;; The attacker must have at least 1 unit remaining, so that the source
  429. ;; area remains occupied. The defender has no such need, as they can lose
  430. ;; the target area.
  431. ;; TODO: This could be subject to rule changes.
  432. [(or (= att-rems 1) (= def-rems 0))
  433. prev-round-result]
  434. [(and (> att-rems 1) (> def-rems 0))
  435. (loop
  436. (simulate-round prev-round-result
  437. rand-int-proc
  438. #:rules rules))]
  439. [else
  440. (error "illegal situation - your code is buggy")])))))