Skip to content

Commit

Permalink
63 do not wait for idle players (#84)
Browse files Browse the repository at this point in the history
* added ability to end turns if players are stale

* changeset and comment clean up

* linter
  • Loading branch information
peersky authored Nov 27, 2024
1 parent c53987d commit 26bcabd
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 27 deletions.
40 changes: 40 additions & 0 deletions .changeset/sour-camels-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
'rankify-contracts': minor
---

# Changeset Summary

## Overview
Added ability to end turns if there are inactive players without waiting for their move.

## Changes

### ArguableVotingTournament.sol
- Increased the size of `RankifyInstanceMainFacetSelectors` from 27 to 28.
- Added a new function selector `RankifyInstanceMainFacet.isActive.selector`.

### RankifyInstanceMainFacet.sol
- Added a new function `isActive` which takes a `gameId` and a `player` address and returns a boolean indicating if the game is active for the player.

### LibQuadraticVoting.sol
- Changed the parameter name from `voterVoted` to `isActive` in the `computeScoresByVPIndex` function.
- Moved the initialization of `notVotedGivesEveryone` to use `q.maxQuadraticPoints`.
- Updated the condition to check `!isActive[vi]` instead of `!voterVoted[vi]`.

### LibTurnBasedGame.sol
- Added a new `isActive` mapping to track active players.
- Introduced `numActivePlayers` to count the number of active players.
- Updated the `resetGame` function to initialize `isActive` to `false` for all players and reset `numActivePlayers`.
- Modified `addPlayer` to initialize `isActive` to `false` for new participants.
- Enhanced `canEndTurnEarly` to check if all active players have made their move before allowing an early turn end.
- Removed out the `_clearCurrentMoves` function
- Updated the `startGame` function to set all players as active initially.
- Modified `recordMove` to mark a player as active when they make a move and increment `numActivePlayers`.

## Summary of Changes
- **Functionality Enhancements**: Added a new `isActive` function in `RankifyInstanceMainFacet.sol` to check the active status of a game for a specific player.
- **Refactoring**: Renamed parameters and adjusted logic in `LibQuadraticVoting.sol` to align with the new active status checking mechanism.
- **Code Organization**: Updated selectors in `ArguableVotingTournament.sol` to accommodate the new functionality.
- **Game Management Enhancements**: Introduced active player tracking and management in `LibTurnBasedGame.sol`, enhancing game state management and turn-based logic.

These changes introduce new functionality to check the active status of a game, which likely impacts how games are managed and interacted with in your application.
3 changes: 2 additions & 1 deletion src/distributions/ArguableVotingTournament.sol
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ contract ArguableVotingTournament is InitializedDiamondDistribution {
action: IDiamondCut.FacetCutAction.Add,
functionSelectors: EIP712InspectorFacetSelectors
});
bytes4[] memory RankifyInstanceMainFacetSelectors = new bytes4[](27);
bytes4[] memory RankifyInstanceMainFacetSelectors = new bytes4[](28);
RankifyInstanceMainFacetSelectors[0] = RankifyInstanceMainFacet.cancelGame.selector;
RankifyInstanceMainFacetSelectors[1] = RankifyInstanceMainFacet.gameCreator.selector;
RankifyInstanceMainFacetSelectors[2] = RankifyInstanceMainFacet.createGame.selector;
Expand Down Expand Up @@ -141,6 +141,7 @@ contract ArguableVotingTournament is InitializedDiamondDistribution {
RankifyInstanceMainFacetSelectors[24] = RankifyInstanceMainFacet.getPlayerVotedArray.selector;
RankifyInstanceMainFacetSelectors[25] = RankifyInstanceMainFacet.getPlayersMoved.selector;
RankifyInstanceMainFacetSelectors[26] = RankifyInstanceMainFacet.estimateGamePrice.selector;
RankifyInstanceMainFacetSelectors[27] = RankifyInstanceMainFacet.isActive.selector;

facetCuts[2] = IDiamondCut.FacetCut({
facetAddress: address(_RankifyMainFacet),
Expand Down
4 changes: 4 additions & 0 deletions src/facets/RankifyInstanceMainFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -382,4 +382,8 @@ contract RankifyInstanceMainFacet is
}
return (playersMoved, game.numPlayersMadeMove);
}

function isActive(uint256 gameId, address player) public view returns (bool) {
return gameId.isActive(player);
}
}
8 changes: 4 additions & 4 deletions src/libraries/LibQuadraticVoting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ library LibQuadraticVoting {
}

