Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.02% covered (success)
95.02%
191 / 201
71.43% covered (warning)
71.43%
10 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
PlayResultProcessorComponent
94.79% covered (success)
94.79%
182 / 192
71.43% covered (warning)
71.43%
10 / 14
69.67
0.00% covered (danger)
0.00%
0 / 1
 checkPreviousPlay
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
7
 checkPreviousPlayAndGetResult
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getNewStatus
85.71% covered (warning)
85.71%
18 / 21
0.00% covered (danger)
0.00%
0 / 1
13.49
 updateTsumegoStatus
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 checkAddFavorite
73.33% covered (warning)
73.33%
11 / 15
0.00% covered (danger)
0.00%
0 / 1
5.47
 checkRemoveFavorite
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
4.18
 updateTsumegoAttempt
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
7
 processRatingChangeStep
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 processRatingChange
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
5
 processDamage
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 processXpChange
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 processErrorAchievement
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
 processUnsortedStuff
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
4.01
 checkMisplay
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3App::uses('TsumegoStatus', 'Model');
4App::uses('SetConnection', 'Model');
5App::uses('Rating', 'Utility');
6App::uses('Util', 'Utility');
7App::uses('Decoder', 'Utility');
8App::uses('HeroPowers', 'Utility');
9App::uses('TsumegoXPAndRating', 'Utility');
10App::uses('Level', 'Utility');
11App::uses('Progress', 'Utility');
12
13class PlayResultProcessorComponent extends Component
14{
15    public $components = ['Session'];
16
17    public function checkPreviousPlay($timeModeComponent): void
18    {
19        $this->checkAddFavorite();
20        $this->checkRemoveFavorite();
21
22        $previousTsumegoID = Util::clearNumericCookie('previousTsumegoID');
23        if (!$previousTsumegoID)
24            return;
25
26        $previousTsumego = ClassRegistry::init('Tsumego')->findById($previousTsumegoID);
27        if (!$previousTsumego)
28            return;
29
30        $result = $this->checkPreviousPlayAndGetResult($previousTsumego);
31
32        $previousTsumegoStatus = ClassRegistry::init('TsumegoStatus')->find('first', [
33            'conditions' => [
34                'tsumego_id' => (int) $previousTsumego['Tsumego']['id'],
35                'user_id' => (int) Auth::getUserID(),
36            ],
37        ]);
38
39        $this->updateTsumegoStatus($previousTsumego, $result, $previousTsumegoStatus);
40
41        if (!isset($result['solved']))
42            return;
43        if (HeroPowers::getSprintRemainingSeconds() > 0)
44            $result['xp-modifier'] = ($result['xp-modifier'] ?: 1) * Constants::$SPRINT_MULTIPLIER;
45
46        $previousStatusValue = $previousTsumegoStatus ? $previousTsumegoStatus['TsumegoStatus']['status'] : 'N';
47
48        // I need to save the original tsumego rating I calculated XP change for
49        // this is to avoid that rating gets changed, and the XP change calculation would
50        // be based on the changed rating, and would slightly differ from the promised change
51        $originalTsumegoRating = $previousTsumego['Tsumego']['rating'];
52
53        $this->processRatingChange($previousTsumego, $result, $previousStatusValue);
54        $this->processDamage($result, $previousStatusValue);
55        $timeModeComponent->processPlayResult($previousTsumego, $result);
56        $this->processXpChange($previousTsumego, $result, $previousStatusValue, $originalTsumegoRating);
57        $this->updateTsumegoAttempt($previousTsumego, $result, $previousStatusValue);
58        $this->processErrorAchievement($result);
59        $this->processUnsortedStuff($previousTsumego, $result);
60    }
61
62    public function checkPreviousPlayAndGetResult(&$previousTsumego): array
63    {
64        $result = [];
65        if ($misplays = $this->checkMisplay())
66        {
67            $result['solved'] = false;
68            $result['misplays'] = $misplays;
69        }
70        if (Decoder::decodeSuccess($previousTsumego['Tsumego']['id']))
71            $result['solved'] = true;
72
73        return $result;
74    }
75
76    private function getNewStatus($solved, $currentStatus, &$result)
77    {
78        if ($solved)
79        {
80            if ($currentStatus == 'W') // half xp state
81            {$result['xp-modifier'] = ($result['xp-modifier'] ?: 1) * Constants::$SECOND_SOLVE_XP_MULTIPLIER;
82                return 'C'; // double solved
83            }
84            if ($currentStatus == 'G')
85            {
86                $result['xp-modifier'] = ($result['xp-modifier'] ?: 1) * Constants::$GOLDEN_TSUMEGO_XP_MULTIPLIER;
87                return 'S';
88            }
89            if ($currentStatus == 'V' || $currentStatus == 'N')
90                return 'S';
91            return $currentStatus; // failed can't be unfailed by solving, user has to wait until next day or rejuvenation
92        }
93
94        // not solved from now
95        if ($currentStatus == 'V') // if it was just visited so far (so we don't overwrite solved)
96        {if (Auth::getUser()['damage'] >= Util::getHealthBasedOnLevel(Auth::getUser()['level']))
97            return 'F';  // only mark as failed when the user has no hearts left
98            return $currentStatus;
99        }
100        if ($currentStatus == 'W')
101        {
102            if (Auth::getUser()['damage'] >= Util::getHealthBasedOnLevel(Auth::getUser()['level']))
103                return 'X'; // only mark as 'stale failed' when the user has no hearts left
104            return $currentStatus;
105        }
106        if ($currentStatus == 'G')
107            return 'V'; // failed golden tsumego
108        return $currentStatus;
109    }
110
111    private function updateTsumegoStatus(array $previousTsumego, array &$result, ?array $previousTsumegoStatus): void
112    {
113        if ($previousTsumegoStatus == null)
114        {
115            $previousTsumegoStatus['TsumegoStatus'] = [];
116            $previousTsumegoStatus['TsumegoStatus']['user_id'] = Auth::getUserID();
117            $previousTsumegoStatus['TsumegoStatus']['tsumego_id'] = $previousTsumego['Tsumego']['id'];
118            $previousTsumegoStatus['TsumegoStatus']['status'] = 'V';
119        }
120        $_COOKIE['previousTsumegoBuffer'] = $previousTsumegoStatus['TsumegoStatus']['status'];
121
122        if (isset($result['solved']))
123        {
124            $newStatus = $this->getNewStatus($result['solved'], $previousTsumegoStatus['TsumegoStatus']['status'], $result);
125            if (TsumegoUtil::isSolvedStatus($newStatus) && !TsumegoUtil::isSolvedStatus($previousTsumegoStatus['TsumegoStatus']['status']))
126                Auth::getUser()['solved'] = Auth::getUser()['solved'] + 1;
127            $previousTsumegoStatus['TsumegoStatus']['status'] = $newStatus;
128        }
129        $previousTsumegoStatus['TsumegoStatus']['created'] = date('Y-m-d H:i:s');
130        ClassRegistry::init('TsumegoStatus')->save($previousTsumegoStatus);
131    }
132
133    private function checkAddFavorite(): void
134    {
135        if (!Auth::isLoggedIn())
136            return;
137
138        $tsumegoID = Util::clearCookie('add_favorite');
139        if (empty($tsumegoID))
140            return;
141
142        $favorite = ClassRegistry::init('Favorite')->find('first', ['conditions' => ['user_id' => Auth::getUserID(), 'tsumego_id' => $tsumegoID]]);
143        if ($favorite)
144            return;
145
146        try
147        {
148            $favorite = [];
149            $favorite['user_id'] = Auth::getUserID();
150            $favorite['tsumego_id'] = $tsumegoID;
151            ClassRegistry::init('Favorite')->create();
152            ClassRegistry::init('Favorite')->save($favorite);
153        }
154        catch (Exception $e)
155        {
156            throw new Exception('Tsumego id = ' . $tsumegoID, 0, $e);
157        }
158    }
159
160    private function checkRemoveFavorite(): void
161    {
162        if (!Auth::isLoggedIn())
163            return;
164
165        $tsumegoID = Util::clearCookie('remove_favorite');
166        if (empty($tsumegoID))
167            return;
168
169        $favorite = ClassRegistry::init('Favorite')->find('first', ['conditions' => ['user_id' => Auth::getUserID(), 'tsumego_id' => $tsumegoID]]);
170        if (!$favorite)
171            return;
172        ClassRegistry::init('Favorite')->delete($favorite['Favorite']['id']);
173    }
174
175    private function updateTsumegoAttempt(array $previousTsumego, array $result, $previousTsumegoStatus): void
176    {
177        if (Auth::isInTimeMode())
178            return;
179        if (TsumegoUtil::isRecentlySolved($previousTsumegoStatus))
180            return;
181        $lastTsumegoAttempt = ClassRegistry::init('TsumegoAttempt')->find(
182            'first',
183            ['conditions'
184                => ['user_id' => Auth::getUserID(),
185                    'tsumego_id' => $previousTsumego['Tsumego']['id']],
186                'order' => 'id DESC']
187        );
188
189        // only not solved ones are updated (misplays get accumulated)
190        if (!$lastTsumegoAttempt || $lastTsumegoAttempt['TsumegoAttempt']['solved'])
191        {
192            $tsumegoAttempt = [];
193            $tsumegoAttempt['TsumegoAttempt']['user_id'] = Auth::getUserID();
194            $tsumegoAttempt['TsumegoAttempt']['tsumego_id'] = $previousTsumego['Tsumego']['id'];
195            $tsumegoAttempt['TsumegoAttempt']['seconds'] = 0;
196            $tsumegoAttempt['TsumegoAttempt']['solved'] = $result['solved'];
197            $tsumegoAttempt['TsumegoAttempt']['tsumego_rating'] = $previousTsumego['Tsumego']['rating'];
198            $tsumegoAttempt['TsumegoAttempt']['misplays'] = 0;
199        }
200        else
201            $tsumegoAttempt = $lastTsumegoAttempt;
202
203        $tsumegoAttempt['TsumegoAttempt']['user_rating'] = Auth::getUser()['rating'];
204        $tsumegoAttempt['TsumegoAttempt']['gain'] = $result['xp-gained'] ?: 0;
205        $tsumegoAttempt['TsumegoAttempt']['seconds'] += Decoder::decodeSeconds($previousTsumego);
206        $tsumegoAttempt['TsumegoAttempt']['solved'] = $result['solved'];
207        $tsumegoAttempt['TsumegoAttempt']['tsumego_rating'] = $previousTsumego['Tsumego']['rating'];
208        $tsumegoAttempt['TsumegoAttempt']['misplays'] += $result['misplays'] ?: 0;
209        $tsumegoAttempt['TsumegoAttempt']['created'] = date('Y-m-d H:i:s');
210        ClassRegistry::init('TsumegoAttempt')->save($tsumegoAttempt);
211    }
212
213    private static function processRatingChangeStep(float &$userRating, float &$tsumegoRating, bool $isWin): void
214    {
215        $userRatingDelta = Rating::calculateRatingChange($userRating, $tsumegoRating, $isWin ? 1 : 0, Constants::$PLAYER_RATING_CALCULATION_MODIFIER);
216        $tsumegoRatingDelta = Rating::calculateRatingChange($tsumegoRating, $userRating, $isWin ? 0 : 1, Constants::$TSUMEGO_RATING_CALCULATION_MODIFIER);
217        $userRating += $userRatingDelta;
218        $tsumegoRating += $tsumegoRatingDelta;
219    }
220
221    private function processRatingChange(array &$previousTsumego, array $result, string $previousTsumegoStatus): void
222    {
223        if (!Auth::ratingisGainedInCurrentMode())
224            return;
225        if (!Level::XPAndRatingIsGainedInTsumegoStatus($previousTsumegoStatus))
226            return;
227        $userRating = (float) Auth::getUser()['rating'];
228        $tsumegoRating = (float) $previousTsumego['Tsumego']['rating'];
229
230        //process misplays first
231        for ($i = 0; $i < $result['misplays']; $i++)
232            self::processRatingChangeStep($userRating, $tsumegoRating, false);
233
234        // lastly process the solve
235        if ($result['solved'])
236            self::processRatingChangeStep($userRating, $tsumegoRating, true);
237
238        Auth::getUser()['rating'] = $userRating;
239        Auth::saveUser();
240
241        $previousTsumego['Tsumego']['rating'] = Util::clampOptional(
242            $tsumegoRating,
243            $previousTsumego['Tsumego']['minimum_rating'],
244            $previousTsumego['Tsumego']['maximum_rating']);
245        $previousTsumego['Tsumego']['activity_value']++;
246        ClassRegistry::init('Tsumego')->save($previousTsumego);
247    }
248
249    private function processDamage(array $result, $previousStatusValue): void
250    {
251        if (!$result['misplays'])
252            return;
253        if (!Auth::isInLevelMode())
254            return;
255        if (TsumegoUtil::isRecentlySolved($previousStatusValue))
256            return;
257        Auth::getUser()['damage'] += $result['misplays'];
258        Auth::saveUser();
259    }
260
261    private function processXpChange(array $previousTsumego, array &$result, string $previousTsumegoStatus, $originalTsumegoRating): void
262    {
263        if (!Auth::XPisGainedInCurrentMode())
264            return;
265        if (!Level::XPAndRatingIsGainedInTsumegoStatus($previousTsumegoStatus))
266            return;
267        if (!$result['solved'])
268            return;
269
270        $multiplier = ($result['xp-modifier'] ?: 1);
271        $multiplier *=  TsumegoXPAndRating::getProgressDeletionMultiplier(TsumegoUtil::getProgressDeletionCount($previousTsumego['Tsumego']));
272
273        $user = & Auth::getUser();
274        $result['xp-gained'] = Rating::ratingToXP($originalTsumegoRating, $multiplier);
275        Level::addXPAsResultOfTsumegoSolving($user, $result['xp-gained']);
276    }
277
278    private function processErrorAchievement(array $result): void
279    {
280        $achievementCondition = ClassRegistry::init('AchievementCondition')->find('first', [
281            'order' => 'value DESC',
282            'conditions' => [
283                'user_id' => Auth::getUserID(),
284                'category' => 'err',
285            ],
286        ]);
287        if (!$achievementCondition)
288        {
289            $achievementCondition = [];
290            ClassRegistry::init('AchievementCondition')->create();
291        }
292        $achievementCondition['AchievementCondition']['category'] = 'err';
293        $achievementCondition['AchievementCondition']['user_id'] = Auth::getUserID();
294        if ($result['solved'])
295            $achievementCondition['AchievementCondition']['value']++;
296        else
297            $achievementCondition['AchievementCondition']['value'] = 0;
298        ClassRegistry::init('AchievementCondition')->save($achievementCondition);
299    }
300
301    private function processUnsortedStuff(array $previousTsumego, array $result): void
302    {
303        if (!$result['solved'])
304            return;
305
306        $solvedTsumegoRank = Rating::getReadableRankFromRating($previousTsumego['Tsumego']['rating']);
307        AppController::saveDanSolveCondition($solvedTsumegoRank, $previousTsumego['Tsumego']['id']);
308        AppController::updateGems($solvedTsumegoRank);
309        if ($_COOKIE['sprint'] == 1)
310            AppController::updateSprintCondition(true);
311        else
312            AppController::updateSprintCondition();
313        if ($_COOKIE['type'] == 'g')
314            AppController::updateGoldenCondition(true);
315
316        Util::clearCookie('sequence');
317        Util::clearCookie('type');
318    }
319
320    /* @return The number of misplays and consumes the misplays cookie in the process */
321    private function checkMisplay(): int
322    {
323        return (int) Util::clearCookie('misplays');
324    }
325}