Skip to content

Commit

Permalink
finish substitutions work
Browse files Browse the repository at this point in the history
  • Loading branch information
mrosack committed Dec 8, 2023
1 parent 7b4955f commit 7639e94
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 36 deletions.
8 changes: 8 additions & 0 deletions api/controllers/gameController/_models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export interface ReplacePlayerRequestBody {
oldSteamId: string;
newSteamId: string;
}
export interface ReplaceRequestedSubstitutionPlayerRequestBody extends ReplacePlayerRequestBody {
password?: string;
}

export interface OpenGamesResponse {
notStarted: Game[];
Expand All @@ -54,3 +57,8 @@ export interface StartTurnSubmitResponse {
export interface GameTurnListItem extends GameTurn {
hasSave: boolean;
}

export interface OpenSlotsGame extends Game {
joinAfterStart: boolean;
substitutionRequested: boolean;
}
38 changes: 23 additions & 15 deletions api/controllers/gameController/listOpen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Get, Route, Tags } from 'tsoa';
import { GAME_REPOSITORY_SYMBOL, IGameRepository } from '../../../lib/dynamoose/gameRepository';
import { inject, provideSingleton } from '../../../lib/ioc';
import { Game } from '../../../lib/models';
import { OpenGamesResponse } from './_models';
import { OpenGamesResponse, OpenSlotsGame } from './_models';

@Route('game')
@Tags('game')
Expand Down Expand Up @@ -44,20 +44,28 @@ export class GameController_ListOpen {
}

@Get('openSlots')
public async openSlots(): Promise<Game[]> {
public async openSlots(): Promise<OpenSlotsGame[]> {
const games = await this.gameRepository.incompleteGames();
return orderBy(games, ['createdAt'], ['desc']).filter(game => {
const numHumans = game.players.filter(player => {
return !!player.steamId;
}).length;

return (
game.inProgress &&
game.allowJoinAfterStart &&
!game.completed &&
((numHumans < game.players.length && numHumans < game.humans) ||
game.players.some(x => x.substitutionRequested))
);
});
return orderBy(games, ['createdAt'], ['desc'])
.filter(game => game.inProgress && !game.completed)
.map(game => {
const numHumans = game.players.filter(player => {
return !!player.steamId;
}).length;

const joinAfterStart =
game.allowJoinAfterStart &&
!game.hashedPassword &&
numHumans < game.players.length &&
numHumans < game.humans;
const substitutionRequested = game.players.some(x => x.substitutionRequested);

return {
...game,
joinAfterStart,
substitutionRequested
};
})
.filter(x => x.joinAfterStart || x.substitutionRequested);
}
}
102 changes: 81 additions & 21 deletions api/controllers/gameController/replacePlayer.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import * as bcrypt from 'bcryptjs';
import { Body, Post, Request, Response, Route, Security, Tags } from 'tsoa';
import { GAME_REPOSITORY_SYMBOL, IGameRepository } from '../../../lib/dynamoose/gameRepository';
import { IUserRepository, USER_REPOSITORY_SYMBOL } from '../../../lib/dynamoose/userRepository';
import { inject, provideSingleton } from '../../../lib/ioc';
import { Game } from '../../../lib/models';
import { Game, GamePlayer, GameTurn, User } from '../../../lib/models';
import { GAME_TURN_SERVICE_SYMBOL, IGameTurnService } from '../../../lib/services/gameTurnService';
import { ISnsProvider, SNS_PROVIDER_SYMBOL } from '../../../lib/snsProvider';
import { UserUtil } from '../../../lib/util/userUtil';
import { ErrorResponse, HttpRequest, HttpResponseError } from '../../framework';
import { ReplacePlayerRequestBody } from './_models';
import { ReplacePlayerRequestBody, ReplaceRequestedSubstitutionPlayerRequestBody } from './_models';
import {
GAME_TURN_REPOSITORY_SYMBOL,
IGameTurnRepository
} from '../../../lib/dynamoose/gameTurnRepository';
import { GameUtil } from '../../../lib/util/gameUtil';

@Route('game')
@Tags('game')
Expand All @@ -16,10 +22,36 @@ export class GameController_ReplacePlayer {
constructor(
@inject(USER_REPOSITORY_SYMBOL) private userRepository: IUserRepository,
@inject(GAME_REPOSITORY_SYMBOL) private gameRepository: IGameRepository,
@inject(GAME_TURN_REPOSITORY_SYMBOL) private gameTurnRepository: IGameTurnRepository,
@inject(GAME_TURN_SERVICE_SYMBOL) private gameTurnService: IGameTurnService,
@inject(SNS_PROVIDER_SYMBOL) private sns: ISnsProvider
) {}

@Security('api_key')
@Response<ErrorResponse>(401, 'Unauthorized')
@Post('{gameId}/turn/replaceRequestedSubstitutionPlayer')
public async replaceRequestedSubstitutionPlayer(
@Request() request: HttpRequest,
gameId: string,
@Body() body: ReplaceRequestedSubstitutionPlayerRequestBody
) {
return this.coreReplace(gameId, body, async ({ game, oldPlayer }) => {
if (body.newSteamId !== request.user) {
throw new HttpResponseError(400, 'You can only ask to put yourself in this game!');
}

if (!oldPlayer.substitutionRequested) {
throw new HttpResponseError(400, `This player hasn't asked to be substituted!`);
}

if (game.hashedPassword) {
if (!(await bcrypt.compare(body.password || '', game.hashedPassword))) {
throw new HttpResponseError(400, 'Supplied password does not match game password!');
}
}
});
}

@Security('api_key')
@Response<ErrorResponse>(401, 'Unauthorized')
@Post('{gameId}/turn/replacePlayer')
Expand All @@ -28,23 +60,43 @@ export class GameController_ReplacePlayer {
gameId: string,
@Body() body: ReplacePlayerRequestBody
): Promise<Game> {
return this.coreReplace(gameId, body, ({ game, newUser }) => {
if (
request.user !== '76561197973299801' &&
game.createdBySteamId !== request.user &&
body.oldSteamId !== request.user
) {
throw new HttpResponseError(
400,
"You don't have permission to replace a player in this game!"
);
}

if (
request.user !== '76561197973299801' &&
(!newUser.willSubstituteForGameTypes ||
newUser.willSubstituteForGameTypes.indexOf(game.gameType) < 0)
) {
throw new HttpResponseError(400, 'User to substitute has not given permission!');
}
});
}

private async coreReplace(
gameId: string,
body: ReplacePlayerRequestBody,
extraValidations: (state: {
game: Game;
newUser: User;
oldPlayer: GamePlayer;
}) => Promise<void> | void
) {
const game = await this.gameRepository.getOrThrow404(gameId);

if (!game.inProgress) {
throw new HttpResponseError(400, 'Game must be in progress to replace!');
}

if (
request.user !== '76561197973299801' &&
game.createdBySteamId !== request.user &&
body.oldSteamId !== request.user
) {
throw new HttpResponseError(
400,
"You don't have permission to replace a player in this game!"
);
}

const oldPlayer = game.players.find(x => x.steamId === body.oldSteamId);

if (!oldPlayer) {
Expand All @@ -68,13 +120,7 @@ export class GameController_ReplacePlayer {
throw new HttpResponseError(400, 'New user not found!');
}

if (
request.user !== '76561197973299801' &&
(!newUser.willSubstituteForGameTypes ||
newUser.willSubstituteForGameTypes.indexOf(game.gameType) < 0)
) {
throw new HttpResponseError(400, 'User to substitute has not given permission!');
}
await extraValidations({ game, newUser, oldPlayer });

users.push(newUser);

Expand All @@ -83,14 +129,28 @@ export class GameController_ReplacePlayer {

oldPlayer.steamId = body.newSteamId;

let turn: GameTurn;

if (game.currentPlayerSteamId === body.oldSteamId) {
game.currentPlayerSteamId = body.newSteamId;

turn = await this.gameTurnRepository.get({ gameId, turn: game.gameTurnRangeKey });

// Reset turn stats
turn.startDate = new Date();
turn.playerSteamId = newUser.steamId;
}

GameUtil.possiblyUpdateAdmin(game);

// Reset substitution requested flag in all flows (just in case an admin is doing this)
oldPlayer.substitutionRequested = false;

await Promise.all([
this.userRepository.saveVersioned(oldUser),
this.userRepository.saveVersioned(newUser),
this.gameRepository.saveVersioned(game)
this.gameRepository.saveVersioned(game),
...(turn ? [this.gameTurnRepository.saveVersioned(turn)] : [])
]);

await this.gameTurnService.getAndUpdateSaveFileForGameState(game, users);
Expand Down

0 comments on commit 7639e94

Please sign in to comment.