Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
90.29% |
158 / 175 |
|
43.75% |
7 / 16 |
CRAP | |
0.00% |
0 / 1 |
| TimeMode | |
90.17% |
156 / 173 |
|
43.75% |
7 / 16 |
57.87 | |
0.00% |
0 / 1 |
| __construct | |
92.86% |
26 / 28 |
|
0.00% |
0 / 1 |
9.03 | |||
| startTimeMode | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
3.14 | |||
| cancelTimeMode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| createNewSession | |
88.24% |
15 / 17 |
|
0.00% |
0 / 1 |
3.01 | |||
| getRatingBounds | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
| getRelevantTsumegos | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
3.21 | |||
| createSessionAttempts | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
| deduceAttemptStatus | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
| processPlayResult | |
94.74% |
18 / 19 |
|
0.00% |
0 / 1 |
6.01 | |||
| skip | |
86.67% |
13 / 15 |
|
0.00% |
0 / 1 |
3.02 | |||
| prepareNextToSolve | |
82.61% |
19 / 23 |
|
0.00% |
0 / 1 |
5.13 | |||
| currentWillBeLast | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| checkFinishSession | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
7 | |||
| calculatePoints | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| exportTimeModeInfo | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
| getCurrentRank | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
| 1 | <?php |
| 2 | |
| 3 | App::uses('TimeModeUtil', 'Utility'); |
| 4 | App::uses('RatingBounds', 'Utility'); |
| 5 | |
| 6 | class 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 | } |