Passed
Pull Request — master (#7)
by Thijs
02:28
created

DecisionNode::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 6
cts 6
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 5
nc 1
nop 4
crap 1
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->considerMove($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 $bestDecisionWithScoreSoFar Best result
98
     * encountered so far. TODO: Can probably be cleaned up by moving that logic
99
     * to the caller.
100
     * @return DecisionWithScore
101
     */
102 12
    private function considerMove(
103
        GameState $stateAfterMove,
104
        DecisionWithScore $bestDecisionWithScoreSoFar = null
105
    ): DecisionWithScore {
106 12
        $nextDecisionWithScore = $this->considerNextMove($stateAfterMove);
107
108 12
        $replaced = false;
109 12
        $bestDecisionWithScore = $this->replaceIfBetter(
110
            $nextDecisionWithScore,
111
            $bestDecisionWithScoreSoFar,
112
            $replaced
113
        );
114 12
        if ($replaced) {
115 12
            $bestDecisionWithScore->decision = $stateAfterMove;
116
        }
117
118 12
        return $bestDecisionWithScore;
119
    }
120
121
    /**
122
     * Recursively evaluate a child decision
123
     * @param GameState $stateAfterMove The GameState that was created as a
124
     * result of the current move.
125
     * @return DecisionWithScore
126
     */
127 12
    private function considerNextMove(GameState $stateAfterMove): DecisionWithScore
128
    {
129 12
        $nextPlayerIsFriendly = $stateAfterMove->getNextPlayer()->isFriendsWith($this->objectivePlayer);
130 12
        $nextDecisionPoint = new static(
131 12
            $this->objectivePlayer,
132
            $stateAfterMove,
133 12
            $this->depthLeft - 1,
134 12
            $nextPlayerIsFriendly ? $this->type : $this->type->alternate()
135
        );
136 12
        return $nextDecisionPoint->decide();
137
    }
138
139
    /**
140
     * Take the best of the two operands
141
     * The meaning of "best" is decided by the "ideal" member variable
142
     * comparator
143
     * @param DecisionWithScore $new
144
     * @param DecisionWithScore $current
145
     * @param bool $replaced Set to true if the second operand was better
146
     * @return DecisionWithScore
147
     */
148 12
    private function replaceIfBetter(
149
        DecisionWithScore $new,
150
        DecisionWithScore $current = null,
151
        &$replaced = false
152
    ): DecisionWithScore {
153 12
        if ($current === null) {
154 12
            $replaced = true;
155 12
            return $new;
156
        }
157
158 11
        $ideal = $this->type == NodeType::MIN()
159 11
            ? DecisionWithScore::getWorstComparator()
160 11
            : DecisionWithScore::getBestComparator();
161 11
        $idealDecisionWithScore = $ideal($new, $current);
162 11
        if ($idealDecisionWithScore === $new) {
163 9
            $replaced = true;
164 9
            return $new;
165
        }
166
167 11
        $replaced = false;
168 11
        return $current;
169
    }
170
}
171