Skip to content

Commit

Permalink
✨ feat: add empty hand action to 4 and 6 players games
Browse files Browse the repository at this point in the history
  • Loading branch information
omidnikrah committed Dec 31, 2024
1 parent 4710359 commit b318c01
Show file tree
Hide file tree
Showing 26 changed files with 276 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { GameState, HandPosition, Player } from '@gol-ya-pooch/shared';
import { IsString } from 'class-validator';
import { IsBoolean, IsOptional, IsString } from 'class-validator';

export class GuessObjectLocationDTO {
@IsString()
Expand All @@ -10,4 +10,8 @@ export class GuessObjectLocationDTO {

@IsString()
hand: HandPosition;

@IsBoolean()
@IsOptional()
isFromEmptyHand?: boolean;
}
13 changes: 13 additions & 0 deletions apps/backend/src/modules/game/dto/player-empty-hand.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { GameState, HandPosition, Player } from '@gol-ya-pooch/shared';
import { IsString } from 'class-validator';

export class PlayerEmptyHandDTO {
@IsString()
gameId: GameState['gameId'];

@IsString()
playerId: Player['id'];

@IsString()
hand: HandPosition | 'both';
}
46 changes: 42 additions & 4 deletions apps/backend/src/modules/game/game.gateway.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { WsValidationExceptionFilter } from '@gol-ya-pooch/backend/common/filters/ws-exception.filter';
import { GetRoomInfoDTO } from '@gol-ya-pooch/backend/modules/game/dto/get-room-info.dto';
import { PlayerEmptyHandDTO } from '@gol-ya-pooch/backend/modules/game/dto/player-empty-hand.dto';
import { PlayerFillHandDTO } from '@gol-ya-pooch/backend/modules/game/dto/player-fill-hand.dto';
import { PlayerPlayingDTO } from '@gol-ya-pooch/backend/modules/game/dto/player-playing.dto';
import { RequestEmptyPlayDTO } from '@gol-ya-pooch/backend/modules/game/dto/request-empty-play.dto';
Expand Down Expand Up @@ -178,7 +179,7 @@ export class GameGateway
@MessageBody() data: GuessObjectLocationDTO,
@ConnectedSocket() client: Socket,
) {
const { gameId, playerId, hand } = data;
const { gameId, playerId, hand, isFromEmptyHand } = data;
const { gameState, isGameFinished, isGuessCorrect, objectLocation } =
await this.gameService.guessObjectLocation(gameId, playerId, hand);

Expand All @@ -199,9 +200,11 @@ export class GameGateway
client.emit(Events.PLAYER_RECEIVE_OBJECT, objectLocation);
}

this.server
.to(gameId)
.emit(Events.GUESS_LOCATION_RESULT, { gameState, isGuessCorrect });
this.server.to(gameId).emit(Events.GUESS_LOCATION_RESULT, {
gameState,
isGuessCorrect,
isFromEmptyHand,
});
}
}

Expand Down Expand Up @@ -231,4 +234,39 @@ export class GameGateway
direction,
});
}

