Passed
Push — master ( c241db...a6f3d5 )
by Thijs
02:46
created

DecisionPoint::considerMove()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

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