Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sinadmode #713

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open

Sinadmode #713

wants to merge 5 commits into from

Conversation

22Chaos
Copy link

@22Chaos 22Chaos commented Oct 21, 2024

This pull-request introduces Sinad mode, a toggle-able setting in the menu that modifies the mission sizes of 6 player Avalon. If mission 1 and 2 both succeed, and mission 3 fails, and if mission 3 was not a superset of m2, (i.e. m3!=m2+1)
The mission size for Mission 4 becomes a 4-man team, and the final mission 5 becomes a 3-man team.

The author hopes that this mode will make 6p games more interesting.

[3, 4, 4, 5, 5],
[3, 4, 4, 5, 5],
[3, 4, 4, 5, 5],
];
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you keep this here but instead conditionally use the sinad versions in the pickingTeam phase?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, alternatively, if you want to have the GUI update live, you can directly mutate this array when you need to. Const arrays can still be modified.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about keeping two global arrays, and having a reference to one of the two arrays as a field of the Game class? updateMissionSizesForSinad() can then update that reference to point to the other array.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, actually, something I didn't think about was that this const object is shared across all games, so modifying it directly will cause issues. We'll need to either do what you did and move it onto the Game object itself as a member, or we'll have to make a static function that is given sinadMode and returns one or the other.

I might slightly prefer your current solution. Let me think a bit :)