/**
* @dev Computes the scores for each proposal by voter preference index. `q` is the precomputed quadratic voting values. `VotersVotes` is a 2D array of votes, where each row corresponds to a voter and each column corresponds to a proposal. `voterVoted` is an array indicating whether each voter has voted. `notVotedGivesEveyone` is the number of points to distribute to each proposal for each voter that did not vote. `proposalsLength` is the number of proposals.
* @dev Computes the scores for each proposal by voter preference index. `q` is the precomputed quadratic voting values. `VotersVotes` is a 2D array of votes, where each row corresponds to a voter and each column corresponds to a proposal. `isActive` is an array indicating whether each voter has voted. `notVotedGivesEveyone` is the number of points to distribute to each proposal for each voter that did not vote. `proposalsLength` is the number of proposals.
*
* Returns:
*
Expand All @@ -62,10 +62,10 @@ library LibQuadraticVoting {
function computeScoresByVPIndex(
qVotingStruct memory q,
uint256[][] memory VotersVotes,
bool[] memory voterVoted,
uint256 notVotedGivesEveryone,
bool[] memory isActive,
uint256 proposalsLength
) internal pure returns (uint256[] memory) {
uint256 notVotedGivesEveryone = q.maxQuadraticPoints;
uint256[] memory scores = new uint256[](proposalsLength);
uint256[] memory creditsUsed = new uint256[](VotersVotes.length);

Expand All @@ -75,7 +75,7 @@ library LibQuadraticVoting {
for (uint256 vi = 0; vi < VotersVotes.length; vi++) {
// For each potential voter
uint256[] memory voterVotes = VotersVotes[vi];
if (!voterVoted[vi]) {
if (!isActive[vi]) {
// Check if voter wasn't voting
scores[proposalIdx] += notVotedGivesEveryone; // Gives benefits to everyone but himself
creditsUsed[vi] = q.voteCredits;
Expand Down
5 changes: 2 additions & 3 deletions src/libraries/LibRankify.sol
Original file line number Diff line number Diff line change
Expand Up @@ -530,14 +530,13 @@ library LibRankify {
uint256[] memory scores = new uint256[](players.length);
bool[] memory playerVoted = new bool[](players.length);
GameState storage game = getGameState(gameId);
// Convert mappiing to array to pass it to libQuadratic
// Convert mapping to array to pass it to libQuadratic
for (uint256 i = 0; i < players.length; ++i) {
playerVoted[i] = game.playerVoted[players[i]];
playerVoted[i] = gameId._getState().isActive[players[i]];
}
uint256[] memory roundScores = game.voting.computeScoresByVPIndex(
votesRevealed,
playerVoted,
game.voting.maxQuadraticPoints,
proposerIndices.length
);
for (uint256 playerIdx = 0; playerIdx < players.length; playerIdx++) {
Expand Down
68 changes: 49 additions & 19 deletions src/libraries/LibTurnBasedGame.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ library LibTBG {
bool hasEnded;
EnumerableSet.AddressSet players;
mapping(address => bool) madeMove;
mapping(address => bool) isActive;
uint256 numPlayersMadeMove;
uint256 numActivePlayers;
mapping(address => uint256) score;
bool isOvertime;
address[] leaderboard;
Expand Down Expand Up @@ -168,6 +170,7 @@ library LibTBG {
for (uint256 i = 0; i < players.length; ++i) {
tbg.instances[gameId].state.score[players[i]] = 0;
tbg.instances[gameId].state.madeMove[players[i]] = false;
tbg.instances[gameId].state.isActive[players[i]] = false;
}
delete tbg.instances[gameId].state.currentTurn;
delete tbg.instances[gameId].state.hasEnded;
Expand All @@ -178,6 +181,7 @@ library LibTBG {
delete tbg.instances[gameId].state.players;
delete tbg.instances[gameId].state.registrationOpenAt;
delete tbg.instances[gameId].state.turnStartedAt;
delete tbg.instances[gameId].state.numActivePlayers;
}

/**
Expand Down Expand Up @@ -221,6 +225,7 @@ library LibTBG {
require(canBeJoined(gameId), "addPlayer->cant join now");
state.players.add(participant);
state.madeMove[participant] = false;
state.isActive[participant] = false;
tbg.playerInGame[participant] = gameId;
}

Expand Down Expand Up @@ -334,10 +339,16 @@ library LibTBG {
*/
function canEndTurnEarly(uint256 gameId) internal view returns (bool) {
State storage state = _getState(gameId);
bool everyoneMadeMove = (state.numPlayersMadeMove) == state.players.length() ? true : false;
if (!state.hasStarted || isGameOver(gameId)) return false;
if (everyoneMadeMove || canEndTurn(gameId)) return true;
return false;

uint256 activePlayersNotMoved = 0;
address[] memory players = state.players.values();
for (uint256 i = 0; i < players.length; i++) {
if (state.isActive[players[i]] && !state.madeMove[players[i]]) {
activePlayersNotMoved++;
}
}
return activePlayersNotMoved == 0 || canEndTurn(gameId);
}

