package tiny_libs
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
From-scratch libraries for teaching: graphics, audio, compression, crypto, networking and more
Install
dune-project
Dependency
Authors
Maintainers
Sources
0.3.6.tar.gz
md5=7c636383d146d30ac6f2fa234a6253c8
sha512=c79f3823c5f8f57e5038eb640d487c61168b84aa07c61999d6622ef9fd0c890e2b03b4c6a7cdbbe9352a49e25dda00ac7bb14693cee8e3d7beeed251351a2af0
doc/src/tiny_libs.ai_search/Mcts.ml.html
Source file Mcts.ml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180(* Claude Code * * Copyright (C) 2026 Yoann Padioleau * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public License * (LGPL) as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. *) (* See Mcts.mli *) type 'move result = { best : 'move option; tried : ('move * int * float) list; playouts : int; nodes : int } (* a node of the tree: the move that led to it, what the playouts * through it came to (always counted for MAX: [wins] is MAX's share), * the children tried so far, and the moves not tried yet *) type 'move node = { move : 'move option; mutable visits : int; mutable wins : float; (* MAX's, between 0 and [visits] *) mutable children : 'move node list; mutable untried : 'move list; prior : float; (* what a policy thought of this move, 1 with none *) } let node ?(prior = 1.) (move : 'move option) (moves : 'move list) : 'move node = { move; visits = 0; wins = 0.; children = []; untried = moves; prior } (* a finished game, as MAX's share: 1 won, 0 lost, 1/2 drawn *) let outcome (game : ('state, 'move) Minimax.game) (state : 'state) : float = let s = game.score state in if s > 0. then 1. else if s < 0. then 0. else 0.5 (* the default playout: uniformly random moves to the end *) let random_playout (st : Lehmer.state) (game : ('state, 'move) Minimax.game) (state : 'state) : 'state = let rec go state = match game.moves state with | [] -> state | moves -> go (game.play state (List.nth moves (Lehmer.int st (List.length moves)))) in go state (* UCB1, from the point of view of whoever is to play at [parent]: what * has been working for *them*, plus what has barely been tried. * * With a policy ([policy = true]) this is PUCT instead: the second * term is weighted by what the policy thought of the move, and an * unvisited child is no longer infinitely attractive but as * attractive as the policy says *) let ucb ~(policy : bool) (exploration : float) (maximizing : bool) (parent_visits : int) (c : 'move node) : float = if policy then let = if c.visits = 0 then 0.5 else c.wins /. float_of_int c.visits in let mine = if maximizing then share else 1. -. share in mine +. (exploration *. c.prior *. sqrt (float_of_int parent_visits) /. (1. +. float_of_int c.visits)) else if c.visits = 0 then Float.infinity else let = c.wins /. float_of_int c.visits in let mine = if maximizing then share else 1. -. share in mine +. (exploration *. sqrt (log (float_of_int parent_visits) /. float_of_int c.visits)) type ('state, 'move) thinking = { game : ('state, 'move) Minimax.game; root_state : 'state; root : 'move node; exploration : float; playout : Lehmer.state -> ('state, 'move) Minimax.game -> 'state -> 'state; (* AlphaGo's two: what the moves are worth before trying them, and * what a position is worth without playing it out *) prior : ('state -> ('move * float) list) option; evaluate : ('state -> float) option; st : Lehmer.state; mutable played : int; mutable nodes : int; } let start ?exploration ?(seed = 0) ?(playout = random_playout) ?prior ?evaluate (game : ('state, 'move) Minimax.game) (state : 'state) : ('state, 'move) thinking = { game; root_state = state; (* PUCT wants a larger constant than UCB1: its exploring term * falls off as 1/(1+N) rather than sqrt(log N / N) *) exploration = (match exploration with Some e -> e | None -> if prior = None then sqrt 2. else 1.5); root = node None (game.moves state); playout; prior; evaluate; st = Lehmer.make seed; played = 0; nodes = 1; } (* one iteration: select, expand, simulate, backup. The path taken is * returned so the result can be added to every node on it *) let rec descend (t : ('state, 'move) thinking) (n : 'move node) (state : 'state) (path : 'move node list) : 'move node list * 'state = if n.untried <> [] then ( (* expand: one of the moves never tried here *) let i = Lehmer.int t.st (List.length n.untried) in let move = List.nth n.untried i in n.untried <- List.filteri (fun j _ -> j <> i) n.untried; let child_state = t.game.play state move in let prior = match t.prior with | None -> 1. | Some policy -> ( match List.assoc_opt move (policy state) with Some p -> p | None -> 0.01) in let child = node ~prior (Some move) (t.game.moves child_state) in n.children <- child :: n.children; t.nodes <- t.nodes + 1; (child :: path, child_state)) else match n.children with (* the game is over here: nothing to expand, nothing to choose *) | [] -> (path, state) | children -> let maximizing = t.game.max_to_play state in let policy = t.prior <> None in let score c = ucb ~policy t.exploration maximizing n.visits c in let best = List.fold_left (fun best c -> if score c > score best then c else best) (List.hd children) children in descend t best (t.game.play state (Option.get best.move)) (best :: path) let iterate (t : ('state, 'move) thinking) : unit = let (path, state) = descend t t.root t.root_state [ t.root ] in let = match t.evaluate with (* a value head replaces the playout: one opinion instead of one * random game -- except at a position that is already over, where * the rules know better than any network *) | Some value when t.game.moves state <> [] -> Float.max 0. (Float.min 1. (value state)) | _ -> outcome t.game (t.playout t.st t.game state) in List.iter (fun n -> n.visits <- n.visits + 1; n.wins <- n.wins +. share) path; t.played <- t.played + 1 let plan (t : ('state, 'move) thinking) : 'move result = let tried = List.map (fun (c : 'move node) -> (Option.get c.move, c.visits, if c.visits = 0 then 0. else c.wins /. float_of_int c.visits)) t.root.children in (* MCTS's answer is the most visited child, not the best scoring one: * a high score on three playouts means nothing, and the selection * rule has already spent its visits on what it believes. * * Ties go to the better share, from the point of view of whoever is * to play. Early on every child has been visited once and the * visits decide nothing at all -- without this the answer would be * whichever move happened to be first in the list, which is how a * search with a *perfect* evaluation was seen to give a losing move * at twelve playouts (Unit_mcts). *) let maximizing = t.game.max_to_play t.root_state in let mine = if maximizing then share else 1. -. share in let best = List.fold_left (fun best (m, visits, ) -> match best with | Some (_, v, s) when v > visits || (v = visits && mine s >= mine share) -> best | _ -> Some (m, visits, share)) None tried in let best = Option.map (fun (m, v, _) -> (m, v)) best in { best = Option.map fst best; tried; playouts = t.played; nodes = t.nodes } let think ~(playouts : int) (t : ('state, 'move) thinking) : ('state, 'move) thinking = for _ = 1 to playouts do iterate t done; t let search ?exploration ?seed ?playout ?prior ?evaluate (game : ('state, 'move) Minimax.game) ~(playouts : int) (state : 'state) : 'move result = plan (think ~playouts (start ?exploration ?seed ?playout ?prior ?evaluate game state))
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>