@@ -566,6 +570,9 @@ class Game extends Room {
if (this.disableVoteHistory) {
this.sendText('The game has vote history disabled.', 'gameplay-text');
}
if (this.enableSinadMode) {
this.sendText('The game has Sinad Mode enabled.', 'gameplay-text');
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also check that enableSinadMode can only be true if it's 6P here?
https://github.com/vck3000/ProAvalon/blob/master/src/gameplay/game.ts#L362-L378

This way if the host misconfigures the game, the server will tell them when they try to start the game.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My current solution to this.

I wanted to make this check inside static checkOptions(options: string[]), but I wasn't sure because it looks like the options array only has Role enum members, and I think startGame(options: string[]) relies on this. Alternatively, I could try making checkOptions non-static to access this.enableSinadMode directly, but looks tricky.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Feel free to make that non-static so you can put it together.

The design of Room and Game is pretty poor. Ideally these kinds of checks are done within the Game.

@@ -270,6 +275,61 @@ class VotingMission implements IPhase {
getProhibitedIndexesToPick(indexOfPlayer: number): number[] {
return [];
}

updateMissionSizesForSinad(): void {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check for Sinad can actually be done as a separate system, but it's not currently a thing.

https://github.com/vck3000/ProAvalon/blob/master/src/gameplay/game.ts#L758-L809

See this section^. Essentially, every role and card gets an opportunity to move the game into a different phase/state after each phase has acted in this function call: checkRoleCardSpecialMoves().

We can generalise this behavior to non-role and non-card specific actions such as a game setting like Sinad. If you're interested we can pursue this. Alternatively, happy for you to leave this implementation here for now.

this.thisRoom.missionHistory[1] === 'succeeded' &&
this.thisRoom.missionHistory[2] === 'failed' &&
this.thisRoom.missionNum === 4 &&
this.thisRoom.pickNum === 1
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have a separate boolean tracking whether this has activated. This will help us avoid duplicate runs into this function. Although this code is only reached when a mission has voted, it'd be simpler and safer to just check for m1 success, m2 success, m3 fail, and then sinad hasn't activated yet to enter this function.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a new method and field to track this. Hope this works.

Comment on lines 308 to 324
for (const player in VH) {
if (VH.hasOwnProperty(player)) {

const playerVH = VH[player];
const playerM2 = playerVH[1][playerVH[1].length -1];
const playerM3 = playerVH[2][playerVH[2].length -1];

if (typeof playerM2 === 'string' && playerM2.includes("VHpicked")) {
setOfPlayersOnM2.add(player);
}
if (typeof playerM3 === 'string' && playerM3.includes("VHpicked")) {
setOfPlayersOnM3.add(player);
}
}
}

let isM2ASubsetOfM3: boolean = [...setOfPlayersOnM2].every(player => setOfPlayersOnM3.has(player));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's pull this code out to another file. Let's also add some tests.

E.g. I quickly spun this up. Should be good reference :).
#714

Note I cheated a bit and used a node v22 feature (Set.difference()). Please find another solution for this, like the one you have here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrote function isSubsetOf(setA, setB) in game.ts. Test case passes.

- Split above func into getPlayersOnMission and subsetOf.
- Room now prevents starting sinad mode without 6p.
- Test cases are WIP.
- rename NUMS_PLAYER_ON_MISSION to camelCase
- bug fix in isSubsetOf func
- add more sinad testcases for new funcs.
- refactor sinad test cases
- more isSubsetOf test cases
- cleanup comments
- better formatting
- BUILD ERROR. DO NOT MERGE.
Comment on lines +117 to +121
game.voteHistory = vh;
game.enableSinadMode = true;
game.playersInGame = ['Piplup', 'Torchic', 'Mudkip', 'Turtwig', 'MewTwo', 'Charmander'];
game.gameMode = GameMode.AVALON;
game.missionHistory = ['succeeded','succeeded','failed'];
Copy link
Author

@22Chaos 22Chaos Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build error:

Property 'voteHistory' does not exist on type 'Game'.
Property 'enableSinadMode' does not exist on type 'Game'.

Not sure why this would happen, since the other fields (missionHistory, playersInGame) did not throw this error.
What's weirder is, $ yarn test works perfectly fine with this. For instance if you change game.enableSinadMode to false here, then yarn test will throw an error.

this.roomCreationType = gameConfig.roomCreationType;
this.getTimeFunc = gameConfig.getTimeFunc;

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add this back in


getPlayersOnMission(missionNum: number): Set<string> {
const set = new Set<string>();
const VH = this.voteHistory; //brevity
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const VH = this.voteHistory; //brevity
const vh = this.voteHistory;

@@ -2283,8 +2300,73 @@ class Game extends Room {
}
return newRating;
}

getPlayersOnMission(missionNum: number): Set<string> {
const set = new Set<string>();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const set = new Set<string>();
const playersOnMission = new Set<string>();

const VH = this.voteHistory; //brevity

if(VH === undefined) {
return set; // empty set.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need comment

}

for (const player in VH) {
//TODO: write check to ensure player really is in this.PlayersInGame.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ok, we can assume that the players in VH are actual players in the game.

Can remove this comment.


for (const player in VH) {
//TODO: write check to ensure player really is in this.PlayersInGame.
if (VH.hasOwnProperty(player)) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you need this if statement?

//TODO: write check to ensure player really is in this.PlayersInGame.
if (VH.hasOwnProperty(player)) {
if(VH[player].length < missionNum) {
//in case mission hasn't happened yet.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//in case mission hasn't happened yet.
// If the mission hasn't happened yet.

return set;
}

const missionNumVH = VH[player][missionNum - 1];
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const missionNumVH = VH[player][missionNum - 1];
const missionVh = VH[player][missionNum - 1];

@@ -2283,8 +2300,73 @@ class Game extends Room {
}
return newRating;
}

getPlayersOnMission(missionNum: number): Set<string> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
getPlayersOnMission(missionNum: number): Set<string> {
getPlayersWhoWentOnMission(missionNum: number): Set<string> {

For some extra little bit of clarity

this.getPlayersOnMission(2)
,this.getPlayersOnMission(3)
)) {
this.numPlayersOnMission[6 - MIN_PLAYERS] = [2,3,4,4,3];
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you run prettier to format the file pls?

https://prettier.io/docs/en/cli.html

It should be installed when you run yarn, but if not, do yarn prettier . --write

Comment on lines +2334 to +2336
console.log(this.getPlayersOnMission(2)) ;
console.log(this.getPlayersOnMission(3)) ;
console.log(!isSubsetOf(this.getPlayersOnMission(2),this.getPlayersOnMission(3)));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please clear these guys out when you're done debugging.

Comment on lines +159 to +160
if(!this.thisRoom.hasSinadRun && this.thisRoom.shouldSinadRun()) {
this.thisRoom.updateMissionSizesSinad();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you rename this to this.thisRoom.checkSinad(), and then move the hasSinadRun check, and shouldSinadRun check into checkSinad()?

That way it's a single entrypoint, making it easier for clients to use.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then you can remove this.thisRoom.hasSinadRun = true; below as well

this.hasSinadRun = true;
}

hasSinadRun: boolean = false;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move this to the top of this class with the other members

@@ -2310,6 +2392,20 @@ function generateAssignmentOrders(num) {
return rolesAssignment;
}

export function isSubsetOf(setA, setB): boolean {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide the types here for setA and setB please

@@ -2283,8 +2300,73 @@ class Game extends Room {
}
return newRating;
}

getPlayersOnMission(missionNum: number): Set<string> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make this method private please. Outside code shouldn't call into this.

expect(mockGame.shouldSinadRun()).toEqual(false);
});

it('should not run if m1 failed', () => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move these tests to the existing game.test.ts?

That way you don't have to use mocks and you can get better test coverage.

Comment on lines +179 to +191
it('should pass these Sets declared the old fashioned way', () => {
const setA = new Set<string>(['a', 'b', 'c']);
const setB = new Set<string>(['a', 'd', 'c','b']);
expect(isSubsetOf(setA,setB)).toEqual(true);
const setC = new Set<string>(['a','e','d','c']);
expect(isSubsetOf(setA,setC)).toEqual(false);
expect(isSubsetOf(undefined,setC)).toEqual(false);
expect(isSubsetOf(setA,undefined)).toEqual(false);
expect(isSubsetOf(new Set<String>(),undefined)).toEqual(false);
expect(isSubsetOf(undefined,new Set<String>())).toEqual(false);
expect(isSubsetOf(undefined, undefined)).toEqual(false);

});
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should test these through the Game's API (i.e. at a higher level) in game.test.ts.

If you want to test isSubsetOf() specifically like you've done here, please split that function to a separate file, and move these tests to just isSubsetOf.test.ts or similar.

@@ -1933,6 +1939,14 @@ function updateRoomDisableVoteHistory(disableVoteHistory) {
}
}

function updateRoomEnableSinadMode(enableSinadMode) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function updateRoomEnableSinadMode(enableSinadMode) {
function updateRoomEnableSinadMode(enableSinadMode: boolean) {

@@ -41,6 +41,7 @@ const gameRecordSchema = new mongoose.Schema({
numFailsHistory: [Number],
voteHistory: Object,
disableVoteHistory: Boolean,
enableSinadMode: Boolean,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you add this to the game finish method in game.ts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants