Completed
Pull Request — master (#7)
by Thijs
02:08
created

DecisionNode::considerMove()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 7
cts 7
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 13
nc 2
nop 2
crap 2
1
<?php
2
3
namespace lucidtaz\minimax\engine;
4
5
use lucidtaz\minimax\game\GameState;
6
use lucidtaz\minimax\game\Player;
7
8
/**
9
 * Node in the decision search tree
10
 * An object of this class can be queried for its ideal decision (and according
11
 * score) by calling the decide() method. It will recursively construct child
12
 * nodes and evaluate them using that method as well.
13
 */
14
class DecisionNode
15
{
16
    /**
17
     * @var Player The player to optimize for.
18
     */
19
    private $objectivePlayer;
20
21
    /**
22
     * @var GameState The current GameState to base future decisions on.
23
     */
24
    private $state;
25
26
    /**
27
     * @var int Limit on how deep we can continue to search, recursion limiter.
28
     */
29
    private $depthLeft;
30
31
    /**
32
     * @var NodeType Whether we are a min-node or a max-node. This enables the
33
     * caller to select either the most favorable or the least favorable
34
     * outcome.
35
     */
36
    private $type;
37
38
    /**
39
     * @param Player $objectivePlayer The Player to optimize for
40
     * @param GameState $state Current GameState to base decisions on
41
     * @param int $depthLeft Recursion limiter
42
     * @param NodeType $type Signifies whether to minimize or maximize the score
43
     */
44 13
    public function __construct(Player $objectivePlayer, GameState $state, int $depthLeft, NodeType $type)
45
    {
46 13
        $this->objectivePlayer = $objectivePlayer;
47 13
        $this->state = $state;
48 13
        $this->depthLeft = $depthLeft;
49 13
        $this->type = $type;
50 13
    }
51
52
    /**
53
     * Determine the ideal decision for this node
54
     * This means either the best or the worst possible outcome for the
55
     * objective player, based on who is actually playing. (If the objective
56
     * player is currently playing, we take the best outcome, otherwise we take
57
     * the worst. This reflects that the opponent also plays optimally.)
58
     * @return DecisionWithScore
59
     */
60 13
    public function decide(): DecisionWithScore
61
    {
62 13
        if ($this->depthLeft == 0) {
63 11
            return $this->makeLeafResult();
64
        }
65
66
        /* @var $possibleMoves GameState[] */
67 13
        $possibleMoves = $this->state->getPossibleMoves();
68 13
        if (empty($possibleMoves)) {
69 10
            return $this->makeLeafResult();
70
        }
71
72 12
        $bestDecisionWithScore = null;
73 12
        foreach ($possibleMoves as $move) {
74 12
            $bestDecisionWithScore = $this->evaluateMove($move, $bestDecisionWithScore);
75
        }
76
77 12
        return $bestDecisionWithScore;
78
    }
79
80
    /**
81
     * Formulate the resulting decision, considering we do not look any further
82
     * The reason for not looking further can either be due to hitting the
83
     * recursion limit or because the game has actually concluded.
84
     * @return DecisionWithScore
85
     */
86 13
    private function makeLeafResult(): DecisionWithScore
87
    {
88 13
        $result = new DecisionWithScore;
89 13
        $result->age = $this->depthLeft;
90 13
        $result->score = $this->state->evaluateScore($this->objectivePlayer);
91 13
        return $result;
92
    }
93
94
    /**
95
     * Apply a move and evaluate the outcome
96
     * @param GameState $stateAfterMove The result of taking the move
97
     * @param DecisionWithScore $bestResultSoFar Best result encountered so far.
98
     * @todo Can probably be cleaned up by moving that logic to the caller.
99
     * @return DecisionWithScore
100
     */
101 12
    private function evaluateMove(
102
        GameState $stateAfterMove,
103
        DecisionWithScore $bestResultSoFar = null
104
    ): DecisionWithScore {
105 12
        $childResult = $this->buildAndEvaluateChildNode($stateAfterMove);
106
107 12
        $replaced = false;
108 12
        $bestResult = $this->replaceIfBetter(
109
            $childResult,
110
            $bestResultSoFar,
111
            $replaced
112
        );
113 12
        if ($replaced) {
114
            // Register that the decision was reached by applying the currently
115
            // evaluated move. We cannot get this decision (GameState) from the
116
            // result itself, because it's already many levels deeper in the
117
            // game tree.
118 12
            $bestResult->decision = $stateAfterMove;
119
        }
120
121 12
        return $bestResult;
122
    }
123
124
    /**
125
     * Recursively evaluate a child decision
126
     * @param GameState $stateAfterMove The GameState that was created as a
127
     * result of the current move.
128
     * @return DecisionWithScore
129
     */
130 12
    private function buildAndEvaluateChildNode(GameState $stateAfterMove): DecisionWithScore
131
    {
132 12
        $nextPlayerIsFriendly = $stateAfterMove->getNextPlayer()->isFriendsWith($this->objectivePlayer);
133 12
        $nextDecisionPoint = new static(
134 12
            $this->objectivePlayer,
135
            $stateAfterMove,
136 12
            $this->depthLeft - 1,
137 12
            $nextPlayerIsFriendly ? $this->type : $this->type->alternate()
138
        );
139 12
        return $nextDecisionPoint->decide();
140
    }
141
142
    /**
143
     * Take the best of the two operands
144
     * The meaning of "best" is decided by the "ideal" member variable
145
     * comparator
146
     * @param DecisionWithScore $new
147
     * @param DecisionWithScore $current
148
     * @param bool $replaced Set to true if the second operand was better
149
     * @return DecisionWithScore
150
     */
151 12
    private function replaceIfBetter(
152
        DecisionWithScore $new,
153
        DecisionWithScore $current = null,
154
        &$replaced = false
155
    ): DecisionWithScore {
156 12
        if ($current === null) {
157 12
            $replaced = true;
158 12
            return $new;
159
        }
160
161 11
        $ideal = $this->type == NodeType::MIN()
162 11
            ? DecisionWithScore::getWorstComparator()
163 11
            : DecisionWithScore::getBestComparator();
164 11
        $idealDecisionWithScore = $ideal($new, $current);
165 11
        if ($idealDecisionWithScore === $new) {
166 9
            $replaced = true;
167 9
            return $new;
168
        }
169
170 11
        $replaced = false;
171 11
        return $current;
172
    }
173
}
174