Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.29% covered (success)
90.29%
158 / 175
43.75% covered (danger)
43.75%
7 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
TimeMode
90.17% covered (success)
90.17%
156 / 173
43.75% covered (danger)
43.75%
7 / 16
57.87
0.00% covered (danger)
0.00%
0 / 1
 __construct
92.86% covered (success)
92.86%
26 / 28
0.00% covered (danger)
0.00%
0 / 1
9.03
 startTimeMode
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
3.14
 cancelTimeMode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createNewSession
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
3.01
 getRatingBounds
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getRelevantTsumegos
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
3.21
 createSessionAttempts
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 deduceAttemptStatus
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 processPlayResult
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
6.01
 skip
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
3.02
 prepareNextToSolve
82.61% covered (warning)
82.61%
19 / 23
0.00% covered (danger)
0.00%
0 / 1
5.13
 currentWillBeLast
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 checkFinishSession
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
7
 calculatePoints
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 exportTimeModeInfo
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getCurrentRank
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
1<?php
2
3App::uses('TimeModeUtil', 'Utility');
4App::uses('RatingBounds', 'Utility');
5
6class TimeMode
7{
8    public function __construct()
9    {
10        if (!Auth::isInTimeMode())
11            return;
12
13        $this->currentSession = ClassRegistry::init('TimeModeSession')->find('first', [
14            'conditions' => [
15                'user_id' => Auth::getUserID(),
16                'time_mode_session_status_id' => TimeModeUtil::$SESSION_STATUS_IN_PROGRESS]]);
17
18        if (!$this->currentSession)
19            return;
20
21        // is TimeModeUtil::$PROBLEM_COUNT normally, but can be less when not enough problems found
22        $attempts =  ClassRegistry::init('TimeModeAttempt')->find('all', [
23            'conditions' => ['time_mode_session_id' => $this->currentSession['TimeModeSession']['id']]]);
24        foreach ($attempts as $attempt)
25        {
26            if ($attempt['TimeModeAttempt']['time_mode_attempt_status_id'] == TimeModeUtil::$ATTEMPT_RESULT_SOLVED)
27                $this->successCount++;
28            elseif (
29                $attempt['TimeModeAttempt']['time_mode_attempt_status_id'] == TimeModeUtil::$ATTEMPT_RESULT_FAILED
30                || $attempt['TimeModeAttempt']['time_mode_attempt_status_id'] == TimeModeUtil::$ATTEMPT_STATUS_SKIPPED
31                || $attempt['TimeModeAttempt']['time_mode_attempt_status_id'] == TimeModeUtil::$ATTEMPT_STATUS_TIMEOUT)
32                    $this->failCount++;
33        }
34        $this->overallCount = count($attempts);
35
36        if ($this->overallCount == 0)
37        {
38            $this->cancelTimeMode();
39            return;
40        }
41
42        $this->currentOrder = ClassRegistry::init('TimeModeAttempt')->find('count', [
43            'conditions' => [
44                'time_mode_session_id' => $this->currentSession['TimeModeSession']['id'],
45                'time_mode_attempt_status_id !=' => TimeModeUtil::$ATTEMPT_RESULT_QUEUED]]) + 1;
46
47        $this->rank = ClassRegistry::init('TimeModeRank')->findById($this->currentSession['TimeModeSession']['time_mode_rank_id']);
48        $this->overallSecondsToSolve = ClassRegistry::init('TimeModeCategory')->find('first', [
49            'conditions' => ['id' => $this->currentSession['TimeModeSession']['time_mode_category_id']]])['TimeModeCategory']['seconds'];
50    }
51
52    public function startTimeMode(int $categoryID, int $rankID): void
53    {
54        if (!Auth::isLoggedIn())
55            throw new AppException('Not logged in.');
56
57        ClassRegistry::init('TimeModeSession')->deleteAll(['user_id' => Auth::getUserID(), 'time_mode_session_status_id' => TimeModeUtil::$SESSION_STATUS_IN_PROGRESS]);
58
59        $relevantTsumegos = $this->getRelevantTsumegos($rankID);
60        if (empty($relevantTsumegos))
61            throw new AppException('No relevant tsumegos.');
62        $currentTimeSession = $this->createNewSession($categoryID, $rankID);
63        $this->createSessionAttempts($currentTimeSession, $relevantTsumegos);
64    }
65
66    public static function cancelTimeMode(): void
67    {
68        ClassRegistry::init('TimeModeSession')->deleteAll(['user_id' => Auth::getUserID(), 'time_mode_session_status_id' => TimeModeUtil::$SESSION_STATUS_IN_PROGRESS]);
69    }
70
71    private function createNewSession(int $categoryID, int $rankID): array
72    {
73        $timeModeCategory = ClassRegistry::init('TimeModeCategory')->findById($categoryID);
74        if (!$timeModeCategory)
75            throw new AppException("Time mode session category with id=" . $categoryID . " not found");
76
77        $timeModeRank = ClassRegistry::init('TimeModeRank')->findById($rankID);
78        if (!$timeModeRank)
79            throw new AppException("Time mode rank category with id=" . $rankID . " not found");
80
81        Auth::getUser()['mode'] = Constants::$TIME_MODE;
82        Auth::saveUser();
83        $currentTimeSession = [];
84        $currentTimeSession['user_id'] = Auth::getUserID();
85        $currentTimeSession['time_mode_session_status_id'] = TimeModeUtil::$SESSION_STATUS_IN_PROGRESS;
86        $currentTimeSession['time_mode_category_id'] = $timeModeCategory['TimeModeCategory']['id'];
87        $currentTimeSession['time_mode_rank_id'] = $rankID;
88        ClassRegistry::init('TimeModeSession')->create($currentTimeSession);
89        ClassRegistry::init('TimeModeSession')->save($currentTimeSession);
90        $currentTimeSession['id'] = ClassRegistry::init('TimeModeSession')->getLastInsertID();
91        return $currentTimeSession;
92    }
93
94    public static function getRatingBounds(?int $timeModeRankID): RatingBounds
95    {
96        $timeModeRankModel = ClassRegistry::init('TimeModeRank');
97
98        // I'm assuming, that the entries in the time_mode_rank tables have primary id ordered in the same order as the ranks, so the next entry
99        // is the higher rank, and the previous is the lower (checked in testTimeModeRankContentsIntegrity)
100        // This allows me to figure out what range should I cover by the current rank, which is
101        // <max_of_smaller_rank or 0 if it doesn't exit, max_of_current_rank if next rank exists or infinity]
102        // this should put every tsumego in some of the rank intervals regardless of the ranks configuration
103        $result = new RatingBounds();
104        if ($smallerRankRow = $timeModeRankModel->find('first', ['conditions' => ['id <' => $timeModeRankID], 'order' => 'id DESC']))
105            $result->min = Rating::getRankMinimalRating(Rating::GetRankFromReadableRank($smallerRankRow['TimeModeRank']['name']) + 1);
106
107        if ($timeModeRankModel->find('first', ['conditions' => ['id >' => $timeModeRankID], 'order' => 'id ASC']))
108        {
109            $currentRankRow = $timeModeRankModel->find('first', ['conditions' => ['id' => $timeModeRankID]]);
110            $result->max = Rating::getRankMinimalRating(Rating::GetRankFromReadableRank($currentRankRow['TimeModeRank']['name']) + 1);
111        }
112
113        return $result;
114    }
115
116    private function getRelevantTsumegos(int $timeModeRankID): array
117    {
118        $ratingBounds = $this->getRatingBounds($timeModeRankID);
119        $query = "SELECT tsumego.id as id FROM tsumego JOIN set_connection ON set_connection.tsumego_id = tsumego.id JOIN `set` ON set.id = set_connection.set_id WHERE `set`.included_in_time_mode = TRUE AND `set`.public = 1";
120        if ($ratingBounds->min)
121            $query .= " AND rating >= " . $ratingBounds->min;
122        if ($ratingBounds->max)
123            $query .= " AND rating < " . $ratingBounds->max;
124        return Util::query($query);
125    }
126
127    private function createSessionAttempts(array $currentTimeSession, $relevantTsumegos): void
128    {
129        shuffle($relevantTsumegos);
130        $relevantTsumegosCount = count($relevantTsumegos);
131        for ($i = 0; $i < TimeModeUtil::$PROBLEM_COUNT && $i < $relevantTsumegosCount; $i++)
132        {
133            $newTimeAttempt = [];
134            $newTimeAttempt['time_mode_session_id'] = $currentTimeSession['id'];
135            $newTimeAttempt['order'] = $i + 1;
136            $newTimeAttempt['tsumego_id'] = $relevantTsumegos[$i]['id'];
137            $newTimeAttempt['time_mode_attempt_status_id'] = TimeModeUtil::$ATTEMPT_RESULT_QUEUED;
138            ClassRegistry::init('TimeModeAttempt')->create($newTimeAttempt);
139            ClassRegistry::init('TimeModeAttempt')->save($newTimeAttempt);
140        }
141    }
142
143    private static function deduceAttemptStatus($result, $timeout)
144    {
145        if ($timeout)
146            return TimeModeUtil::$ATTEMPT_STATUS_TIMEOUT;
147        if (!$result['solved'])
148            return TimeModeUtil::$ATTEMPT_RESULT_FAILED;
149        return TimeModeUtil::$ATTEMPT_RESULT_SOLVED;
150    }
151
152    public function processPlayResult($previousTsumego, $result): void
153    {
154        if (!$this->currentSession)
155            return;
156        $currentAttempt = ClassRegistry::init('TimeModeAttempt')->find('first', [
157            'conditions' => [
158                'time_mode_session_id' => $this->currentSession['TimeModeSession']['id'],
159                'tsumego_id' => $previousTsumego['Tsumego']['id'],
160                'time_mode_attempt_status_id' => TimeModeUtil::$ATTEMPT_RESULT_QUEUED]]);
161        if (!$currentAttempt)
162            return; // this tsumego is not related to our time mode session, we ignore it
163        $timeout = Util::clearCookie('timeout');
164        $currentAttempt['TimeModeAttempt']['time_mode_attempt_status_id'] = self::deduceAttemptStatus($result, $timeout);
165        $seconds = $timeout ? $this->overallSecondsToSolve : Decoder::decodeSeconds($previousTsumego);
166        if (is_null($seconds))
167            throw new Exception("Seconds not provided.");
168        $timeModeCategory = ClassRegistry::init('TimeModeCategory')->findById($this->currentSession['TimeModeSession']['time_mode_category_id']);
169
170        $currentAttempt['TimeModeAttempt']['seconds'] = $seconds;
171        $currentAttempt['TimeModeAttempt']['points'] = $result['solved'] ? self::calculatePoints($seconds, $timeModeCategory['TimeModeCategory']['seconds']) : 0;
172        ClassRegistry::init('TimeModeAttempt')->save($currentAttempt);
173        $this->currentOrder++;
174    }
175
176    public function skip(): void
177    {
178        if (!$this->currentSession)
179            return;
180        $currentAttempt = ClassRegistry::init('TimeModeAttempt')->find('first', [
181            'conditions' => [
182                'time_mode_session_id' => $this->currentSession['TimeModeSession']['id'],
183                'started NOT' => null,
184                'time_mode_attempt_status_id' => TimeModeUtil::$ATTEMPT_RESULT_QUEUED]]);
185        if (!$currentAttempt)
186            return;
187        $currentAttempt['TimeModeAttempt']['time_mode_attempt_status_id'] = TimeModeUtil::$ATTEMPT_STATUS_SKIPPED;
188        $seconds = min($this->overallSecondsToSolve, time() - strtotime($currentAttempt['TimeModeAttempt']['started']));
189        $currentAttempt['TimeModeAttempt']['seconds'] = $seconds;
190        $currentAttempt['TimeModeAttempt']['points'] = 0;
191        ClassRegistry::init('TimeModeAttempt')->save($currentAttempt);
192        $this->currentOrder++;
193    }
194
195    // @return if not null, new tsumego id to show in the time mode
196    public function prepareNextToSolve(): ?int
197    {
198        if (!$this->currentSession)
199            return null;
200
201        $this->secondsToSolve = $this->overallSecondsToSolve;
202
203        // first one which doesn't have a status yet
204        $attempt = ClassRegistry::init('TimeModeAttempt')->find('first', [
205            'conditions' => [
206                'time_mode_session_id' => $this->currentSession['TimeModeSession']['id'],
207                'time_mode_attempt_status_id' => TimeModeUtil::$ATTEMPT_RESULT_QUEUED],
208            'order' => 'id ASC']);
209        if (!$attempt)
210            return null;
211        $attempt = $attempt['TimeModeAttempt'];
212
213        if (!$attempt['started'])
214        {
215            $attempt['started'] = date('Y-m-d H:i:s.u');
216            ClassRegistry::init('TimeModeAttempt')->save($attempt);
217        }
218        else
219        {
220            $start = new DateTime($attempt['started']);
221            $now = new DateTime();
222
223            $secondsSinceStarted = $now->getTimestamp() + $now->format('u') / 1e6 - ($start->getTimestamp() + $start->format('u') / 1e6);
224            $this->secondsToSolve = max(0, $this->secondsToSolve - $secondsSinceStarted);
225            if ($this->secondsToSolve == 0)
226            {
227                $attempt['time_mode_attempt_status_id'] = TimeModeUtil::$ATTEMPT_STATUS_TIMEOUT;
228                ClassRegistry::init('TimeModeAttempt')->save($attempt);
229                return $this->prepareNextToSolve();
230            }
231        }
232        return $attempt['tsumego_id'];
233    }
234
235    public function currentWillBeLast(): bool
236    {
237        return $this->currentOrder + 1 > $this->overallCount;
238    }
239
240    public function checkFinishSession(): ?int
241    {
242        if (!$this->currentSession)
243            return null;
244
245        if ($this->currentOrder - 1 < $this->overallCount)
246            return null;
247
248        $attempts = ClassRegistry::init('TimeModeAttempt')->find('all', ['conditions'
249        => ['time_mode_session_id' => $this->currentSession['TimeModeSession']['id']]]) ?: [];
250        assert(count($attempts) == $this->overallCount);
251
252        $overallPoints = 0.0;
253        $correctCount = 0;
254        foreach ($attempts as $attempt)
255        {
256            $overallPoints += $attempt['TimeModeAttempt']['points'];
257            if ($attempt['TimeModeAttempt']['time_mode_attempt_status_id'] == TimeModeUtil::$ATTEMPT_RESULT_SOLVED)
258                $correctCount++;
259        }
260
261        $sessionSuccessful = Util::getRatio($correctCount, count($attempts)) >= TimeModeUtil::$RATIO_OF_SOLVED_TO_SUCCEED;
262        $this->currentSession['TimeModeSession']['time_mode_session_status_id'] = $sessionSuccessful ? TimeModeUtil::$SESSION_STATUS_SOLVED : TimeModeUtil::$SESSION_STATUS_FAILED;
263        $this->currentSession['TimeModeSession']['points'] = $overallPoints;
264        ClassRegistry::init('TimeModeSession')->save($this->currentSession);
265        Auth::getUser()['mode'] = Constants::$LEVEL_MODE;
266        Auth::saveUser();
267
268        return $this->currentSession['TimeModeSession']['id'];
269    }
270
271    public static function calculatePoints(float $timeUsed, float $max): float
272    {
273        $timeRatio = 1 - ($timeUsed / $max);
274        return min(100 * (TimeModeUtil::$POINTS_RATIO_FOR_FINISHING + (1 - TimeModeUtil::$POINTS_RATIO_FOR_FINISHING) * $timeRatio), 100.0);
275    }
276
277    public function exportTimeModeInfo(): string
278    {
279        if (!$this->currentSession)
280            return 'null';
281        return 'new TimeModeState({'
282            . 'rank: \'' . $this->getCurrentRank() . '\','
283            . 'failCount: ' . $this->failCount . ','
284            . 'successCount: ' . $this->successCount . ','
285            . 'overallCount: ' . $this->overallCount . '})';
286    }
287
288    public function getCurrentRank(): string
289    {
290        if (!$this->rank)
291            return '';
292        return $this->rank['TimeModeRank']['name'];
293    }
294
295    public $currentSession;
296    public $rank;
297    public $secondsToSolve = 0; // remaining time
298    public $overallSecondsToSolve; // the time to solve the problem
299    public $successCount = 0;
300    public $failCount = 0;
301    public $overallCount = 0;
302    public $currentOrder;
303}