/**
Expand All @@ -357,21 +368,6 @@ library LibTBG {
_;
}

/**
* @dev Clears the current moves in a game. `state` is the State.
*
* Modifies:
*
* - Sets the madeMove of each player in `game` to false.
*/
function _clearCurrentMoves(State storage state) internal {
for (uint256 i = 0; i < state.players.length(); ++i) {
address player = state.players.at(i);
state.madeMove[player] = false;
}
state.numPlayersMadeMove = 0;
}

/**
* @dev Resets the states of the players in a game. `State` is the state.
*
Expand Down Expand Up @@ -520,6 +516,14 @@ library LibTBG {
state.turnStartedAt = block.timestamp;
state.startedAt = block.timestamp;
_resetPlayerStates(state);

// Initialize all players as active
uint256 playerCount = state.players.length();
state.numActivePlayers = playerCount;
for (uint256 i = 0; i < playerCount; i++) {
address player = state.players.at(i);
state.isActive[player] = true;
}
}

/**
Expand Down Expand Up @@ -666,6 +670,10 @@ library LibTBG {
require(gameId == tbg.playerInGame[player], "is not in the game");
state.madeMove[player] = true;
state.numPlayersMadeMove += 1;

// Set player as active when they make a move
state.isActive[player] = true;
state.numActivePlayers++;
}

function isPlayerTurnComplete(uint256 gameId, address player) internal view returns (bool) {
Expand Down Expand Up @@ -733,7 +741,6 @@ library LibTBG {
function nextTurn(uint256 gameId) internal returns (bool, bool, bool) {
require(canEndTurnEarly(gameId), "nextTurn->CanEndEarly");
State storage state = _getState(gameId);
_clearCurrentMoves(state);
state.currentTurn += 1;
state.turnStartedAt = block.timestamp;
bool _isLastTurn = isLastTurn(gameId);
Expand All @@ -743,6 +750,24 @@ library LibTBG {
}
state.hasEnded = isGameOver(gameId);

// Update player activity status for next turn
uint256 playerCount = state.players.length();
state.numActivePlayers = 0;

for (uint256 i = 0; i < playerCount; i++) {
address player = state.players.at(i);
// If player didn't make a move this turn, mark them as inactive
if (!state.madeMove[player]) {
// console.log('LibTBG::nextTurn - ','player inactive!');
state.isActive[player] = false;
} else {
// console.log('LibTBG::nextTurn - ','player active!');
state.numActivePlayers++;
}
state.madeMove[player] = false;
}
state.numPlayersMadeMove = 0;

(state.leaderboard, ) = sortByScore(gameId);
return (_isLastTurn, state.isOvertime, state.hasEnded);
}
Expand Down Expand Up @@ -927,4 +952,9 @@ library LibTBG {
_quickSort(players, scores, 0, int256(scores.length - 1));
return (players, scores);
}

function isActive(uint256 gameId, address player) internal view returns (bool) {
State storage state = _getState(gameId);
return state.isActive[player];
}
}
68 changes: 68 additions & 0 deletions test/RankifyInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,74 @@ describe(scriptName, () => {
beforeEach(async () => {
await startGame(1);
});
it('Can finish turn early if previous turn participant did not made a move', async () => {
proposalsStruct = await mockValidProposals(
getPlayers(adr, RInstanceSettings.RInstance_MIN_PLAYERS),
rankifyInstance,
adr.gameMaster1,
1,
false,
);
for (let i = 0; i < proposalsStruct.length; i++) {
const { params } = proposalsStruct[i];
if (i !== 0) {
await rankifyInstance.connect(adr.gameMaster1.wallet).submitProposal(params);
} else {
console.log('skipped proposal', i);
}
}

await time.increase(Number(RInstanceSettings.RInstance_TIME_PER_TURN) + 1);
votes = [];
await endTurn(1, rankifyInstance);
proposalsStruct = await mockValidProposals(
getPlayers(adr, RInstanceSettings.RInstance_MIN_PLAYERS),
rankifyInstance,
adr.gameMaster1,
1,
false,
);
for (let i = 0; i < proposalsStruct.length; i++) {
const { params } = proposalsStruct[i];
if (i !== 0) {
await rankifyInstance.connect(adr.gameMaster1.wallet).submitProposal(params);
}
}
votes = await mockValidVotes(
getPlayers(adr, RInstanceSettings.RInstance_MIN_PLAYERS),
rankifyInstance,
1,
adr.gameMaster1,
false,
);
const players = getPlayers(adr, RInstanceSettings.RInstance_MIN_PLAYERS);
for (let i = 0; i < votes.length; i++) {
if (i !== 0) {
await rankifyInstance
.connect(adr.gameMaster1.wallet)
.submitVote(1, votes[i].voteHidden, players[i].wallet.address);
}
}

await time.increase(Number(RInstanceSettings.RInstance_TIME_PER_TURN) + 1);
await expect(endTurn(1, rankifyInstance)).to.emit(rankifyInstance, 'TurnEnded');
await mockValidVotes(
getPlayers(adr, RInstanceSettings.RInstance_MIN_PLAYERS),
rankifyInstance,
1,
adr.gameMaster1,
true,
);
expect(await rankifyInstance.isActive(1, proposalsStruct[0].params.proposer)).to.be.false;
proposalsStruct = await mockValidProposals(players, rankifyInstance, adr.gameMaster1, 1, false);
for (let i = 0; i < proposalsStruct.length; i++) {
if (i !== 1) {
await rankifyInstance.connect(adr.gameMaster1.wallet).submitProposal(proposalsStruct[i].params);
}
}
expect(await rankifyInstance.isActive(1, proposalsStruct[0].params.proposer)).to.be.true;
await expect(endTurn(1, rankifyInstance)).to.be.revertedWith('nextTurn->CanEndEarly');
});
it('First turn has started', async () => {
expect(await rankifyInstance.connect(adr.player1.wallet).getTurn(1)).to.be.equal(1);
});
Expand Down

0 comments on commit 26bcabd

Please sign in to comment.