@woosh/meep-engine 2.134.4 → 2.135.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/bundle-worker-image-decoder.js +1 -1
- package/build/bundle-worker-terrain.js +1 -1
- package/editor/tools/v2/TransformControlsGizmo.js +1 -1
- package/editor/view/node-graph/NodeGraphEditorView.js +2 -2
- package/package.json +1 -1
- package/src/core/assert.d.ts +0 -2
- package/src/core/assert.d.ts.map +1 -1
- package/src/core/assert.js +0 -6
- package/src/core/color/Color.d.ts +0 -5
- package/src/core/color/Color.d.ts.map +1 -1
- package/src/core/color/Color.js +1 -7
- package/src/core/geom/2d/hash-grid/SpatialHashGrid.js +386 -386
- package/src/core/geom/2d/line/line_segment_compute_line_segment_intersection_2d.js +1 -1
- package/src/core/geom/2d/quad-tree-binary/QuadTree.js +714 -714
- package/src/core/geom/3d/triangle/computeTriangleRayIntersection.js +160 -160
- package/src/core/geom/3d/triangle/computeTriangleRayIntersectionBarycentric.js +96 -96
- package/src/core/geom/packing/max-rect/MaxRectanglesPacker.js +1 -1
- package/src/core/geom/packing/max-rect/findBestContainer.js +4 -4
- package/src/core/geom/packing/max-rect/packOneBox.js +2 -2
- package/src/core/geom/vec3/v3_rigid_align_paired_unit_vectors.d.ts +23 -0
- package/src/core/geom/vec3/v3_rigid_align_paired_unit_vectors.d.ts.map +1 -0
- package/src/core/geom/vec3/v3_rigid_align_paired_unit_vectors.js +96 -0
- package/src/core/graph/layout/box/BoxLayouter.js +7 -7
- package/src/core/graph/layout/box/position_box_next_to_box.js +6 -6
- package/src/core/math/computeWholeDivisorLow.js +33 -33
- package/src/core/math/linalg/eigen/eigen_values_find_spectral_gap.d.ts.map +1 -0
- package/src/core/math/linalg/eigen/matrix_eigenvalues_in_place.d.ts +10 -0
- package/src/core/math/linalg/eigen/matrix_eigenvalues_in_place.d.ts.map +1 -0
- package/src/core/{graph → math/linalg}/eigen/matrix_eigenvalues_in_place.js +8 -7
- package/src/core/math/linalg/eigen/matrix_householder_in_place.d.ts.map +1 -0
- package/src/core/{graph → math/linalg}/eigen/matrix_householder_in_place.js +11 -5
- package/src/core/math/linalg/eigen/matrix_qr_in_place.d.ts +15 -0
- package/src/core/math/linalg/eigen/matrix_qr_in_place.d.ts.map +1 -0
- package/src/core/{graph → math/linalg}/eigen/matrix_qr_in_place.js +8 -2
- package/src/core/math/linalg/eigen/matrix_top_eigenvector_power_iteration.d.ts +17 -0
- package/src/core/math/linalg/eigen/matrix_top_eigenvector_power_iteration.d.ts.map +1 -0
- package/src/core/math/linalg/eigen/matrix_top_eigenvector_power_iteration.js +107 -0
- package/src/core/math/linalg/polynomial_complex_roots_aberth_ehrlich.d.ts +19 -0
- package/src/core/math/linalg/polynomial_complex_roots_aberth_ehrlich.d.ts.map +1 -0
- package/src/core/math/linalg/polynomial_complex_roots_aberth_ehrlich.js +161 -0
- package/src/core/math/linalg/polynomial_real_roots_in_interval.d.ts +15 -0
- package/src/core/math/linalg/polynomial_real_roots_in_interval.d.ts.map +1 -0
- package/src/core/math/linalg/polynomial_real_roots_in_interval.js +200 -0
- package/src/core/math/solveCubic.d.ts +15 -0
- package/src/core/math/solveCubic.d.ts.map +1 -0
- package/src/core/math/solveCubic.js +82 -0
- package/src/core/math/spline/spline3_hermite_bounds_t.d.ts +23 -0
- package/src/core/math/spline/spline3_hermite_bounds_t.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_bounds_t.js +109 -0
- package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite.d.ts +25 -0
- package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite.js +44 -0
- package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_1d.d.ts +16 -0
- package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_1d.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_1d.js +120 -0
- package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_2d.d.ts +11 -0
- package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_2d.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_2d.js +451 -0
- package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_nd.d.ts +12 -0
- package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_nd.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_nd.js +339 -0
- package/src/core/math/spline/spline3_hermite_intersects_spline3_hermite.d.ts +15 -0
- package/src/core/math/spline/spline3_hermite_intersects_spline3_hermite.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_intersects_spline3_hermite.js +21 -0
- package/src/core/math/spline/spline3_hermite_to_monomial.d.ts +24 -0
- package/src/core/math/spline/spline3_hermite_to_monomial.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_to_monomial.js +37 -0
- package/src/core/math/spline/v3_computeCatmullRomSplineUniformDistance.js +1 -1
- package/src/core/model/node-graph/visual/NodeGraphVisualData.js +1 -1
- package/src/core/model/reactive/model/util/createRandomReactiveExpression.js +185 -185
- package/src/core/process/delay.js +16 -16
- package/src/engine/animation/async/TimeSeries.js +300 -300
- package/src/engine/animation/curve/AnimationCurve.d.ts +3 -2
- package/src/engine/animation/curve/AnimationCurve.d.ts.map +1 -1
- package/src/engine/animation/curve/AnimationCurve.js +3 -2
- package/src/engine/animation/curve/draw/position_canvas_to_curve.js +2 -2
- package/src/engine/animation/curve/draw/position_curve_to_canvas.js +2 -2
- package/src/engine/ecs/fow/shader/FogOfWarRenderer.js +145 -145
- package/src/engine/ecs/gui/position/ViewportPositionSystem.js +2 -2
- package/src/engine/ecs/parent/entity_node_compute_bounding_box.js +1 -1
- package/src/engine/ecs/transform/Transform.d.ts +0 -10
- package/src/engine/ecs/transform/Transform.d.ts.map +1 -1
- package/src/engine/ecs/transform/Transform.js +0 -12
- package/src/engine/graphics/composit/CompositLayer.js +254 -254
- package/src/engine/graphics/ecs/mesh-v2/sg_hierarchy_compute_bounding_box_via_parent_entity.js +1 -1
- package/src/engine/graphics/ecs/path/tube/build/build_geometry_linear.js +2 -2
- package/src/engine/graphics/material/optimization/MaterialOptimizationContext.js +3 -3
- package/src/engine/graphics/particles/particular/engine/utils/volume/AttributeValue.js +201 -201
- package/src/engine/graphics/particles/particular/engine/utils/volume/prototypeParticleVolume.js +1 -1
- package/src/engine/graphics/render/buffer/slot/parameter/ProgramValueSlotParameterSet.js +2 -2
- package/src/engine/graphics/render/forward_plus/LightManager.js +1226 -1226
- package/src/engine/graphics/render/forward_plus/model/PointLight.js +1 -1
- package/src/engine/graphics/sh3/lpv/lpv_obtain_storage_cached_volume.js +1 -1
- package/src/engine/graphics/sh3/path_tracer/texture/sample_material.js +2 -2
- package/src/engine/graphics/texture/atlas/TextureAtlasDebugger.js +1 -1
- package/src/engine/graphics/texture/sampler/HarmonicDiffusionGrid.js +145 -145
- package/src/engine/graphics/texture/sampler/serialization/TextureBinaryBufferSerializer.js +2 -2
- package/src/engine/intelligence/behavior/ecs/BehaviorComponent.d.ts +2 -6
- package/src/engine/intelligence/behavior/ecs/BehaviorComponent.d.ts.map +1 -1
- package/src/engine/intelligence/behavior/ecs/BehaviorComponent.js +0 -10
- package/src/engine/intelligence/mcts/MonteCarlo.js +275 -275
- package/src/engine/navigation/ecs/path_following/PathFollower.js +222 -222
- package/src/generation/grid/GridData.js +220 -220
- package/src/generation/grid/generation/GridTaskDensityMarkerDistribution.js +385 -385
- package/src/view/elements/image/SvgImageView.js +1 -1
- package/src/view/elements/windrose/WindRoseDiagram.js +369 -369
- package/src/view/minimap/gl/MinimapFogOfWar.js +3 -3
- package/src/view/util/DomSizeObserver.js +1 -1
- package/src/core/binary/clz32.d.ts +0 -6
- package/src/core/binary/clz32.d.ts.map +0 -1
- package/src/core/binary/clz32.js +0 -5
- package/src/core/binary/type/dataTypeFromTypedArray.d.ts +0 -8
- package/src/core/binary/type/dataTypeFromTypedArray.d.ts.map +0 -1
- package/src/core/binary/type/dataTypeFromTypedArray.js +0 -11
- package/src/core/collection/array/computeHashIntegerArray.d.ts +0 -1
- package/src/core/collection/array/computeHashIntegerArray.d.ts.map +0 -1
- package/src/core/collection/array/computeHashIntegerArray.js +0 -7
- package/src/core/collection/array/typed/typedArrayToDataType.d.ts +0 -6
- package/src/core/collection/array/typed/typedArrayToDataType.d.ts.map +0 -1
- package/src/core/collection/array/typed/typedArrayToDataType.js +0 -6
- package/src/core/geom/3d/mat4/MATRIX_4_IDENTITY.d.ts +0 -6
- package/src/core/geom/3d/mat4/MATRIX_4_IDENTITY.d.ts.map +0 -1
- package/src/core/geom/3d/mat4/MATRIX_4_IDENTITY.js +0 -7
- package/src/core/graph/eigen/eigen_values_find_spectral_gap.d.ts.map +0 -1
- package/src/core/graph/eigen/matrix_eigenvalues_in_place.d.ts +0 -8
- package/src/core/graph/eigen/matrix_eigenvalues_in_place.d.ts.map +0 -1
- package/src/core/graph/eigen/matrix_householder_in_place.d.ts.map +0 -1
- package/src/core/graph/eigen/matrix_qr_in_place.d.ts +0 -9
- package/src/core/graph/eigen/matrix_qr_in_place.d.ts.map +0 -1
- package/src/core/math/spline/cubicCurve.d.ts +0 -6
- package/src/core/math/spline/cubicCurve.d.ts.map +0 -1
- package/src/core/math/spline/cubicCurve.js +0 -6
- package/src/core/math/spline/spline_bezier2.d.ts +0 -6
- package/src/core/math/spline/spline_bezier2.d.ts.map +0 -1
- package/src/core/math/spline/spline_bezier2.js +0 -6
- package/src/core/math/spline/spline_bezier3.d.ts +0 -6
- package/src/core/math/spline/spline_bezier3.d.ts.map +0 -1
- package/src/core/math/spline/spline_bezier3.js +0 -6
- package/src/core/math/spline/spline_bezier3_bounds.d.ts +0 -6
- package/src/core/math/spline/spline_bezier3_bounds.d.ts.map +0 -1
- package/src/core/math/spline/spline_bezier3_bounds.js +0 -6
- package/src/core/math/spline/spline_hermite3.d.ts +0 -6
- package/src/core/math/spline/spline_hermite3.d.ts.map +0 -1
- package/src/core/math/spline/spline_hermite3.js +0 -6
- package/src/core/math/spline/spline_hermite3_bounds.d.ts +0 -6
- package/src/core/math/spline/spline_hermite3_bounds.d.ts.map +0 -1
- package/src/core/math/spline/spline_hermite3_bounds.js +0 -6
- package/src/core/math/spline/spline_hermite3_to_bezier.d.ts +0 -2
- package/src/core/math/spline/spline_hermite3_to_bezier.d.ts.map +0 -1
- package/src/core/math/spline/spline_hermite3_to_bezier.js +0 -6
- package/src/engine/intelligence/behavior/decorator/RepeatUntilFailureBehavior.d.ts +0 -37
- package/src/engine/intelligence/behavior/decorator/RepeatUntilFailureBehavior.d.ts.map +0 -1
- package/src/engine/intelligence/behavior/decorator/RepeatUntilFailureBehavior.js +0 -70
- /package/src/core/{graph → math/linalg}/eigen/eigen_values_find_spectral_gap.d.ts +0 -0
- /package/src/core/{graph → math/linalg}/eigen/eigen_values_find_spectral_gap.js +0 -0
- /package/src/core/{graph → math/linalg}/eigen/matrix_householder_in_place.d.ts +0 -0
|
@@ -1,275 +1,275 @@
|
|
|
1
|
-
import { assert } from "../../../core/assert.js";
|
|
2
|
-
import { returnZero } from "../../../core/function/returnZero.js";
|
|
3
|
-
import { mix } from "../../../core/math/mix.js";
|
|
4
|
-
import { seededRandom } from "../../../core/math/random/seededRandom.js";
|
|
5
|
-
import { MoveEdge } from "./MoveEdge.js";
|
|
6
|
-
import { StateNode, StateType } from "./StateNode.js";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* From: A Survey of Monte Carlo Tree Search Methods
|
|
10
|
-
* The value Cp = 1/√2 was shown by Kocsis and Szepesvari [120] to satisfy the Hoeffding ineqality with rewards in the range [0, 1]
|
|
11
|
-
* @type {number}
|
|
12
|
-
*/
|
|
13
|
-
const C_ks = 1 / Math.sqrt(2);
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
17
|
-
* @param {StateNode} parent
|
|
18
|
-
* @param {StateNode} child
|
|
19
|
-
* @returns {number}
|
|
20
|
-
*/
|
|
21
|
-
function computeNodeSelectionScore(parent, child) {
|
|
22
|
-
const playouts = child.playouts;
|
|
23
|
-
|
|
24
|
-
if (playouts === 0) {
|
|
25
|
-
// child has 0 playouts, this can only happen if it ended up with no moves, avoid division by 0
|
|
26
|
-
return 0;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Exploitation heuristic
|
|
30
|
-
const Q = mix((child.wins + 1) / playouts, child.heuristicValue, 0.65);
|
|
31
|
-
|
|
32
|
-
// Based on UCB1
|
|
33
|
-
// exploration heuristic
|
|
34
|
-
const u = Math.sqrt((2 * Math.log(parent.playouts)) / playouts);
|
|
35
|
-
|
|
36
|
-
return Q + C_ks * u;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @template S
|
|
41
|
-
* @author Alex Goldring
|
|
42
|
-
* @copyright Company Named Limited (c) 2025
|
|
43
|
-
*/
|
|
44
|
-
export class MonteCarloTreeSearch {
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
*
|
|
48
|
-
* @type {S}
|
|
49
|
-
*/
|
|
50
|
-
rootState = null;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
*
|
|
54
|
-
* @type {StateNode|null}
|
|
55
|
-
*/
|
|
56
|
-
root = null;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
*
|
|
60
|
-
* @type {function(state:S, source:StateNode):MoveEdge[]}
|
|
61
|
-
*/
|
|
62
|
-
computeValidMoves = null;
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
*
|
|
66
|
-
* @type {function(state:S):StateType}
|
|
67
|
-
*/
|
|
68
|
-
computeTerminalFlag = null;
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Depth to which plays will be explored
|
|
72
|
-
* @type {number}
|
|
73
|
-
*/
|
|
74
|
-
maxExplorationDepth = 1000;
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
*
|
|
78
|
-
* @type {Function}
|
|
79
|
-
*/
|
|
80
|
-
random = seededRandom(0);
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* @param {S} rootState
|
|
84
|
-
* @param {function(state:S, source:StateNode):MoveEdge[]} computeValidMoves
|
|
85
|
-
* @param {function(state:S):StateType} computeTerminalFlag
|
|
86
|
-
* @param {function(S):S} cloneState
|
|
87
|
-
* @param {function(StateNode, S):number} heuristic Estimation function for evaluation of intermediate stated, guides exploration
|
|
88
|
-
*/
|
|
89
|
-
initialize(
|
|
90
|
-
{
|
|
91
|
-
rootState,
|
|
92
|
-
computeValidMoves,
|
|
93
|
-
computeTerminalFlag,
|
|
94
|
-
cloneState,
|
|
95
|
-
heuristic = returnZero
|
|
96
|
-
}
|
|
97
|
-
) {
|
|
98
|
-
assert.isFunction(computeValidMoves, `computeValidMoves`);
|
|
99
|
-
assert.isFunction(computeTerminalFlag, `computeTerminalFlag`);
|
|
100
|
-
assert.isFunction(cloneState, `cloneState`);
|
|
101
|
-
|
|
102
|
-
this.computeValidMoves = computeValidMoves;
|
|
103
|
-
this.computeTerminalFlag = computeTerminalFlag;
|
|
104
|
-
this.cloneState = cloneState;
|
|
105
|
-
this.heuristic = heuristic;
|
|
106
|
-
|
|
107
|
-
this.rootState = rootState;
|
|
108
|
-
|
|
109
|
-
this.root = new StateNode();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
*
|
|
114
|
-
* @param {StateNode} node
|
|
115
|
-
* @param {S} state
|
|
116
|
-
* @returns {StateNode}
|
|
117
|
-
*/
|
|
118
|
-
selectRandom(node, state) {
|
|
119
|
-
let score;
|
|
120
|
-
|
|
121
|
-
let i, bestScore, bestMove;
|
|
122
|
-
|
|
123
|
-
const random = this.random;
|
|
124
|
-
|
|
125
|
-
while (
|
|
126
|
-
node.isExpanded() &&
|
|
127
|
-
node.moves.length > 0 &&
|
|
128
|
-
!node.isTerminal()
|
|
129
|
-
) {
|
|
130
|
-
|
|
131
|
-
bestScore = Number.NEGATIVE_INFINITY;
|
|
132
|
-
bestMove = null;
|
|
133
|
-
|
|
134
|
-
const moves = node.moves;
|
|
135
|
-
|
|
136
|
-
const numMoves = moves.length;
|
|
137
|
-
|
|
138
|
-
assert.notEqual(numMoves, 0, 'number of moves is 0, this is invalid state');
|
|
139
|
-
|
|
140
|
-
for (i = 0; i < numMoves; i++) {
|
|
141
|
-
|
|
142
|
-
const move = moves[i];
|
|
143
|
-
|
|
144
|
-
const randomRoll = random();
|
|
145
|
-
|
|
146
|
-
if (move.isTargetMaterialized()) {
|
|
147
|
-
|
|
148
|
-
const child = move.target;
|
|
149
|
-
|
|
150
|
-
const s = computeNodeSelectionScore(node, child);
|
|
151
|
-
|
|
152
|
-
assert.notNaN(s, 'computed Node score');
|
|
153
|
-
assert.
|
|
154
|
-
|
|
155
|
-
score = s + randomRoll;
|
|
156
|
-
} else {
|
|
157
|
-
|
|
158
|
-
//use a constant value for unexplored nodes
|
|
159
|
-
score = randomRoll * 100;
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (score > bestScore) {
|
|
165
|
-
bestScore = score;
|
|
166
|
-
bestMove = move;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (!bestMove.isTargetMaterialized()) {
|
|
172
|
-
//materialize the target state
|
|
173
|
-
materializedEdgeTarget(state, node, bestMove, this.computeTerminalFlag, this.heuristic);
|
|
174
|
-
|
|
175
|
-
} else {
|
|
176
|
-
//just follow the edge
|
|
177
|
-
bestMove.move(state);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
node = bestMove.target;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return node;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Perform a playout from the root node
|
|
189
|
-
* @returns {S} final state of the playout
|
|
190
|
-
*/
|
|
191
|
-
playout() {
|
|
192
|
-
const computeValidMoves = this.computeValidMoves;
|
|
193
|
-
|
|
194
|
-
const computeTerminalFlag = this.computeTerminalFlag;
|
|
195
|
-
|
|
196
|
-
const state = this.cloneState(this.rootState);
|
|
197
|
-
|
|
198
|
-
assert.notEqual(state, this.rootState, 'cloneState must produce a new state object, instead it produced the same one');
|
|
199
|
-
|
|
200
|
-
let node = this.root;
|
|
201
|
-
|
|
202
|
-
while (!node.isTerminal() && node.depth < this.maxExplorationDepth) {
|
|
203
|
-
|
|
204
|
-
if (!node.isExpanded()) {
|
|
205
|
-
node.expand(state, computeValidMoves, computeTerminalFlag);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const child = this.selectRandom(node, state);
|
|
209
|
-
|
|
210
|
-
if (child === node) {
|
|
211
|
-
// prevent infinite recursion
|
|
212
|
-
// this should not happen?
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
node = child;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (!node.isTerminal() && node.depth >= this.maxExplorationDepth) {
|
|
220
|
-
//cap the state by depth, propagate heuristic score
|
|
221
|
-
node.type = StateType.DepthCapped;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// record play-through
|
|
225
|
-
const terminalFlag = node.type;
|
|
226
|
-
|
|
227
|
-
if (terminalFlag === StateType.Win) {
|
|
228
|
-
node.addPlayouts(1, 1, 0);
|
|
229
|
-
} else if (terminalFlag === StateType.Loss) {
|
|
230
|
-
node.addPlayouts(1, 0, 1);
|
|
231
|
-
} else if (terminalFlag === StateType.Tie || terminalFlag === StateType.DepthCapped) {
|
|
232
|
-
node.addPlayouts(1, 0, 0);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return state;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* @template S
|
|
241
|
-
* @param {S} state
|
|
242
|
-
* @param {StateNode} source
|
|
243
|
-
* @param {MoveEdge} edge
|
|
244
|
-
* @param {function(S):StateType} computeTerminalFlag
|
|
245
|
-
* @param {function(StateNode, S)} heuristic
|
|
246
|
-
*/
|
|
247
|
-
function materializedEdgeTarget(state, source, edge, computeTerminalFlag, heuristic) {
|
|
248
|
-
|
|
249
|
-
const child = new StateNode();
|
|
250
|
-
child.parent = source;
|
|
251
|
-
child.depth = source.depth + 1;
|
|
252
|
-
|
|
253
|
-
const computedState = edge.move(state);
|
|
254
|
-
|
|
255
|
-
const terminalFlag = computeTerminalFlag(computedState);
|
|
256
|
-
|
|
257
|
-
assert.enum(terminalFlag, StateType, 'terminalFlag');
|
|
258
|
-
|
|
259
|
-
child.type = terminalFlag;
|
|
260
|
-
|
|
261
|
-
edge.target = child;
|
|
262
|
-
|
|
263
|
-
const childHeuristicScore = heuristic(child, computedState);
|
|
264
|
-
|
|
265
|
-
assert.notNaN(childHeuristicScore, 'childHeuristicScore');
|
|
266
|
-
|
|
267
|
-
child.heuristicValue = childHeuristicScore;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
// bubble the heuristic score up the tree
|
|
271
|
-
// child.bubbleUpHeuristicScore(); // heuristic value changes sign depending on the team making the move, so aggregation becomes tricky
|
|
272
|
-
|
|
273
|
-
return computedState;
|
|
274
|
-
}
|
|
275
|
-
|
|
1
|
+
import { assert } from "../../../core/assert.js";
|
|
2
|
+
import { returnZero } from "../../../core/function/returnZero.js";
|
|
3
|
+
import { mix } from "../../../core/math/mix.js";
|
|
4
|
+
import { seededRandom } from "../../../core/math/random/seededRandom.js";
|
|
5
|
+
import { MoveEdge } from "./MoveEdge.js";
|
|
6
|
+
import { StateNode, StateType } from "./StateNode.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* From: A Survey of Monte Carlo Tree Search Methods
|
|
10
|
+
* The value Cp = 1/√2 was shown by Kocsis and Szepesvari [120] to satisfy the Hoeffding ineqality with rewards in the range [0, 1]
|
|
11
|
+
* @type {number}
|
|
12
|
+
*/
|
|
13
|
+
const C_ks = 1 / Math.sqrt(2);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
*
|
|
17
|
+
* @param {StateNode} parent
|
|
18
|
+
* @param {StateNode} child
|
|
19
|
+
* @returns {number}
|
|
20
|
+
*/
|
|
21
|
+
function computeNodeSelectionScore(parent, child) {
|
|
22
|
+
const playouts = child.playouts;
|
|
23
|
+
|
|
24
|
+
if (playouts === 0) {
|
|
25
|
+
// child has 0 playouts, this can only happen if it ended up with no moves, avoid division by 0
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Exploitation heuristic
|
|
30
|
+
const Q = mix((child.wins + 1) / playouts, child.heuristicValue, 0.65);
|
|
31
|
+
|
|
32
|
+
// Based on UCB1
|
|
33
|
+
// exploration heuristic
|
|
34
|
+
const u = Math.sqrt((2 * Math.log(parent.playouts)) / playouts);
|
|
35
|
+
|
|
36
|
+
return Q + C_ks * u;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @template S
|
|
41
|
+
* @author Alex Goldring
|
|
42
|
+
* @copyright Company Named Limited (c) 2025
|
|
43
|
+
*/
|
|
44
|
+
export class MonteCarloTreeSearch {
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
*
|
|
48
|
+
* @type {S}
|
|
49
|
+
*/
|
|
50
|
+
rootState = null;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
*
|
|
54
|
+
* @type {StateNode|null}
|
|
55
|
+
*/
|
|
56
|
+
root = null;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
*
|
|
60
|
+
* @type {function(state:S, source:StateNode):MoveEdge[]}
|
|
61
|
+
*/
|
|
62
|
+
computeValidMoves = null;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
*
|
|
66
|
+
* @type {function(state:S):StateType}
|
|
67
|
+
*/
|
|
68
|
+
computeTerminalFlag = null;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Depth to which plays will be explored
|
|
72
|
+
* @type {number}
|
|
73
|
+
*/
|
|
74
|
+
maxExplorationDepth = 1000;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
*
|
|
78
|
+
* @type {Function}
|
|
79
|
+
*/
|
|
80
|
+
random = seededRandom(0);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {S} rootState
|
|
84
|
+
* @param {function(state:S, source:StateNode):MoveEdge[]} computeValidMoves
|
|
85
|
+
* @param {function(state:S):StateType} computeTerminalFlag
|
|
86
|
+
* @param {function(S):S} cloneState
|
|
87
|
+
* @param {function(StateNode, S):number} heuristic Estimation function for evaluation of intermediate stated, guides exploration
|
|
88
|
+
*/
|
|
89
|
+
initialize(
|
|
90
|
+
{
|
|
91
|
+
rootState,
|
|
92
|
+
computeValidMoves,
|
|
93
|
+
computeTerminalFlag,
|
|
94
|
+
cloneState,
|
|
95
|
+
heuristic = returnZero
|
|
96
|
+
}
|
|
97
|
+
) {
|
|
98
|
+
assert.isFunction(computeValidMoves, `computeValidMoves`);
|
|
99
|
+
assert.isFunction(computeTerminalFlag, `computeTerminalFlag`);
|
|
100
|
+
assert.isFunction(cloneState, `cloneState`);
|
|
101
|
+
|
|
102
|
+
this.computeValidMoves = computeValidMoves;
|
|
103
|
+
this.computeTerminalFlag = computeTerminalFlag;
|
|
104
|
+
this.cloneState = cloneState;
|
|
105
|
+
this.heuristic = heuristic;
|
|
106
|
+
|
|
107
|
+
this.rootState = rootState;
|
|
108
|
+
|
|
109
|
+
this.root = new StateNode();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
*
|
|
114
|
+
* @param {StateNode} node
|
|
115
|
+
* @param {S} state
|
|
116
|
+
* @returns {StateNode}
|
|
117
|
+
*/
|
|
118
|
+
selectRandom(node, state) {
|
|
119
|
+
let score;
|
|
120
|
+
|
|
121
|
+
let i, bestScore, bestMove;
|
|
122
|
+
|
|
123
|
+
const random = this.random;
|
|
124
|
+
|
|
125
|
+
while (
|
|
126
|
+
node.isExpanded() &&
|
|
127
|
+
node.moves.length > 0 &&
|
|
128
|
+
!node.isTerminal()
|
|
129
|
+
) {
|
|
130
|
+
|
|
131
|
+
bestScore = Number.NEGATIVE_INFINITY;
|
|
132
|
+
bestMove = null;
|
|
133
|
+
|
|
134
|
+
const moves = node.moves;
|
|
135
|
+
|
|
136
|
+
const numMoves = moves.length;
|
|
137
|
+
|
|
138
|
+
assert.notEqual(numMoves, 0, 'number of moves is 0, this is invalid state');
|
|
139
|
+
|
|
140
|
+
for (i = 0; i < numMoves; i++) {
|
|
141
|
+
|
|
142
|
+
const move = moves[i];
|
|
143
|
+
|
|
144
|
+
const randomRoll = random();
|
|
145
|
+
|
|
146
|
+
if (move.isTargetMaterialized()) {
|
|
147
|
+
|
|
148
|
+
const child = move.target;
|
|
149
|
+
|
|
150
|
+
const s = computeNodeSelectionScore(node, child);
|
|
151
|
+
|
|
152
|
+
assert.notNaN(s, 'computed Node score');
|
|
153
|
+
assert.isFinite(s, `computed Node score`);
|
|
154
|
+
|
|
155
|
+
score = s + randomRoll;
|
|
156
|
+
} else {
|
|
157
|
+
|
|
158
|
+
//use a constant value for unexplored nodes
|
|
159
|
+
score = randomRoll * 100;
|
|
160
|
+
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if (score > bestScore) {
|
|
165
|
+
bestScore = score;
|
|
166
|
+
bestMove = move;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!bestMove.isTargetMaterialized()) {
|
|
172
|
+
//materialize the target state
|
|
173
|
+
materializedEdgeTarget(state, node, bestMove, this.computeTerminalFlag, this.heuristic);
|
|
174
|
+
|
|
175
|
+
} else {
|
|
176
|
+
//just follow the edge
|
|
177
|
+
bestMove.move(state);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
node = bestMove.target;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
return node;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Perform a playout from the root node
|
|
189
|
+
* @returns {S} final state of the playout
|
|
190
|
+
*/
|
|
191
|
+
playout() {
|
|
192
|
+
const computeValidMoves = this.computeValidMoves;
|
|
193
|
+
|
|
194
|
+
const computeTerminalFlag = this.computeTerminalFlag;
|
|
195
|
+
|
|
196
|
+
const state = this.cloneState(this.rootState);
|
|
197
|
+
|
|
198
|
+
assert.notEqual(state, this.rootState, 'cloneState must produce a new state object, instead it produced the same one');
|
|
199
|
+
|
|
200
|
+
let node = this.root;
|
|
201
|
+
|
|
202
|
+
while (!node.isTerminal() && node.depth < this.maxExplorationDepth) {
|
|
203
|
+
|
|
204
|
+
if (!node.isExpanded()) {
|
|
205
|
+
node.expand(state, computeValidMoves, computeTerminalFlag);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const child = this.selectRandom(node, state);
|
|
209
|
+
|
|
210
|
+
if (child === node) {
|
|
211
|
+
// prevent infinite recursion
|
|
212
|
+
// this should not happen?
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
node = child;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!node.isTerminal() && node.depth >= this.maxExplorationDepth) {
|
|
220
|
+
//cap the state by depth, propagate heuristic score
|
|
221
|
+
node.type = StateType.DepthCapped;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// record play-through
|
|
225
|
+
const terminalFlag = node.type;
|
|
226
|
+
|
|
227
|
+
if (terminalFlag === StateType.Win) {
|
|
228
|
+
node.addPlayouts(1, 1, 0);
|
|
229
|
+
} else if (terminalFlag === StateType.Loss) {
|
|
230
|
+
node.addPlayouts(1, 0, 1);
|
|
231
|
+
} else if (terminalFlag === StateType.Tie || terminalFlag === StateType.DepthCapped) {
|
|
232
|
+
node.addPlayouts(1, 0, 0);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return state;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @template S
|
|
241
|
+
* @param {S} state
|
|
242
|
+
* @param {StateNode} source
|
|
243
|
+
* @param {MoveEdge} edge
|
|
244
|
+
* @param {function(S):StateType} computeTerminalFlag
|
|
245
|
+
* @param {function(StateNode, S)} heuristic
|
|
246
|
+
*/
|
|
247
|
+
function materializedEdgeTarget(state, source, edge, computeTerminalFlag, heuristic) {
|
|
248
|
+
|
|
249
|
+
const child = new StateNode();
|
|
250
|
+
child.parent = source;
|
|
251
|
+
child.depth = source.depth + 1;
|
|
252
|
+
|
|
253
|
+
const computedState = edge.move(state);
|
|
254
|
+
|
|
255
|
+
const terminalFlag = computeTerminalFlag(computedState);
|
|
256
|
+
|
|
257
|
+
assert.enum(terminalFlag, StateType, 'terminalFlag');
|
|
258
|
+
|
|
259
|
+
child.type = terminalFlag;
|
|
260
|
+
|
|
261
|
+
edge.target = child;
|
|
262
|
+
|
|
263
|
+
const childHeuristicScore = heuristic(child, computedState);
|
|
264
|
+
|
|
265
|
+
assert.notNaN(childHeuristicScore, 'childHeuristicScore');
|
|
266
|
+
|
|
267
|
+
child.heuristicValue = childHeuristicScore;
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
// bubble the heuristic score up the tree
|
|
271
|
+
// child.bubbleUpHeuristicScore(); // heuristic value changes sign depending on the team making the move, so aggregation becomes tricky
|
|
272
|
+
|
|
273
|
+
return computedState;
|
|
274
|
+
}
|
|
275
|
+
|