-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathRhascau.sol
1085 lines (997 loc) · 49.7 KB
/
Rhascau.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
import "../lib/openzeppelin-contracts/contracts/access/Ownable.sol";
/*
ERROR LIST REFERENCE:
E1 - Contract is paused
E2 - Insufficient funds provided with transaction
E3 - Provided entry fee is lower than minimum entry fee
E4 - User is already part of this or different game
E5 - Game has already started or has been finished
E6 - Game room is full
E7 - User is already part of this game
E8 - Game has alraedy started
E9 - User is not part of this game
E10 - Game has not started yet
E11 - Game has already finished
E12 - Not users turn
E13 - Vehicle is in base
E14 - Vehicle is rooted
E15 - Vehicle would start another lap
E16 - skill used on an empty tile
E17 - Can't use this skill on own vehicle
E18 - Incorrect vehicle selected
E19 - Dice need to be rolled first
E20 - Not enough funds
E21 - Failed to send Ether
E22 - Need to roll 6 to leave base
E23 - Vehicle is already on the board
E24 - Vehicle has already finished the lap
E25 - This move would cause a collision with user's own vehicle
E26 - Dice has already been rolled
E27 - Blockhash is not available yet
E28 - This move would be a waste of turn
E29 - Can't hurry this player yet
E30 - Destroy is on cooldown
E31 - Root is on cooldown
E32 - Dash is on cooldown
E33 - Bonus is on cooldown
*/
/* INTERFACES */
interface IRhascauManager {
function increaseUserRanking(uint256, address) external;
function getPlayerAddress(address) external view returns (address);
function updateUserStats(address, bool) external;
}
interface IARBsys {
function arbBlockNumber() external view returns (uint);
function arbBlockHash(uint256) external view returns (bytes32);
}
contract Rhascau is Ownable {
IRhascauManager public rhascauManager;
IARBsys public arbSys;
/* CONSTANTS */
uint256 constant REWARD_PER_ETH_WON = 4000;
uint8 constant PIECE_PER_PLAYER = 4;
uint8 constant MAX_PLAYERS = 4;
uint8 constant TILE_COUNT = 40;
uint256 constant MIN_ENTRY_FEE = 0.001 ether;
uint8 constant DESTROY_COOLDOWN = 1;
uint8 constant ROOT_COOLDOWN = 3;
uint8 constant ROLLAGAIN_COOLDOWN = 4;
uint8 constant DASH_COOLDOWN = 1;
/* EVENTS */
event GameStarted(uint256 _roomId);
event RoomStateChanged(uint256 _roomId, uint8 _playersCount);
event LapFinished(uint256 _roomId, uint8 _vehicleId);
event VehicleLeftBase(uint256 _roomId, uint8 _vehicleId);
event VehicleRooted(uint256 _roomId, uint8 _vehicleId);
event VehicleDashed(uint256 _roomId, uint8 _vehicleId, uint8 _from, uint8 _to);
event PlayerUsedBonus(uint256 _roomId, classEnum _class);
event VehicleDestroyed(uint256 _roomId, uint8 _vehicleId, skillsEnum _skill);
event VehicleMoved(uint256 _roomId, uint8 _vehicleId, uint8 _from, uint8 _to);
event DiceRolled(classEnum _class, uint256 _roomId, uint8 _result);
event VehicleUnrooted(uint256 _roomId, uint8 _vehicleId);
event NewTurn(uint256 _roomId, classEnum _class);
event VehicleDashedAndDestroyed(uint256 _roomId, uint8 _vehicleId, uint8 _destroyedVehicleId, uint8 _from, uint8 _to);
event VehicleMovedAndDestroyed(uint256 _roomId, uint8 _vehicleId, uint8 _from, uint8 _to, uint8 _destroyedVehicleId);
event RapidMoves(uint256 _roomId);
event EmojiSent(uint256 _roomId, classEnum _class, uint8 _type);
event GameFinished(uint256 _roomId, classEnum _class, address _winner, uint256 _prize);
event PlayerIsAFK(uint256 _roomId, classEnum _class);
event PlayerIsBack(uint256 _roomId, classEnum _class);
constructor(address _rhascauManagerAddress, address _arbSysAddress) {
rhascauManager = IRhascauManager(_rhascauManagerAddress);
arbSys = IARBsys(_arbSysAddress);
}
/// @dev mapping handling the randomness of the dice roll (see rollDice)
mapping (address => uint256) blockHashToBeUsed;
/// @dev mapping handling first game of the day and first win of the day
mapping (address => RewardsTimer) userToRewardTimer;
mapping (address => bool) isUserInGame;
//array of all game rooms
GameRoom[] public games;
bool public isContractPaused = false;
uint256 public providerFee = 2;
uint256 public turnTime = 40;
address public protocolsFeeAddress = 0x56E380e2A76A35eb8f6caF8B03D085C786E0d436;
enum classEnum {ONE, TWO, THREE, FOUR}
enum skillsEnum {DESTROY, ROOT, ROLLAGAIN, DASH, NONE}
struct GameInfo {
uint8 playersCount;
uint8 lastRoll;
uint256 moveTimestamp;
uint256 entryFee;
bool hasStarted;
bool hasEnded;
}
struct SkillsCooldown {
uint8 destroyCooldown;
uint8 rootCooldown;
uint8 rollAgainCooldown;
uint8 dashCooldown;
}
struct Vehicle {
uint8 id;
bool isOnBoard;
bool isRooted;
bool isLapDone;
bool isInitialized;
classEnum class;
}
struct Tile {
bool isOccupied;
Vehicle vehicle;
}
struct DiceRoll {
uint8 diceResult;
bool toBeUsed;
}
struct GameRoom {
GameInfo info;
Tile[TILE_COUNT] board;
mapping(address => mapping(uint8 => Vehicle)) players;
mapping(address => SkillsCooldown) cooldowns;
mapping(address => DiceRoll) diceRolls;
mapping(classEnum => address) classToPlayer;
mapping(classEnum => bool) afkRecord;
uint8 queue;
uint8 killCount;
}
struct RewardsTimer {
uint256 lastTimePlayed;
uint256 lastTimeWon;
}
/* MODIFIERS */
/// @dev Checks if the contract is paused
modifier contractWorking() {
require(isContractPaused == false, "E1");
_;
}
/// @dev Checks if the user has enough funds to enter the game
/// @param _fee amount provided by user
modifier enoughFunds(uint256 _fee) {
require(msg.value == _fee, "E2");
if(_fee != 0) require(msg.value >= MIN_ENTRY_FEE, "E3");
_;
}
/// @dev Checks if the user is eligable to join the game. User can not be part of another game at the moment of joining. Room must not be full. If game in the room has already started or finished, user can not join.
/// @param _roomId id of the game room
modifier joinableGame(uint256 _roomId) {
require(isUserInGame[msg.sender] == false, "E4");
require(games[_roomId].info.hasStarted == false && games[_roomId].info.hasEnded == false, "E5");
require(games[_roomId].info.playersCount < 4, "E6");
require(games[_roomId].players[msg.sender][getPlayerClass(_roomId, msg.sender) * PIECE_PER_PLAYER].isInitialized == false, "E7");
_;
}
/// @dev Checks if the user can leave current game room. Possible only when the game has not started
/// @param _roomId id of the game room
modifier playerCanLeave(uint256 _roomId) {
require(games[_roomId].info.hasStarted == false, "E8");
require(isPlayerInGame(_roomId, msg.sender) == true, "E9");
_;
}
/// @dev Checks if the user is in current game room. Game should have been started and not ended
/// @param _roomId id of the game room
modifier eligablePlayer(uint256 _roomId) {
require(isPlayerInGame(_roomId, msg.sender) == true, "E9");
require(games[_roomId].info.hasStarted == true, "E10");
require(games[_roomId].info.hasEnded == false, "E11");
_;
}
/// @dev Checks if its user turn to take an action.
/// @param _roomId id of the game room
modifier playersTurn(uint256 _roomId) {
require(getPlayerClass(_roomId, msg.sender) == games[_roomId].queue, "E12");
_;
}
/// @dev Checks wether user can take an action with the vehicle. Vehicle must be on board, not rooted, and can't cross the finish line with given _diceRoll.
/// @param _roomId id of the game room
/// @param _vehicleId id of the vehicle
/// @param _diceRoll number of tiles to move
modifier eligableVehicle(uint256 _roomId, uint8 _vehicleId, uint8 _diceRoll) {
require(games[_roomId].players[msg.sender][_vehicleId].isOnBoard == true, "E13");
require(games[_roomId].players[msg.sender][_vehicleId].isRooted == false, "E14");
require(howManyTilesLeft(getPlayerClass(_roomId, msg.sender), getVehicleTileIndex(_roomId, _vehicleId, msg.sender)) >= _diceRoll, "E15");
_;
}
/// @dev Checks if the user can use the skill. Skill can not be on cooldown, and must be casted on valid target. Check official rhascau rules at https://www.rhascau.com/
/// @param _roomId id of the game room
/// @param _skill skill to be used
/// @param _targetTile tile on which the skill will be casted
modifier skillAvailable(uint256 _roomId, skillsEnum _skill, uint8 _targetTile) {
require(games[_roomId].board[_targetTile].isOccupied == true, "E16");
if(_skill == skillsEnum.DESTROY) {
require(games[_roomId].cooldowns[msg.sender].destroyCooldown == 0, "E30");
require(games[_roomId].board[_targetTile].vehicle.class != games[_roomId].players[msg.sender][getPlayerClass(_roomId, msg.sender) * PIECE_PER_PLAYER].class, "E17");
}
else if(_skill == skillsEnum.ROOT) {
require(games[_roomId].cooldowns[msg.sender].rootCooldown == 0, "E31");
require(games[_roomId].board[_targetTile].vehicle.class != games[_roomId].players[msg.sender][getPlayerClass(_roomId, msg.sender) * PIECE_PER_PLAYER].class, "E17");
}
else {
require(games[_roomId].cooldowns[msg.sender].dashCooldown == 0, "E32");
require(games[_roomId].board[_targetTile].vehicle.class == games[_roomId].players[msg.sender][getPlayerClass(_roomId, msg.sender) * PIECE_PER_PLAYER].class, "E18");
require(!games[_roomId].players[games[_roomId].classToPlayer[games[_roomId].board[_targetTile].vehicle.class]][games[_roomId].board[_targetTile].vehicle.id].isRooted, "E14");
}
_;
}
/// @dev Checks if the game is already started.
/// @param _roomId id of the game room
modifier gameStarted(uint256 _roomId) {
require(games[_roomId].info.hasStarted == true, "E10");
_;
}
/// @dev Ensures that dice has been rolled by player before taking an action.
/// @param _roomId id of the game room
modifier diceRolled(uint256 _roomId) {
require(games[_roomId].diceRolls[msg.sender].toBeUsed == true, "E19");
_;
}
/* SETTERS FOR OWNER */
/// @dev Sets the address of the rhascau manager contract
/// @param _rhascauManagerAddress address of the rhascau manager contract
function setRhascauManager(address _rhascauManagerAddress) external onlyOwner {
rhascauManager = IRhascauManager(_rhascauManagerAddress);
}
/// @dev Swiches the state of contrat (paused or not)
function switchContract() external onlyOwner {
isContractPaused = !isContractPaused;
}
/// @dev Sets the fee collected by the protocol (in %)
/// @param _providerFee fee collected by the protocol
function setProviderFee(uint256 _providerFee) external onlyOwner {
providerFee = _providerFee;
}
/// @dev Sets the time user has to take an action before other players can skip his turn.
/// @param _turnTime time in blocks
function setTurnTime(uint256 _turnTime) external onlyOwner {
turnTime = _turnTime;
}
/// @dev Sets the address of the protocols fee collector
/// @param _protocolsFeeAddress address of the protocols fee collector
function setProtocolsFeeAddress(address _protocolsFeeAddress) external onlyOwner {
protocolsFeeAddress = _protocolsFeeAddress;
}
/* SUPPORTING FUNCTIONS */
/// @dev Removes vehicle form the board
/// @param _roomId id of the game room
/// @param _tileIndex id of the tile from which the vehicle will be removed
function removeVehicleFromTile(uint256 _roomId, uint8 _tileIndex) internal {
games[_roomId].players[games[_roomId].classToPlayer[games[_roomId].board[_tileIndex].vehicle.class]][games[_roomId].board[_tileIndex].vehicle.id].isOnBoard = false;
}
/// @dev Checks wether user plays via burner wallet or directly with his address (via smart contract). Neccessary for correct funds transfer and ranking system.
/// @param _user address that plays the game
/// @return address correct address of the user
function getCorrectAddress(address _user) internal view returns (address) {
if(rhascauManager.getPlayerAddress(_user) != address(0)) return rhascauManager.getPlayerAddress(_user);
else return _user;
}
/// @dev Checks for missing player's id in the game room.
/// @param _roomId id of the game room
/// @return uint8 index of the missing player
function getMissingPlayerIndex(uint256 _roomId) internal view returns (uint8) {
for(uint i=0; i<MAX_PLAYERS; i++) {
if(games[_roomId].classToPlayer[classEnum(i)] == address(0)) return uint8(i);
}
}
/// @dev Updates user statistics after the game has ended (games played, games won).
/// @param _roomId id of the game room
/// @param _winner address of the winner
function updateStatistics(uint256 _roomId, address _winner) internal {
uint8 winnerClass = getPlayerClass(_roomId, _winner);
for(uint i=0; i<MAX_PLAYERS; i++)
{
address user = getCorrectAddress(games[_roomId].classToPlayer[classEnum(i)]);
if(i == winnerClass) {
rhascauManager.updateUserStats(user, true);
}
else {
rhascauManager.updateUserStats(user, false);
}
}
}
/// @dev Helper function to increase user ranking appropriatly (see: getCorrectAddress).
/// @param _burner address of the user/burner
/// @param _amount amount of ranking points to be added
function increaseRankingInternal(address _burner, uint256 _amount) internal {
rhascauManager.increaseUserRanking(_amount, getCorrectAddress(_burner));
}
/// @dev Assigns game participation points to all players in the game room, taking into account first game/win of the day.
/// @param _roomId id of the game room
function assignGameParticipationPoints(uint256 _roomId) internal
{
for(uint i=0; i<MAX_PLAYERS; i++)
{
address user = games[_roomId].classToPlayer[classEnum(i)];
if(!games[_roomId].afkRecord[classEnum(i)])
{
if(block.timestamp - userToRewardTimer[user].lastTimePlayed >= 1 days)
{
increaseRankingInternal(user, 100);
}
else increaseRankingInternal(user, 50);
}
isUserInGame[user] = false;
userToRewardTimer[user].lastTimePlayed = block.timestamp;
}
}
/// @dev Checks if the player is in the game room.
/// @param _roomId id of the game room
/// @param _player address of the player
/// @return bool true if player is in the game room, otherwise false
function isPlayerInGame(uint256 _roomId, address _player) internal view returns (bool) {
return games[_roomId].players[_player][getPlayerClass(_roomId, _player) * PIECE_PER_PLAYER].isInitialized;
}
/// @dev Returns player class (see: classEnum)
/// @param _roomId id of the game room
/// @param _player address of the player
/// @return uint8 player class
function getPlayerClass(uint256 _roomId, address _player) public view returns (uint8) {
if(games[_roomId].players[_player][0].isInitialized == true) return 0;
else if(games[_roomId].players[_player][4].isInitialized == true) return 1;
else if(games[_roomId].players[_player][8].isInitialized == true) return 2;
else return 3;
}
/// @dev Returns player's starting tile index.
/// @param _playerClass player class (see: classEnum)
/// @return uint8 starting tile index
function getPlayerStartingPoint(uint8 _playerClass) internal pure returns (uint8) {
if(_playerClass == 0) return 39;
else if(_playerClass == 1) return 9;
else if(_playerClass == 2) return 19;
else return 29;
}
/// @dev Returns how many tiles are left to the finish line from _tileIndex, with respect to _playerClass.
/// @param _playerClass player class (see: classEnum)
/// @param _tileIndex id of the tile
/// @return uint8 number of tiles left
function howManyTilesLeft(uint8 _playerClass, uint8 _tileIndex) internal pure returns (uint8) {
int8 startingPoint = int8(getPlayerStartingPoint(_playerClass));
int8 tileIndex = int8(_tileIndex);
if(tileIndex == startingPoint) return 40;
return uint8((40 - (tileIndex - startingPoint)) % 40);
}
/// @dev Checks if the player is about to finish the game with current _diceRoll.
/// @param _roomId id of the game room
/// @param _vehicleId id of the vehicle
/// @param _diceRoll number of tiles player can move
/// @param _player address of the player
/// @return bool true if player is about to finish the game, otherwise false
function isAboutToFinish(uint256 _roomId, uint8 _vehicleId, uint8 _diceRoll, address _player) internal view returns (bool) {
return howManyTilesLeft(getPlayerClass(_roomId, _player), getVehicleTileIndex(_roomId, _vehicleId, _player)) == _diceRoll;
}
/// @dev Sends reward to the winner and protocol fee to the protocol fee collector address.
/// @param _winner address of the winner
/// @param _prize reward for the winner
/// @param _protocolFee protocol fee
function sendReward(address _winner, uint256 _prize, uint256 _protocolFee) private {
require(_prize <= address(this).balance, "E20");
(bool sent,) = _winner.call{value: _prize}("");
require(sent, "E21");
require(_protocolFee <= address(this).balance, "E20");
(bool sent2,) = protocolsFeeAddress.call{value: _protocolFee}("");
require(sent2, "E21");
}
/// @dev Calculates the rewards and protocol fees, assigns ranking points depending on the game outcome. Check official rhascau point assignment rules at https://www.rhascau.com/
/// @param _roomId id of the game room
/// @param _player address of the winner
/// @param _enemyInteraction true if the winner has interacted with the enemy in his game-finishing move, otherwise false
function assignWinner(uint256 _roomId, address _player, bool _enemyInteraction) internal {
games[_roomId].info.hasEnded = true;
uint256 reward = 4 * games[_roomId].info.entryFee - 4 * games[_roomId].info.entryFee / 100 * providerFee;
uint256 pointsReward = ((4 * games[_roomId].info.entryFee * 4000)/1e18);
uint256 protocolsFee = 4 * games[_roomId].info.entryFee - reward;
if(games[_roomId].info.entryFee != 0)
{
sendReward(getCorrectAddress(_player), reward, protocolsFee);
}
if(block.timestamp - userToRewardTimer[_player].lastTimeWon >= 1 days)
{
userToRewardTimer[_player].lastTimeWon = block.timestamp;
if(_enemyInteraction) increaseRankingInternal(_player, 220 + pointsReward);
else increaseRankingInternal(_player, 200 + pointsReward);
}
else
{
if(_enemyInteraction) increaseRankingInternal(_player, 170 + pointsReward);
else increaseRankingInternal(_player, 150 + pointsReward);
}
assignGameParticipationPoints(_roomId);
updateStatistics(_roomId, _player);
emit GameFinished(_roomId, classEnum(getPlayerClass(_roomId, _player)), getCorrectAddress(_player), reward);
}
/// @dev Finds the tile index on which given vehicle is currently located.
/// @param _roomId id of the game room
/// @param _vehicleId id of the vehicle
/// @param _player address of the player
/// @return uint8 tile index
function getVehicleTileIndex(uint256 _roomId, uint8 _vehicleId, address _player) internal view returns (uint8) {
for(uint i=0; i<TILE_COUNT; i++) {
if(games[_roomId].board[i].vehicle.id == games[_roomId].players[_player][_vehicleId].id && games[_roomId].board[i].isOccupied) {
return uint8(i);
}
}
return 99;
}
/// @dev Removes the root efect from the vehicle and updates the cooldowns.
/// @param _roomId id of the game room
/// @param _player address of the player
function checkRootAndCooldowns(uint256 _roomId, address _player) internal {
uint8 playerClass = getPlayerClass(_roomId, _player);
GameRoom storage game = games[_roomId];
for(uint8 i=playerClass * PIECE_PER_PLAYER; i < playerClass * PIECE_PER_PLAYER + PIECE_PER_PLAYER; i++) {
if(game.players[_player][i].isRooted) {
game.players[_player][i].isRooted = false;
emit VehicleUnrooted(_roomId, i);
}
}
if(game.cooldowns[_player].rootCooldown > 0) game.cooldowns[_player].rootCooldown--;
if(game.cooldowns[_player].rollAgainCooldown > 0) game.cooldowns[_player].rollAgainCooldown--;
if(game.cooldowns[_player].dashCooldown > 0) game.cooldowns[_player].dashCooldown--;
}
/// @dev Updates the queue (turn order), sets the turn timestamp for the next player, updates roots and cooldowns (see checkRootAndCooldowns).
/// @param _roomId id of the game room
function updateQueue(uint256 _roomId) internal {
address currentPlayer = games[_roomId].classToPlayer[classEnum(games[_roomId].queue)];
games[_roomId].diceRolls[currentPlayer].toBeUsed = false;
blockHashToBeUsed[currentPlayer] = 0;
games[_roomId].queue = (games[_roomId].queue + 1) % 4;
games[_roomId].info.moveTimestamp = arbSys.arbBlockNumber();
//root handling
checkRootAndCooldowns(_roomId, currentPlayer);
emit NewTurn(_roomId, classEnum(games[_roomId].queue));
}
/// @dev Checks if player has any possible moves. Mammoth function that checks all conditions being:
/// a) is vehicle on board and not rooted
/// b) can player take new vehicle from the base
/// c) will given move result in a collision with his own vehicle (which is prohibited)
/// @param _roomId id of the game room
/// @param _player address of the player
/// @param _diceRoll number of tiles to move
/// @return bool true if player can move, otherwise false
function canPlayerMove(uint256 _roomId, address _player, uint8 _diceRoll) public view returns (bool)
{
uint8[4] memory potentialShips;
uint8 counter = 0;
uint8 playerClass = getPlayerClass(_roomId, _player);
uint8 startingPoint = getPlayerStartingPoint(playerClass);
GameRoom storage game = games[_roomId];
for(uint8 i=playerClass * PIECE_PER_PLAYER; i< playerClass * PIECE_PER_PLAYER + PIECE_PER_PLAYER; i++)
{
if((game.players[_player][i].isOnBoard == true && !game.players[_player][i].isRooted) || (game.players[_player][i].isLapDone == false && game.players[_player][i].isOnBoard == false))
{
potentialShips[counter] = i;
counter++;
}
}
if(
((game.killCount < 2 && (_diceRoll == 6 || _diceRoll == 1)))
||
((game.killCount >= 2 && (_diceRoll == 12 || _diceRoll == 2)))
)
{
for(uint i=0; i<counter; i++)
{
if(game.players[_player][potentialShips[i]].isOnBoard == false &&
(
game.board[startingPoint].isOccupied == false ||
game.board[startingPoint].isOccupied && game.board[startingPoint].vehicle.class != classEnum(playerClass)
)
) return true;
}
}
for(uint i=0; i<TILE_COUNT; i++)
{
if(game.board[i].isOccupied)
{
for(uint j=0; j<counter; j++)
{
if(game.board[i].vehicle.id == potentialShips[j] && game.players[_player][potentialShips[j]].isOnBoard == true)
{
if((game.board[(i + _diceRoll) % 40].isOccupied == false || game.board[(i + _diceRoll) % 40].isOccupied && game.board[(i + _diceRoll) % 40].vehicle.class != classEnum(playerClass)) && howManyTilesLeft(getPlayerClass(_roomId, _player), getVehicleTileIndex(_roomId, potentialShips[j], _player)) >= _diceRoll) return true;
else if (isAboutToFinish(_roomId, potentialShips[j], _diceRoll, _player) && (game.board[startingPoint].isOccupied == false || (game.board[startingPoint].isOccupied && game.board[startingPoint].vehicle.class != classEnum(playerClass)))) return true;
}
}
}
}
return false;
}
/// @dev Allows to put another ship on board if _diceRoll is 6 or 1 (in case of rapid moves active, 12 or 2),
/// and if there is no other friendly ship on the field where ships are deployed (see: getPlayerStartingPoint).
/// Updates the queue unless the _diceRoll is 6 or 12 (in case of rapid moves active), which gives user additional move.
/// @param _roomId id of the game room
/// @param _vehicleId id of the vehicle
/// @param _diceRoll dice roll
function leaveBase(uint256 _roomId, uint8 _vehicleId, uint8 _diceRoll) internal
{
GameRoom storage room = games[_roomId];
bool rapidActive = room.killCount >= 2;
if(rapidActive) require(_diceRoll == 12 || _diceRoll == 2, "E22");
else require(_diceRoll == 6 || _diceRoll == 1, "E22");
require(room.players[msg.sender][_vehicleId].isOnBoard == false, "E23");
require(room.players[msg.sender][_vehicleId].isLapDone == false, "E24");
uint8 playerClass = getPlayerClass(_roomId, msg.sender);
uint8 startingPoint = getPlayerStartingPoint(playerClass);
if(room.board[startingPoint].isOccupied == true)
{
if(uint8(room.board[startingPoint].vehicle.class) == playerClass) revert("E25");
removeVehicleFromTile(_roomId, startingPoint);
increaseRankingInternal(msg.sender, 20);
emit VehicleDestroyed(_roomId, room.board[startingPoint].vehicle.id, skillsEnum(4));
}
room.players[msg.sender][_vehicleId].isOnBoard = true;
room.board[startingPoint].vehicle = games[_roomId].players[msg.sender][_vehicleId];
room.board[startingPoint].isOccupied = true;
room.diceRolls[msg.sender].toBeUsed = false;
if(!rapidActive && _diceRoll == 1 || rapidActive && _diceRoll == 2) updateQueue(_roomId);
else room.info.moveTimestamp = arbSys.arbBlockNumber();
emit VehicleLeftBase(_roomId, _vehicleId);
}
/// @dev Allows to use skill on the target tile, if the skill is available. DESTROY and ROOT can be used only on the tiles with enemy vehicles. DASH can be used on friendly vehicle, unless this would cause a collision with friendly vehicle, or crossing the finish line.
/// @param _roomId id of the game room
/// @param _skill skill to use
/// @param _targetTile id of target tile
function useSkill(uint256 _roomId, skillsEnum _skill, uint8 _targetTile) internal
skillAvailable(_roomId,_skill,_targetTile)
{
GameRoom storage room = games[_roomId];
Tile storage targetTile = room.board[_targetTile];
if(_skill == skillsEnum.DESTROY)
{
targetTile.isOccupied = false;
removeVehicleFromTile(_roomId, _targetTile);
room.cooldowns[msg.sender].destroyCooldown = DESTROY_COOLDOWN;
room.killCount++;
if(room.killCount == 2) emit RapidMoves(_roomId);
increaseRankingInternal(msg.sender, 20);
emit VehicleDestroyed(_roomId, targetTile.vehicle.id, skillsEnum.DESTROY);
}
else if(_skill == skillsEnum.ROOT)
{
games[_roomId].players[games[_roomId].classToPlayer[games[_roomId].board[_targetTile].vehicle.class]][games[_roomId].board[_targetTile].vehicle.id].isRooted = true;
room.cooldowns[msg.sender].rootCooldown = ROOT_COOLDOWN;
emit VehicleRooted(_roomId, targetTile.vehicle.id);
}
else //dash
{
room.cooldowns[msg.sender].dashCooldown = DASH_COOLDOWN;
if(room.board[(_targetTile + 1) % 40].isOccupied == true)
{
emit VehicleDashedAndDestroyed(_roomId, targetTile.vehicle.id, room.board[(_targetTile + 1) % 40].vehicle.id, _targetTile, (_targetTile + 1) % 40);
require(uint8(room.board[(_targetTile + 1) % 40].vehicle.class) != getPlayerClass(_roomId, msg.sender),"E25");
removeVehicleFromTile(_roomId, (_targetTile + 1) % 40);
if(isAboutToFinish(_roomId, games[_roomId].board[_targetTile].vehicle.id, 1, msg.sender))
{
games[_roomId].players[msg.sender][games[_roomId].board[_targetTile].vehicle.id].isOnBoard = false;
games[_roomId].players[msg.sender][games[_roomId].board[_targetTile].vehicle.id].isLapDone = true;
room.board[(_targetTile + 1) % 40].isOccupied == false;
emit LapFinished(_roomId, targetTile.vehicle.id);
if(!games[_roomId].info.hasEnded) assignWinner(_roomId, msg.sender, true);
}
else
{
room.board[(_targetTile + 1) % 40].vehicle = targetTile.vehicle;
increaseRankingInternal(msg.sender, 20);
}
}
else
{
emit VehicleDashed(_roomId, games[_roomId].board[_targetTile].vehicle.id, _targetTile, (_targetTile + 1) % 40);
if(isAboutToFinish(_roomId, games[_roomId].board[_targetTile].vehicle.id, 1, msg.sender))
{
games[_roomId].players[msg.sender][games[_roomId].board[_targetTile].vehicle.id].isOnBoard = false;
games[_roomId].players[msg.sender][games[_roomId].board[_targetTile].vehicle.id].isLapDone = true;
room.board[(_targetTile + 1) % 40].isOccupied == false;
emit LapFinished(_roomId, targetTile.vehicle.id);
if(!games[_roomId].info.hasEnded) assignWinner(_roomId, msg.sender, false);
}
else
{
room.board[(_targetTile + 1) % 40].vehicle = targetTile.vehicle;
room.board[(_targetTile + 1) % 40].isOccupied = true;
}
}
targetTile.isOccupied = false;
}
}
/// @dev Allows to move vehicle, optionally with skill casted on _targetTile (see: useSkill). Manages ROLLAGAIN skill, turn times, turn repetition, collisions, board state.
/// @param _roomId id of the game room
/// @param _vehicleId id of the vehicle to move
/// @param _diceRoll number of tiles to move
/// @param _skill skill to use (otherwise skillsEnum.NONE)
/// @param _targetTile id of tile skill is casted on (if _skill == skillsEnum.NONE, then _targetTile can be set to any value)
function moveVehicle(uint256 _roomId, uint8 _vehicleId, uint8 _diceRoll, skillsEnum _skill, uint8 _targetTile) internal
eligableVehicle(_roomId, _vehicleId, _diceRoll)
{
GameRoom storage room = games[_roomId];
bool rapidActive = room.killCount >= 2;
uint8 currentTileIndex = getVehicleTileIndex(_roomId, _vehicleId, msg.sender);
uint8 newTileIndex = (currentTileIndex + _diceRoll) % 40;
uint8 playerClass = getPlayerClass(_roomId, msg.sender);
Tile storage currentTile = room.board[currentTileIndex];
Tile storage newTile = room.board[newTileIndex];
bool repeat = false;
if(_skill != skillsEnum.NONE)
{
if(_skill == skillsEnum.ROLLAGAIN)
{
require(room.cooldowns[msg.sender].rollAgainCooldown == 0, "E33");
repeat = true;
room.cooldowns[msg.sender].rollAgainCooldown = ROLLAGAIN_COOLDOWN;
}
else useSkill(_roomId, _skill, _targetTile);
}
if(_skill == skillsEnum.DASH && _targetTile == currentTileIndex) {
if(newTileIndex == getPlayerStartingPoint(playerClass)) revert("E15");
currentTileIndex = (currentTileIndex + 1) % 40;
newTileIndex = (newTileIndex + 1) % 40;
currentTile = room.board[currentTileIndex];
newTile = room.board[newTileIndex];
require(newTile.vehicle.class != classEnum(playerClass) || newTile.isOccupied != true, "E25");
}
if(((!rapidActive && _diceRoll == 6 ) || (rapidActive && _diceRoll == 12)) && repeat == false) {
games[_roomId].info.moveTimestamp = arbSys.arbBlockNumber();
repeat = true;
}
if(newTile.isOccupied == true)
{
require(newTile.vehicle.class != classEnum(playerClass), "E25");
removeVehicleFromTile(_roomId, newTileIndex);
if(isAboutToFinish(_roomId, _vehicleId, _diceRoll, msg.sender))
{
room.players[msg.sender][_vehicleId].isOnBoard = false;
room.players[msg.sender][_vehicleId].isLapDone = true;
newTile.isOccupied = false;
emit VehicleMovedAndDestroyed(_roomId, currentTile.vehicle.id, currentTileIndex, newTileIndex, newTile.vehicle.id);
emit LapFinished(_roomId, _vehicleId);
if(!games[_roomId].info.hasEnded) assignWinner(_roomId, msg.sender, true);
}
else //enemy destroyed reward
{
emit VehicleMovedAndDestroyed(_roomId, currentTile.vehicle.id, currentTileIndex, newTileIndex, newTile.vehicle.id);
newTile.vehicle = room.players[msg.sender][_vehicleId];
increaseRankingInternal(msg.sender, 20); //kill
}
}
else
{
if(isAboutToFinish(_roomId, _vehicleId, _diceRoll, msg.sender))
{
room.players[msg.sender][_vehicleId].isOnBoard = false;
room.players[msg.sender][_vehicleId].isLapDone = true;
emit VehicleMoved(_roomId, currentTile.vehicle.id, currentTileIndex, newTileIndex);
emit LapFinished(_roomId, _vehicleId);
if(!games[_roomId].info.hasEnded) assignWinner(_roomId, msg.sender, false);
}
else
{
newTile.isOccupied = true;
newTile.vehicle = room.players[msg.sender][_vehicleId];
emit VehicleMoved(_roomId, currentTile.vehicle.id, currentTileIndex, newTileIndex);
}
}
currentTile.isOccupied = false;
if(_skill == skillsEnum.ROLLAGAIN)
{
emit PlayerUsedBonus(_roomId, classEnum(playerClass));
games[_roomId].diceRolls[msg.sender].diceResult = 0;
games[_roomId].info.moveTimestamp = arbSys.arbBlockNumber();
}
if(repeat == false)
{
updateQueue(_roomId);
}
else
{
room.diceRolls[msg.sender].toBeUsed = false;
}
}
/* EXTERNAL FUNCTIONS */
/// @dev Sends the emoji.
/// @param _roomId id of the game room
/// @param _type type of the emoji
function sendEmoji(uint256 _roomId, uint8 _type) external {
require(isPlayerInGame(_roomId, msg.sender), "E9");
classEnum class = classEnum(getPlayerClass(_roomId, msg.sender));
emit EmojiSent(_roomId, class, _type);
}
/// @dev Signals that player is no longer AFK, so his turnTime returns to normal.
/// @param _roomId id of the game room
function returnFromAFK(uint256 _roomId) external {
classEnum playerClass = classEnum(getPlayerClass(_roomId, msg.sender));
games[_roomId].afkRecord[playerClass] = false;
emit PlayerIsBack(_roomId, playerClass);
}
/// @dev Creates new game room with given stake (_entryFee), puts players ships on board, sets cooldowns, entry fees, isUserInGame mapping.
/// @param _entryFee entry fee for the game
function initiateGame(uint256 _entryFee) external payable
enoughFunds(_entryFee) contractWorking()
{
require(isUserInGame[msg.sender] == false, "E4");
GameRoom storage room = games.push();
room.info.playersCount = 1;
room.info.entryFee = _entryFee;
uint8 startingPoint = getPlayerStartingPoint(0);
for(uint8 i = 0; i<PIECE_PER_PLAYER; i++)
{
if(i == 0)
{
room.players[msg.sender][0] = Vehicle(0,true,false,false,true,classEnum(0));
room.board[startingPoint].isOccupied = true;
room.board[startingPoint].vehicle = room.players[msg.sender][0];
}
else if(i == 1)
{
room.players[msg.sender][1] = Vehicle(1,true,false,false,true,classEnum(0));
room.board[(startingPoint + 1) % 40].isOccupied = true;
room.board[(startingPoint + 1) % 40].vehicle = room.players[msg.sender][1];
}
else
room.players[msg.sender][i] = Vehicle(i,false,false,false,true,classEnum(0));
}
room.cooldowns[msg.sender] = SkillsCooldown(0,0,0,0);
room.classToPlayer[classEnum(0)] = msg.sender;
room.cooldowns[msg.sender].rootCooldown = 1;
isUserInGame[msg.sender] = true;
emit RoomStateChanged(games.length - 1, 1);
}
/// @dev Joins the game with given id (_roomId), puts players ships on board, sets cooldowns, isUserInGame mapping, updates room statistics, and in case of last player, sets turn time.
/// @param _roomId id of the game room
/// @return counter id of the player in room
function joinGame(uint256 _roomId) external payable
joinableGame(_roomId)
returns (uint256)
{
GameRoom storage room = games[_roomId];
require(msg.value == room.info.entryFee, "E2");
uint8 counter = getMissingPlayerIndex(_roomId);
uint8 startingPoint = getPlayerStartingPoint(counter);
for(uint8 i=counter * PIECE_PER_PLAYER; i<counter * PIECE_PER_PLAYER + PIECE_PER_PLAYER; i++)
{
if(i == counter * PIECE_PER_PLAYER)
{
room.players[msg.sender][i] = Vehicle(i,true,false,false,true,classEnum(counter));
room.board[startingPoint].isOccupied = true;
room.board[startingPoint].vehicle = room.players[msg.sender][i];
}
else if(i == counter * PIECE_PER_PLAYER + 1)
{
room.players[msg.sender][i] = Vehicle(i,true,false,false,true,classEnum(counter));
room.board[(startingPoint + 1) % 40].isOccupied = true;
room.board[(startingPoint + 1) % 40].vehicle = room.players[msg.sender][i];
}
else
room.players[msg.sender][i] = Vehicle(i,false,false,false,true,classEnum(counter));
}
room.cooldowns[msg.sender] = SkillsCooldown(0,0,0,0);
room.classToPlayer[classEnum(counter)] = msg.sender;
room.info.playersCount++;
room.cooldowns[msg.sender].rootCooldown = 1;
isUserInGame[msg.sender] = true;
emit RoomStateChanged(_roomId, room.info.playersCount);
if(room.info.playersCount == 4)
{
room.info.hasStarted = true;
emit GameStarted(_roomId);
room.info.moveTimestamp = arbSys.arbBlockNumber() + 2 * turnTime;
}
return counter;
}
/// @dev Leaves game room, returns entry fees, removes players ships from board, updates cooldowns, isUserInGame mapping, and room statistics.
/// @param _roomId id of the game room
function leaveGame(uint256 _roomId) external
playerCanLeave(_roomId)
{
GameRoom storage room = games[_roomId];
uint8 playerClass = getPlayerClass(_roomId, msg.sender);
uint8 startingPoint = getPlayerStartingPoint(playerClass);
for(uint8 i=playerClass * PIECE_PER_PLAYER; i< playerClass * PIECE_PER_PLAYER + PIECE_PER_PLAYER; i++)
{
if(i == playerClass * PIECE_PER_PLAYER)
{
room.players[msg.sender][i].isOnBoard = false;
room.board[startingPoint].isOccupied = false;
}
else if(i == playerClass * PIECE_PER_PLAYER + 1)
{
room.players[msg.sender][i].isOnBoard = false;
room.board[(startingPoint + 1) % 40].isOccupied = false;
}
room.players[msg.sender][i].isInitialized = false;
}
room.info.playersCount--;
isUserInGame[msg.sender] = false;
room.classToPlayer[classEnum(playerClass)] = address(0);
room.cooldowns[msg.sender].rootCooldown = 0;
if(room.info.entryFee > 0)
{
(bool sent,) = msg.sender.call{value: room.info.entryFee}("");
require(sent, "E21");
}
emit RoomStateChanged(_roomId, room.info.playersCount);
}
/// @dev Handles commit reveal scheme for dice rolls based on arbitrum block number. First user interaction is commit, second is reveal. Commit is available for 256 blocks for security reasons.
/// @param _roomId id of the game room
function rollDice(uint256 _roomId) external
eligablePlayer(_roomId)
playersTurn(_roomId)
gameStarted(_roomId)
{
DiceRoll storage rolls = games[_roomId].diceRolls[msg.sender];
require(rolls.toBeUsed == false || ((rolls.diceResult == 6 && games[_roomId].killCount < 2) || (rolls.diceResult == 12 && games[_roomId].killCount >= 2) && rolls.toBeUsed == false) || rolls.diceResult == 0, "E26");
if(blockHashToBeUsed[msg.sender] == 0 || arbSys.arbBlockNumber() > blockHashToBeUsed[msg.sender] + 252) //rolling the dice
{
blockHashToBeUsed[msg.sender] = arbSys.arbBlockNumber() + 1;
return;
}
bytes32 blockHash = arbSys.arbBlockHash(blockHashToBeUsed[msg.sender]);
if(blockHash == bytes32(0)) revert("E27");
uint256 rand = uint256(blockHash);
blockHashToBeUsed[msg.sender] = 0;
uint8 diceResult = uint8(rand % 6) + 1;
if(games[_roomId].killCount >= 2) rolls.diceResult = 2 * diceResult;
else rolls.diceResult = diceResult;
rolls.toBeUsed = true;
emit DiceRolled(classEnum(getPlayerClass(_roomId, msg.sender)), _roomId, diceResult);
}
/// @dev Handles player move logic. Checks if player can move, if so, moves him (optionally with the usage of certrain _skill). Skills can be used only while moving (_leaveBase = false).
/// @param _roomId id of the game room
/// @param _vehicleId id of the vehicle to be moved or taken out of base
/// @param _skill skill to be used
/// @param _targetTile id of the tile where skill will be used
/// @param _leaveBase if true and other conditions met (see leaveBase), vehicle will be taken out of base, otherwise it will be moved
function makeMove(uint256 _roomId, uint8 _vehicleId, uint8 _skill, uint8 _targetTile, bool _leaveBase) external
diceRolled(_roomId)
{
skillsEnum skill = skillsEnum(_skill);
uint8 diceResult = games[_roomId].diceRolls[msg.sender].diceResult;
require(games[_roomId].players[msg.sender][_vehicleId].isInitialized, "E18");
if(_skill == 2 && ((games[_roomId].killCount < 2 && diceResult == 6) || diceResult == 12)) revert("E28");
if(canPlayerMove(_roomId, msg.sender, diceResult))
{
if(_leaveBase) leaveBase(_roomId, _vehicleId, diceResult);
else
{
moveVehicle(_roomId, _vehicleId, diceResult, skill, _targetTile);
}
}
else updateQueue(_roomId);
}
/// @dev Function switches the game queue to the next player if the current player is out of time for making move.
/// @param _roomId id of the game room
function hurryPlayer(uint256 _roomId) external {
GameRoom storage room = games[_roomId];
if(!room.afkRecord[classEnum(room.queue)])
{
require(arbSys.arbBlockNumber() >= games[_roomId].info.moveTimestamp + turnTime, "E29");
room.afkRecord[classEnum(room.queue)] = true;
emit PlayerIsAFK(_roomId, classEnum(room.queue));
}
updateQueue(_roomId);
}
/* EXTERNAL VIEW FUNCTIONS */
/// @dev Returns record of players that are AFK
/// @param _roomId id of the game room
/// @return array of AFK players
function getAfkPlayers(uint256 _roomId) external view returns (bool[4] memory) {
bool[4] memory afkPlayers;
for(uint i=0;i<4;i++)
{
if(games[_roomId].afkRecord[classEnum(i)] == true) afkPlayers[i] = true;
else afkPlayers[i] = false;
}
return afkPlayers;
}
/// @dev Returns array of 40 tiles representing the board state.
/// @param _roomId id of the game room
/// @return array of 40 tiles representing the board state.
function getBoardState(uint256 _roomId) external view returns (Tile[40] memory)
{
return (games[_roomId].board);
}
/// @dev Returns the information about the room for specified player: skills cooldowns, current player id to move in gameroom, dice result for specified player, rapid moves state
/// @param _roomId id of the game room
/// @param _player address of the player
/// @return SkillsCooldown struct, uint8 current player id to move in gameroom, uint8 dice result for specified player, bool rapid moves state
function getRoomInfo(uint256 _roomId, address _player) external view returns (SkillsCooldown memory, uint8, uint8, bool)
{
uint8 diceResult = 0;
bool rapidMoves = games[_roomId].killCount >= 2;
if(games[_roomId].diceRolls[_player].toBeUsed)
{
if(rapidMoves) diceResult = games[_roomId].diceRolls[_player].diceResult / 2;
else diceResult = games[_roomId].diceRolls[_player].diceResult;
}
return (games[_roomId].cooldowns[_player], games[_roomId].queue, diceResult, rapidMoves);
}
/// @dev Returns the ids of the ships that are in the base
/// @param _roomId id of the game room
/// @return uint8[] array of the ids of ships in base
function getShipsInBase(uint256 _roomId) external view returns (uint8[] memory)
{
uint8[16] memory shipsInBase;
uint8 counter=0;
for(uint8 i=0;i<MAX_PLAYERS;i++)
{
for(uint8 j=i*PIECE_PER_PLAYER; j < i*PIECE_PER_PLAYER + PIECE_PER_PLAYER;j++)
{
if(games[_roomId].players[games[_roomId].classToPlayer[classEnum(i)]][j].isLapDone)