@SubscribeMessage(Events.REQUEST_EMPTY_HAND)
async handlePlayerEmptyHand(
@MessageBody() data: PlayerEmptyHandDTO,
@ConnectedSocket() client: Socket,
) {
const { gameId, playerId, hand } = data;

const { canEmptyPlay, hasObjectInHand, objectLocation } =
await this.gameService.emptyPlayerHand(gameId, playerId, hand);

if (hasObjectInHand) {
await this.handleGuessObjectLocation(
{
gameId,
playerId,
hand: objectLocation.hand === 'left' ? 'right' : 'left',
isFromEmptyHand: true,
},
client,
);
return;
}

if (canEmptyPlay) {
this.server.to(gameId).emit(Events.PLAYER_EMPTY_HAND, {
playerId,
hand,
});
} else {
this.server.to(gameId).emit(Events.REACH_EMPTY_HANDS_LIMIT, {
message: 'خالی بازی‌هاتون تموم شده.',
});
}
}
}
55 changes: 53 additions & 2 deletions apps/backend/src/modules/game/game.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export class GameService {
currentTurn: null,
objectLocation: null,
round: 1,
emptyPlays: gameSize / 2,
scores: {
teamA: 0,
teamB: 0,
Expand Down Expand Up @@ -263,6 +264,51 @@ export class GameService {
};
}

async emptyPlayerHand(
gameId: GameState['gameId'],
playerId: Player['id'],
hand: HandPosition | 'both',
): Promise<{
canEmptyPlay: boolean;
hasObjectInHand: boolean;
objectLocation: IObjectLocation;
}> {
const gameState = await this.getGameState(gameId);

if (gameState.emptyPlays <= 0) {
return {
objectLocation: null,
canEmptyPlay: false,
hasObjectInHand: false,
};
}

const hasObjectInHand =
gameState.objectLocation.playerId === playerId &&
(hand === 'both' || gameState.objectLocation.hand === hand);

if (hasObjectInHand) {
return {
objectLocation: gameState.objectLocation,
canEmptyPlay: false,
hasObjectInHand: true,
};
}

gameState.emptyPlays--;

await this.redisClient.set(
`game:${gameState.gameId}`,
JSON.stringify(gameState),
);

return {
objectLocation: null,
canEmptyPlay: true,
hasObjectInHand: false,
};
}

async guessObjectLocation(
gameId: GameState['gameId'],
playerId: Player['id'],
Expand All @@ -276,10 +322,10 @@ export class GameService {
await this.areTeamsReady(gameId);

const gameState = await this.getGameState(gameId);
let isGameFinished = false;
const isGuessCorrect =
playerId === gameState.objectLocation.playerId &&
hand === gameState.objectLocation.hand;
let isGameFinished = false;

if (isGuessCorrect) {
gameState.currentTurn =
Expand All @@ -305,7 +351,12 @@ export class GameService {

gameState.round += 1;

await this.redisClient.set(`game:${gameId}`, JSON.stringify(gameState));
gameState.emptyPlays = gameState.gameSize / 2;

await this.redisClient.set(
`game:${gameState.gameId}`,
JSON.stringify(gameState),
);

return {
gameState: this.serializeGameState(gameState),
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/public/images/btn-shape-center.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/frontend/public/images/btn-shape-left.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/frontend/public/images/btn-shape-right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions apps/frontend/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './useRequestEmptyPlay';
export * from './useGuessHand';
export * from './useLocalStorage';
export * from './useSound';
export * from './useEmptyHand';
23 changes: 23 additions & 0 deletions apps/frontend/src/hooks/useEmptyHand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useSocket } from '@gol-ya-pooch/frontend/hooks/useSocket';
import { useGameStore } from '@gol-ya-pooch/frontend/stores';
import { Events, HandPosition, Player } from '@gol-ya-pooch/shared';

export const useEmptyHand = () => {
const { gameState } = useGameStore();
const { emit } = useSocket();

const requestEmptyHand = (
playerId: Player['id'],
hand: HandPosition | 'both',
) => {
emit(Events.REQUEST_EMPTY_HAND, {
gameId: gameState?.gameId,
playerId,
hand,
});
};

return {
requestEmptyHand,
};
};
38 changes: 35 additions & 3 deletions apps/frontend/src/hooks/useGameControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export const useGameControls = () => {
setGamePhase,
setFinishGameResult,
setHandFillingData,
setEmptyHand,
resetEmptyHands,
} = useGameStore();
const { player, setObjectLocation } = usePlayerStore();
const { emit, on, off } = useSocket();
Expand Down Expand Up @@ -164,20 +166,29 @@ export const useGameControls = () => {

on(
Events.GUESS_LOCATION_RESULT,
(data: { gameState: PublicGameState; isGuessCorrect: boolean }) => {
(data: {
gameState: PublicGameState;
isGuessCorrect: boolean;
isFromEmptyHand: boolean;
}) => {
if (data.isGuessCorrect) {
showToast('حدس گل درست بود 🎉', 5000);
} else {
playBuzzSound();
showToast('دست گل نبود', 5000);
showToast(
data.isFromEmptyHand ? 'گل رو پوچ کردی!' : 'دست گل نبود',
5000,
);
}
setGameState(data.gameState);

if (data.gameState.gameSize > 2) {
resetEmptyHands();
setGamePhase(GamePhases.SPREADING_OBJECT);
filledHands.current = [];
targetFillHandData.current = null;
}

setGameState(data.gameState);
},
);

Expand All @@ -190,13 +201,27 @@ export const useGameControls = () => {
},
);

on(Events.REACH_EMPTY_HANDS_LIMIT, (data: { message: string }) => {
showToast(data.message, 3000);
});

return () => {
off(Events.GAME_STATE_UPDATED);
off(Events.PLAYER_RECEIVE_OBJECT);
off(Events.GUESS_LOCATION_RESULT);
off(Events.REACH_EMPTY_HANDS_LIMIT);
};
}, []);

useEffect(() => {
if (
phase === GamePhases.SPREADING_OBJECT &&
player?.id === gameState?.gameMaster
) {
showToast('اوستا گل رو پخش کن');
}
}, [phase, player, gameState]);

useEffect(() => {
on(Events.PLAYER_FILL_HAND, (data: PlayerFillHand) => {
setHandFillingData(data);
Expand Down Expand Up @@ -232,6 +257,13 @@ export const useGameControls = () => {
setRequestedPlayerIdToEmptyPlay(playerId);
});

on(
Events.PLAYER_EMPTY_HAND,
(data: { playerId: Player['id']; hand: HandPosition }) => {
setEmptyHand(data.playerId, data.hand);
},
);

on(Events.PLAYER_PLAYING, (playerId: Player['id']) => {
setPlayingPlayerId(playerId);
});
Expand Down
Loading

0 comments on commit b318c01

Please sign in to comment.