diff --git a/apps/ligretto-gameplay-backend/src/IOC_TYPES.ts b/apps/ligretto-gameplay-backend/src/IOC_TYPES.ts index a4b09084..215c5655 100644 --- a/apps/ligretto-gameplay-backend/src/IOC_TYPES.ts +++ b/apps/ligretto-gameplay-backend/src/IOC_TYPES.ts @@ -1,21 +1,17 @@ export const IOC_TYPES = { - WebSocketHandler: Symbol.for('WebSocketHandler'), - GameService: Symbol.for('GameService'), - GameRepository: Symbol.for('GameRepository'), - PlaygroundService: Symbol.for('PlaygroundService'), - PlaygroundRepository: Symbol.for('PlaygroundRepository'), - PlayerService: Symbol.for('PlayerService'), - PlayerRepository: Symbol.for('PlayerRepository'), - Gameplay: Symbol.for('Gameplay'), - GameplayController: Symbol.for('GameplayController'), - GamesController: Symbol.for('GamesController'), - RoomRepository: Symbol.for('RoomRepository'), - RoomService: Symbol.for('RoomService'), - Emitter: Symbol.for('Emitter'), - Database: Symbol.for('Database'), - UserRepository: Symbol.for('UserRepository'), - UserService: Symbol.for('UserService'), - BotController: Symbol.for('BotController'), - AuthService: Symbol.for('AuthService'), - LigrettoCoreService: Symbol.for('LigrettoCoreService'), + IWebSocketHandler: Symbol.for('IWebSocketHandler'), + IGameService: Symbol.for('IGameService'), + IGameRepository: Symbol.for('IGameRepository'), + IPlaygroundService: Symbol.for('IPlaygroundService'), + IPlaygroundRepository: Symbol.for('IPlaygroundRepository'), + IPlayerService: Symbol.for('IPlayerService'), + IPlayerRepository: Symbol.for('IPlayerRepository'), + IGameplay: Symbol.for('IGameplay'), + IGamesController: Symbol.for('IGamesController'), + IDatabase: Symbol.for('IDatabase'), + IUserRepository: Symbol.for('IUserRepository'), + IUserService: Symbol.for('IUserService'), + IController: Symbol.for('IController'), + IAuthService: Symbol.for('IAuthService'), + ILigrettoCoreService: Symbol.for('ILigrettoCoreService'), } diff --git a/apps/ligretto-gameplay-backend/src/controllers/__tests__/games-contoller.spec.ts b/apps/ligretto-gameplay-backend/src/controllers/__tests__/games-contoller.spec.ts index 41149161..aceb14b4 100644 --- a/apps/ligretto-gameplay-backend/src/controllers/__tests__/games-contoller.spec.ts +++ b/apps/ligretto-gameplay-backend/src/controllers/__tests__/games-contoller.spec.ts @@ -29,15 +29,15 @@ describe('Games Controller', () => { let createGameService = jest.fn().mockReturnValue({ id: gameId }) let saveGameRoundService = jest.fn().mockReturnValue({}) - let gamesController: GamesController = container.get(IOC_TYPES.GamesController) + let gamesController: GamesController = container.get(IOC_TYPES.IGamesController) beforeEach(() => { container = createIOC() socketMockImpl = createSocketMockImpl() createGameService = jest.fn().mockReturnValue({ id: gameId }) saveGameRoundService = jest.fn().mockReturnValue({}) - container.rebind(IOC_TYPES.LigrettoCoreService).toConstantValue({ createGameService, saveGameRoundService }) - gamesController = container.get(IOC_TYPES.GamesController) + container.rebind(IOC_TYPES.ILigrettoCoreService).toConstantValue({ createGameService, saveGameRoundService }) + gamesController = container.get(IOC_TYPES.IGamesController) }) it('should be defined', () => { @@ -46,7 +46,7 @@ describe('Games Controller', () => { describe('createGame', () => { it('Should create relevant state on create game', async () => { - const database: Database = container.get(IOC_TYPES.Database) + const database: Database = container.get(IOC_TYPES.IDatabase) const roomName = 'createGame' @@ -80,8 +80,8 @@ describe('Games Controller', () => { beforeEach(async () => { socketMockImpl.data = { user: { id: userId } } - gamesController = container.get(IOC_TYPES.GamesController) - const database: Database = container.get(IOC_TYPES.Database) + gamesController = container.get(IOC_TYPES.IGamesController) + const database: Database = container.get(IOC_TYPES.IDatabase) await database.set(storage => { storage.users = { @@ -111,14 +111,14 @@ describe('Games Controller', () => { it('Should create relevant state on join room as first player', async () => { await gamesController.handleMessage(socketMockImpl, connectToRoomEmitAction({ roomUuid }) as AnyAction) - const database: Database = container.get(IOC_TYPES.Database) + const database: Database = container.get(IOC_TYPES.IDatabase) const state = await database.get(db => db) expect(state).toMatchSnapshot() }) it('Should create relevant state on join room second player', async () => { - const database: Database = container.get(IOC_TYPES.Database) + const database: Database = container.get(IOC_TYPES.IDatabase) const secondUserSocket = createSocketMockImpl({ id: 'secondUserSocketId' }) secondUserSocket.data = { user: { id: 'secondUserId' } } const secondUserId = 'secondUserId' @@ -140,7 +140,7 @@ describe('Games Controller', () => { }) it('Should create relevant state on join room by second connection', async () => { - const database: Database = container.get(IOC_TYPES.Database) + const database: Database = container.get(IOC_TYPES.IDatabase) await gamesController.handleMessage(socketMockImpl, connectToRoomEmitAction({ roomUuid }) as AnyAction) const secondUserSocket = createSocketMockImpl() secondUserSocket.data = { user: { id: userId } } @@ -169,8 +169,8 @@ describe('Games Controller', () => { let socketTwo = createSocketMockImpl({ id: 'socket2', data: { user: { id: userTwoId } } }) beforeEach(async () => { - gamesController = container.get(IOC_TYPES.GamesController) - const database: Database = container.get(IOC_TYPES.Database) + gamesController = container.get(IOC_TYPES.IGamesController) + const database: Database = container.get(IOC_TYPES.IDatabase) socketOne = createSocketMockImpl({ id: 'socket1', data: { user: { id: userOneId } } }) socketTwo = createSocketMockImpl({ id: 'socket2', data: { user: { id: userTwoId } } }) @@ -195,7 +195,7 @@ describe('Games Controller', () => { }) it('Should remove current socketId from user socket ids if user connected from few accounts', async () => { - const database: Database = container.get(IOC_TYPES.Database) + const database: Database = container.get(IOC_TYPES.IDatabase) await database.set(storage => { storage.users = { @@ -219,7 +219,7 @@ describe('Games Controller', () => { }) it('Should create a relevant game state if one of two players leaved', async () => { - const database: Database = container.get(IOC_TYPES.Database) + const database: Database = container.get(IOC_TYPES.IDatabase) await database.set(storage => { storage.users = { @@ -249,7 +249,7 @@ describe('Games Controller', () => { }) it('Should create a relevant state if last user disconnected', async () => { - const database: Database = container.get(IOC_TYPES.Database) + const database: Database = container.get(IOC_TYPES.IDatabase) await gamesController.handleMessage(socketOne, connectToRoomEmitAction({ roomUuid }) as AnyAction) await gamesController.disconnectionHandler(socketOne) diff --git a/apps/ligretto-gameplay-backend/src/controllers/bot-controller.ts b/apps/ligretto-gameplay-backend/src/controllers/bot-controller.ts index 6d301828..217a2726 100644 --- a/apps/ligretto-gameplay-backend/src/controllers/bot-controller.ts +++ b/apps/ligretto-gameplay-backend/src/controllers/bot-controller.ts @@ -4,12 +4,12 @@ import type { Socket } from 'socket.io' import { inject, injectable } from 'inversify' import { initBot, stopBot } from '../bot' import { IOC_TYPES } from '../IOC_TYPES' -import { GameService } from '../entities/game/game.service' +import { IGameService } from '../entities/game/game.service' import { Controller } from './controller' @injectable() export class BotController extends Controller { - @inject(IOC_TYPES.GameService) private gameService: GameService + @inject(IOC_TYPES.IGameService) private gameService: IGameService handlers: Controller['handlers'] = { [addBotAction.type]: (socket, action) => this.addBotToGame(socket, action), diff --git a/apps/ligretto-gameplay-backend/src/controllers/controller.ts b/apps/ligretto-gameplay-backend/src/controllers/controller.ts index 6826e9e2..e19b3887 100644 --- a/apps/ligretto-gameplay-backend/src/controllers/controller.ts +++ b/apps/ligretto-gameplay-backend/src/controllers/controller.ts @@ -2,8 +2,13 @@ import type { Socket } from 'socket.io' import { injectable } from 'inversify' import type { AnyAction } from '../types/any-action' +export interface IController { + handleMessage: (socket: Socket, action: AnyAction) => void +} + +// XXX: Is there the necessity of IController @injectable() -export abstract class Controller { +export abstract class Controller implements IController { protected abstract handlers: { [actionType: string]: (socket: Socket, action: A) => void } diff --git a/apps/ligretto-gameplay-backend/src/controllers/gameplay-controller.ts b/apps/ligretto-gameplay-backend/src/controllers/gameplay-controller.ts index d0de0072..32061706 100644 --- a/apps/ligretto-gameplay-backend/src/controllers/gameplay-controller.ts +++ b/apps/ligretto-gameplay-backend/src/controllers/gameplay-controller.ts @@ -12,14 +12,14 @@ import { takeFromStackDeckAction, } from '@memebattle/ligretto-shared' import { IOC_TYPES } from '../IOC_TYPES' -import { Gameplay } from '../gameplay/gameplay' -import { GameService } from '../entities/game/game.service' +import { IGameplay } from '../gameplay/gameplay' +import { IGameService } from '../entities/game/game.service' import { wait } from '../utils/wait' @injectable() export class GameplayController extends Controller { - @inject(IOC_TYPES.Gameplay) private gameplay: Gameplay - @inject(IOC_TYPES.GameService) private gameService: GameService + @inject(IOC_TYPES.IGameplay) private gameplay: IGameplay + @inject(IOC_TYPES.IGameService) private gameService: IGameService protected handlers: Controller['handlers'] = { [startGameEmitAction.type]: (socket, action: ReturnType) => this.startGame(socket, action), diff --git a/apps/ligretto-gameplay-backend/src/controllers/games-controller.ts b/apps/ligretto-gameplay-backend/src/controllers/games-controller.ts index c4b1c70c..682f58fc 100644 --- a/apps/ligretto-gameplay-backend/src/controllers/games-controller.ts +++ b/apps/ligretto-gameplay-backend/src/controllers/games-controller.ts @@ -2,9 +2,10 @@ import { inject, injectable } from 'inversify' import { IOC_TYPES } from '../IOC_TYPES' import { Controller } from './controller' import type { Socket } from 'socket.io' -import { GameService } from '../entities/game/game.service' -import { UserService } from '../entities/user' +import { IGameService } from '../entities/game/game.service' +import { IUserService } from '../entities/user' import type { Game } from '@memebattle/ligretto-shared' +import type { IController } from './controller' import { connectToRoomEmitAction, connectToRoomErrorAction, @@ -25,10 +26,13 @@ import { import { SOCKET_ROOM_LOBBY } from '../config' import { gameToRoom } from '../utils/mappers' +export interface IGamesController extends IController { + disconnectionHandler: (socket: Socket) => Promise +} @injectable() -export class GamesController extends Controller { - @inject(IOC_TYPES.GameService) private gameService: GameService - @inject(IOC_TYPES.UserService) private userService: UserService +export class GamesController extends Controller implements IGamesController { + @inject(IOC_TYPES.IGameService) private gameService: IGameService + @inject(IOC_TYPES.IUserService) private userService: IUserService protected handlers: Controller['handlers'] = { [createRoomEmitAction.type]: (socket, action) => this.createGame(socket, action), diff --git a/apps/ligretto-gameplay-backend/src/database/database.ts b/apps/ligretto-gameplay-backend/src/database/database.ts index ef3e2043..ea5813b6 100644 --- a/apps/ligretto-gameplay-backend/src/database/database.ts +++ b/apps/ligretto-gameplay-backend/src/database/database.ts @@ -9,7 +9,7 @@ export const storage: Storage = { type Accessor = (storage: Storage) => T type Setter = (storage: Storage) => T -export interface Database { +export interface IDatabase { get(accessor: Accessor): Promise set(setter: Setter): Promise // eslint-disable-next-line @typescript-eslint/ban-types @@ -34,7 +34,7 @@ const createChangesWatcher = (obj: T) => { } @injectable() -export class Database implements Database { +export class Database implements IDatabase { private storage: Storage = { games: {}, users: {}, diff --git a/apps/ligretto-gameplay-backend/src/database/index.ts b/apps/ligretto-gameplay-backend/src/database/index.ts index 88c93628..f9a665d8 100644 --- a/apps/ligretto-gameplay-backend/src/database/index.ts +++ b/apps/ligretto-gameplay-backend/src/database/index.ts @@ -1 +1 @@ -export { Database } from './database' +export { Database, IDatabase } from './database' diff --git a/apps/ligretto-gameplay-backend/src/entities/game/game.repo.ts b/apps/ligretto-gameplay-backend/src/entities/game/game.repo.ts index 6a3326ef..5398cef6 100644 --- a/apps/ligretto-gameplay-backend/src/entities/game/game.repo.ts +++ b/apps/ligretto-gameplay-backend/src/entities/game/game.repo.ts @@ -1,12 +1,23 @@ import { inject, injectable } from 'inversify' import type { Game, Player, UUID, Spectator } from '@memebattle/ligretto-shared' import { PlayerStatus } from '@memebattle/ligretto-shared' -import { Database } from '../../database' +import { IDatabase } from '../../database' import { IOC_TYPES } from '../../IOC_TYPES' +export interface IGameRepository { + addGame(gameId: UUID, game: Game): Promise + getGame(gameId: UUID): Promise + updateGame(gameId: UUID, updater: (game: Game) => Game): Promise + getGameByName(gameName: string): Promise + removeGame(gameId: UUID): Promise + getGames(): Promise + getGamesByNames(games: Record): Record + createPlayer(playerData: Partial & { id: Player['id'] }): Player + createSpectator(playerData: Partial & { id: Spectator['id'] }): Spectator +} @injectable() -export class GameRepository { - @inject(IOC_TYPES.Database) private database: Database +export class GameRepository implements IGameRepository { + @inject(IOC_TYPES.IDatabase) private database: IDatabase async addGame(gameId: UUID, game: Game) { return this.database.set(storage => (storage.games[gameId] = game)) @@ -16,7 +27,7 @@ export class GameRepository { return this.database.get(storage => storage.games[gameId]) } - async updateGame(gameId: UUID, updater: (game: Game) => Game): Promise { + async updateGame(gameId: UUID, updater: (game: Game) => Game) { const game = await this.getGame(gameId) return this.database.set(storage => (storage.games[gameId] = updater(game))) @@ -32,12 +43,12 @@ export class GameRepository { return this.database.set(storage => delete storage.games[gameId]) } - async getGames(): Promise { + async getGames() { const games = (await this.database.get(storage => Object.values(storage.games))).filter(Boolean) as Game[] return games } - getGamesByNames(games: Record): Record { + getGamesByNames(games: Record) { const result: Record = {} Object.values(games).forEach(game => { @@ -47,7 +58,7 @@ export class GameRepository { return result } - createPlayer(playerData: Partial & { id: Player['id'] }): Player { + createPlayer(playerData: Partial & { id: Player['id'] }) { return { isHost: false, status: PlayerStatus.DontReadyToPlay, @@ -65,7 +76,7 @@ export class GameRepository { } } - createSpectator(playerData: Partial & { id: Spectator['id'] }): Spectator { + createSpectator(playerData: Partial & { id: Spectator['id'] }) { return { ...playerData, } diff --git a/apps/ligretto-gameplay-backend/src/entities/game/game.service.ts b/apps/ligretto-gameplay-backend/src/entities/game/game.service.ts index 444f4496..9f9a172f 100644 --- a/apps/ligretto-gameplay-backend/src/entities/game/game.service.ts +++ b/apps/ligretto-gameplay-backend/src/entities/game/game.service.ts @@ -1,12 +1,28 @@ import { inject, injectable } from 'inversify' import { groupBy, mapValues, merge, mergeWith, omit } from 'lodash' -import { GameRepository } from './game.repo' +import { IGameRepository } from './game.repo' import type { Game, GameResults, Player, Spectator, UUID } from '@memebattle/ligretto-shared' import { PlayerStatus, GameStatus } from '@memebattle/ligretto-shared' import { createInitialPlayerCards } from '../../utils/create-initial-player-cards' import { IOC_TYPES } from '../../IOC_TYPES' import { nonNullable } from '../../utils/nonNullable' -import { LigrettoCoreService } from '../../services/ligretto-core' +import { ILigrettoCoreService } from '../../services/ligretto-core' + +export interface IGameService { + createGame(name: string, config?: Partial): Promise + initiateStartGame(gameId: UUID): Promise + startGame(gameId: UUID): Promise + pauseGame(gameId: UUID): Promise + addPlayer(gameId: UUID, playerData: Partial & { id: Player['id'] }): Promise<{ game: Game; player: Player }> + addSpectator(gameId: UUID, spectatorData: Partial & { id: Spectator['id'] }): Promise<{ game: Game; spectator: Spectator }> + updateGamePlayer(gameId: Game['id'], playerId: Player['id'], playerData: Partial): Promise + getGame(gameId: UUID): Promise + getRoundResult(gameId: UUID): Promise<{ [x: string]: { roundScore: number } }> + endGame(gameId: UUID): Promise<(Game | GameResults | undefined)[]> + finishRound(gameId: UUID): Promise<{ game?: Game; gameResults?: GameResults }> + getGames(): Promise + leaveGame(gameId: UUID, userId: Player['id'] | Spectator['id']): Promise +} const emptyGame: Game = { id: 'base', @@ -22,9 +38,9 @@ const emptyGame: Game = { } @injectable() -export class GameService { - @inject(IOC_TYPES.GameRepository) private gameRepository: GameRepository - @inject(IOC_TYPES.LigrettoCoreService) private ligrettoCoreService: LigrettoCoreService +export class GameService implements IGameService { + @inject(IOC_TYPES.IGameRepository) private gameRepository: IGameRepository + @inject(IOC_TYPES.ILigrettoCoreService) private ligrettoCoreService: ILigrettoCoreService async createGame(name: string, config: Partial = {}): Promise { const isGameExists = !!(await this.gameRepository.getGameByName(name)) diff --git a/apps/ligretto-gameplay-backend/src/entities/player/player.repo.ts b/apps/ligretto-gameplay-backend/src/entities/player/player.repo.ts index d1171034..83eb84cf 100644 --- a/apps/ligretto-gameplay-backend/src/entities/player/player.repo.ts +++ b/apps/ligretto-gameplay-backend/src/entities/player/player.repo.ts @@ -1,11 +1,26 @@ import { inject, injectable } from 'inversify' -import type { Card, CardsDeck, UUID } from '@memebattle/ligretto-shared' -import { Database } from '../../database' +import type { Card, CardsDeck, UUID, Player } from '@memebattle/ligretto-shared' +import { IDatabase } from '../../database' import { IOC_TYPES } from '../../IOC_TYPES' +export interface IPlayerRepository { + getPlayer(gameId: UUID, playerId: UUID): Promise + getCards(gameId: UUID, playerId: UUID): Promise<(Card | null)[] | undefined> + getCard(gameId: UUID, playerId: UUID, position: number): Promise + addCard(gameId: UUID, playerId: UUID, card: Card, position: number): Promise<(Card | null)[] | undefined> + removeCard(gameId: UUID, playerId: UUID, position: number): Promise<(Card | null)[] | undefined> + getLigrettoDeck(gameId: UUID, playerId: UUID): Promise + removeCardFromLigrettoDeck(gameId: UUID, playerId: UUID): Promise + getStackDeck(gameId: UUID, playerId: UUID): Promise + getStackOpenDeck(gameId: UUID, playerId: UUID): Promise + removeCardFromStackOpenDeck(gameId: UUID, playerId: UUID): Promise + updateStackDeck(gameId: UUID, playerId: UUID, updater: (cardsDeck: CardsDeck) => CardsDeck): Promise + updateStackOpenDeck(gameId: UUID, playerId: UUID, updater: (cardsDeck: CardsDeck) => CardsDeck): Promise +} + @injectable() -export class PlayerRepository { - @inject(IOC_TYPES.Database) private database: Database +export class PlayerRepository implements IPlayerRepository { + @inject(IOC_TYPES.IDatabase) private database: IDatabase async getPlayer(gameId: UUID, playerId: UUID) { return this.database.get(storage => storage.games[gameId].players[playerId]) diff --git a/apps/ligretto-gameplay-backend/src/entities/player/player.service.ts b/apps/ligretto-gameplay-backend/src/entities/player/player.service.ts index 387008e0..ab9f35d4 100644 --- a/apps/ligretto-gameplay-backend/src/entities/player/player.service.ts +++ b/apps/ligretto-gameplay-backend/src/entities/player/player.service.ts @@ -1,14 +1,24 @@ import { inject, injectable } from 'inversify' import { last, shuffle } from 'lodash' -import { PlayerRepository } from './player.repo' -import type { Card, UUID } from '@memebattle/ligretto-shared' +import { IPlayerRepository } from './player.repo' +import type { Card, UUID, Player } from '@memebattle/ligretto-shared' import { IOC_TYPES } from '../../IOC_TYPES' +export interface IPlayerService { + getCard(gameId: UUID, playerId: UUID, position: number): Promise + removeCard(gameId: UUID, playerId: UUID, position: number): Promise + removeCardFromStackOpenDeck(gameId: UUID, playerId: UUID): Promise + getCardFromStackOpenDeck(gameId: UUID, playerId: UUID): Promise + takeFromStackDeck(gameId: UUID, playerId: UUID): Promise + takeFromLigrettoDeck(gameId: UUID, playerId: string): Promise +} + @injectable() -export class PlayerService { - @inject(IOC_TYPES.PlayerRepository) private playerRepository: PlayerRepository +export class PlayerService implements IPlayerService { + @inject(IOC_TYPES.IPlayerRepository) private playerRepository: IPlayerRepository - async getPlayer(gameId: UUID, playerId: UUID) { + // XXX: For now this method is unused + private async getPlayer(gameId: UUID, playerId: UUID): Promise { return await this.playerRepository.getPlayer(gameId, playerId) } @@ -16,7 +26,7 @@ export class PlayerService { return await this.playerRepository.getCard(gameId, playerId, position) } - async addCard(gameId: UUID, playerId: UUID, card: Card) { + private async addCard(gameId: UUID, playerId: UUID, card: Card): Promise { const cards = await this.playerRepository.getCards(gameId, playerId) const emptyCardIndex = cards?.findIndex(card => card === null) @@ -32,7 +42,7 @@ export class PlayerService { return undefined } - async removeCardFromLigrettoDeck(gameId: UUID, playerId: UUID) { + private async removeCardFromLigrettoDeck(gameId: UUID, playerId: UUID): Promise { return this.playerRepository.removeCardFromLigrettoDeck(gameId, playerId) } @@ -47,7 +57,7 @@ export class PlayerService { return last(deck?.cards) } - async shuffleStackDeck(gameId: UUID, playerId: UUID) { + private async shuffleStackDeck(gameId: UUID, playerId: UUID): Promise { const stackOpenDeck = await this.playerRepository.getStackOpenDeck(gameId, playerId) const stackDeck = await this.playerRepository.getStackDeck(gameId, playerId) @@ -66,7 +76,7 @@ export class PlayerService { })) } - async takeFromStackDeck(gameId: UUID, playerId: UUID) { + async takeFromStackDeck(gameId: UUID, playerId: UUID): Promise { const stackDeck = await this.playerRepository.getStackDeck(gameId, playerId) if (stackDeck?.cards.length === 0) { await this.shuffleStackDeck(gameId, playerId) diff --git a/apps/ligretto-gameplay-backend/src/entities/playground/index.ts b/apps/ligretto-gameplay-backend/src/entities/playground/index.ts index 788da4ca..a61bacd8 100644 --- a/apps/ligretto-gameplay-backend/src/entities/playground/index.ts +++ b/apps/ligretto-gameplay-backend/src/entities/playground/index.ts @@ -1,2 +1,2 @@ -export { PlaygroundService } from './playground.service' -export { PlaygroundRepository } from './playground.repo' +export { IPlaygroundService, PlaygroundService } from './playground.service' +export { IPlaygroundRepository, PlaygroundRepository } from './playground.repo' diff --git a/apps/ligretto-gameplay-backend/src/entities/playground/playground.repo.ts b/apps/ligretto-gameplay-backend/src/entities/playground/playground.repo.ts index 425c6197..fd5ff7d5 100644 --- a/apps/ligretto-gameplay-backend/src/entities/playground/playground.repo.ts +++ b/apps/ligretto-gameplay-backend/src/entities/playground/playground.repo.ts @@ -1,11 +1,19 @@ import { inject, injectable } from 'inversify' import type { CardsDeck, UUID } from '@memebattle/ligretto-shared' -import { Database } from '../../database/database' +import { IDatabase } from '../../database/database' import { IOC_TYPES } from '../../IOC_TYPES' +export interface IPlaygroundRepository { + getDecks(gameId: UUID): Promise<(CardsDeck | null)[]> + getDeck(gameId: UUID, position: number): Promise + addDroppedDeck(gameId: UUID, cardsDeck: CardsDeck): Promise + removeDeck(gameId: UUID, position: number): Promise + updateDeck(gameId: UUID, position: number, updater: (deck: CardsDeck | null) => CardsDeck): Promise +} + @injectable() -export class PlaygroundRepository { - @inject(IOC_TYPES.Database) private database: Database +export class PlaygroundRepository implements IPlaygroundRepository { + @inject(IOC_TYPES.IDatabase) private database: IDatabase getDecks(gameId: UUID) { return this.database.get(storage => storage.games[gameId].playground.decks) diff --git a/apps/ligretto-gameplay-backend/src/entities/playground/playground.service.ts b/apps/ligretto-gameplay-backend/src/entities/playground/playground.service.ts index 8bd1fe1b..a498650a 100644 --- a/apps/ligretto-gameplay-backend/src/entities/playground/playground.service.ts +++ b/apps/ligretto-gameplay-backend/src/entities/playground/playground.service.ts @@ -1,9 +1,14 @@ import { inject, injectable } from 'inversify' import { last } from 'lodash' -import { PlaygroundRepository } from './playground.repo' +import { IPlaygroundRepository } from './playground.repo' import type { Card, CardsDeck, Game, UUID } from '@memebattle/ligretto-shared' import { IOC_TYPES } from '../../IOC_TYPES' +export interface IPlaygroundService { + putCard(gameId: UUID, card: Card, deckIndex: number): Promise + getAvailableDeckPosition(gameId: Game['id'], card: Card, deckPosition?: number): Promise +} + const isDeckAvailable = (deck: CardsDeck | null, card: Card) => { const topCard: Card | undefined = last(deck?.cards) if (!topCard) { @@ -13,14 +18,14 @@ const isDeckAvailable = (deck: CardsDeck | null, card: Card) => { } @injectable() -export class PlaygroundService { - @inject(IOC_TYPES.PlaygroundRepository) private playgroundRepository: PlaygroundRepository +export class PlaygroundService implements IPlaygroundService { + @inject(IOC_TYPES.IPlaygroundRepository) private playgroundRepository: IPlaygroundRepository - async getDecks(gameId: UUID) { + private async getDecks(gameId: UUID) { return await this.playgroundRepository.getDecks(gameId) } - async findAvailableDeckIndex(gameId: UUID, card: Card) { + private async findAvailableDeckIndex(gameId: UUID, card: Card) { const decks = await this.getDecks(gameId) return decks.findIndex(deck => isDeckAvailable(deck, card)) } @@ -50,7 +55,7 @@ export class PlaygroundService { } } - async checkIsDeckAvailable(gameId: UUID, card: Card, position: number) { + private async checkIsDeckAvailable(gameId: UUID, card: Card, position: number) { const deck = await this.playgroundRepository.getDeck(gameId, position) const topCard: Card | undefined = last(deck?.cards) diff --git a/apps/ligretto-gameplay-backend/src/entities/user/index.ts b/apps/ligretto-gameplay-backend/src/entities/user/index.ts index ba0c118d..293d09a2 100644 --- a/apps/ligretto-gameplay-backend/src/entities/user/index.ts +++ b/apps/ligretto-gameplay-backend/src/entities/user/index.ts @@ -1,2 +1,2 @@ -export { UserRepository } from './user.repo' -export { UserService } from './user.service' +export { UserRepository, IUserRepository } from './user.repo' +export { UserService, IUserService } from './user.service' diff --git a/apps/ligretto-gameplay-backend/src/entities/user/user.repo.ts b/apps/ligretto-gameplay-backend/src/entities/user/user.repo.ts index e5c3dca3..35679918 100644 --- a/apps/ligretto-gameplay-backend/src/entities/user/user.repo.ts +++ b/apps/ligretto-gameplay-backend/src/entities/user/user.repo.ts @@ -1,14 +1,23 @@ +import type { Storage } from '../../database/storage' + import { inject, injectable } from 'inversify' import { IOC_TYPES } from '../../IOC_TYPES' -import { Database } from '../../database' +import { IDatabase } from '../../database' import type { User } from '../../types/user' import { omit } from 'lodash' import type { UUID } from '@memebattle/ligretto-shared' import assert from 'node:assert' +export interface IUserRepository { + createOrUpdate({ userId, socketId }: { userId: UUID; socketId: string }): Promise + updateUser(updatePayload: Partial & { id: User['id'] }): Promise + removeUser(userId: User['id']): Promise, never>> + getUser(userId: User['id']): Promise +} + @injectable() -export class UserRepository { - @inject(IOC_TYPES.Database) private database: Database +export class UserRepository implements IUserRepository { + @inject(IOC_TYPES.IDatabase) private database: IDatabase createOrUpdate({ userId, socketId }: { userId: UUID; socketId: string }) { return this.database.set(storage => { diff --git a/apps/ligretto-gameplay-backend/src/entities/user/user.service.ts b/apps/ligretto-gameplay-backend/src/entities/user/user.service.ts index ad875912..b8fb7d51 100644 --- a/apps/ligretto-gameplay-backend/src/entities/user/user.service.ts +++ b/apps/ligretto-gameplay-backend/src/entities/user/user.service.ts @@ -1,12 +1,19 @@ import { inject, injectable } from 'inversify' import { IOC_TYPES } from '../../IOC_TYPES' -import { UserRepository } from './user.repo' +import { IUserRepository } from './user.repo' import type { User } from '../../types/user' import type { UUID } from '@memebattle/ligretto-shared' +import type { Storage } from '../../database/storage' +export interface IUserService { + connectUser(payload: { userId: UUID; socketId: UUID }): Promise + joinGame({ userId, gameId }: { userId: UUID; gameId: UUID }): Promise + disconnectionHandler(payload: { userId: User['id']; socketId: UUID }): Promise, never>> + getUser(userId: User['id']): Promise +} @injectable() -export class UserService { - @inject(IOC_TYPES.UserRepository) private userRepository: UserRepository +export class UserService implements IUserService { + @inject(IOC_TYPES.IUserRepository) private userRepository: IUserRepository connectUser(payload: { userId: UUID; socketId: UUID }) { return this.userRepository.createOrUpdate(payload) diff --git a/apps/ligretto-gameplay-backend/src/gameplay/gameplay.ts b/apps/ligretto-gameplay-backend/src/gameplay/gameplay.ts index 85b7879d..384949e1 100644 --- a/apps/ligretto-gameplay-backend/src/gameplay/gameplay.ts +++ b/apps/ligretto-gameplay-backend/src/gameplay/gameplay.ts @@ -1,15 +1,23 @@ import { inject, injectable } from 'inversify' -import { PlayerService } from '../entities/player/player.service' -import { PlaygroundService } from '../entities/playground' -import { GameService } from '../entities/game/game.service' +import { IPlayerService } from '../entities/player/player.service' +import { IPlaygroundService } from '../entities/playground' +import { IGameService } from '../entities/game/game.service' import { IOC_TYPES } from '../IOC_TYPES' import type { Game, GameResults, UUID } from '@memebattle/ligretto-shared' +export interface IGameplay { + startGame(gameId: UUID): Promise + playerPutCard(gameId: UUID, playerId: UUID, cardPosition: number, deckPosition?: number): Promise + playerPutFromStackOpenDeck(gameId: UUID, playerId: UUID, deckPosition?: number): Promise + playerTakeFromLigrettoDeck(gameId: UUID, playerId: UUID): Promise<{ game?: Game; gameResults?: GameResults }> + playerTakeFromStackDeck(gameId: UUID, playerId: UUID): Promise +} + @injectable() -export class Gameplay { - @inject(IOC_TYPES.GameService) private gameService: GameService - @inject(IOC_TYPES.PlayerService) private playerService: PlayerService - @inject(IOC_TYPES.PlaygroundService) private playgroundService: PlaygroundService +export class Gameplay implements IGameplay { + @inject(IOC_TYPES.IGameService) private gameService: IGameService + @inject(IOC_TYPES.IPlayerService) private playerService: IPlayerService + @inject(IOC_TYPES.IPlaygroundService) private playgroundService: IPlaygroundService async startGame(gameId: UUID) { try { @@ -81,7 +89,8 @@ export class Gameplay { } } - async endGame(gameId: UUID) { + // XXX: for now this method is not used + private async endGame(gameId: UUID) { try { const roundResult = await this.gameService.getRoundResult(gameId) await this.gameService.endGame(gameId) diff --git a/apps/ligretto-gameplay-backend/src/inversify.config.ts b/apps/ligretto-gameplay-backend/src/inversify.config.ts index b740cd5e..1b1cf8cf 100644 --- a/apps/ligretto-gameplay-backend/src/inversify.config.ts +++ b/apps/ligretto-gameplay-backend/src/inversify.config.ts @@ -2,39 +2,52 @@ import 'reflect-metadata' import { Container } from 'inversify' import { IOC_TYPES } from './IOC_TYPES' import { GameService } from './entities/game/game.service' +import type { IGameService } from './entities/game/game.service' import { GameplayController } from './controllers/gameplay-controller' import { GamesController } from './controllers/games-controller' +import type { IGamesController } from './controllers/games-controller' +import type { IController } from './controllers/controller' import { Gameplay } from './gameplay/gameplay' +import type { IGameplay } from './gameplay/gameplay' import { WebSocketHandler } from './websocket-handlers' +import type { IWebSocketHandler } from './websocket-handlers' import { GameRepository } from './entities/game/game.repo' +import type { IGameRepository } from './entities/game/game.repo' import { PlaygroundRepository, PlaygroundService } from './entities/playground' +import type { IPlaygroundRepository, IPlaygroundService } from './entities/playground' import { PlayerRepository } from './entities/player/player.repo' import { PlayerService } from './entities/player/player.service' +import type { IPlayerRepository } from './entities/player/player.repo' +import type { IPlayerService } from './entities/player/player.service' import { Database } from './database' +import type { IDatabase } from './database' import { UserService, UserRepository } from './entities/user' +import type { IUserService, IUserRepository } from './entities/user' import { BotController } from './controllers/bot-controller' import { AuthService } from './services/auth' import { LigrettoCoreService } from './services/ligretto-core' +import type { IAuthService } from './services/auth' +import type { ILigrettoCoreService } from './services/ligretto-core' export const createIOC = () => { const IOC = new Container() - IOC.bind(IOC_TYPES.WebSocketHandler).to(WebSocketHandler) - IOC.bind(IOC_TYPES.GameService).to(GameService) - IOC.bind(IOC_TYPES.GameRepository).to(GameRepository) - IOC.bind(IOC_TYPES.PlaygroundRepository).to(PlaygroundRepository) - IOC.bind(IOC_TYPES.PlaygroundService).to(PlaygroundService) - IOC.bind(IOC_TYPES.PlayerRepository).to(PlayerRepository) - IOC.bind(IOC_TYPES.PlayerService).to(PlayerService) - IOC.bind(IOC_TYPES.UserService).to(UserService) - IOC.bind(IOC_TYPES.UserRepository).to(UserRepository) - IOC.bind(IOC_TYPES.Gameplay).to(Gameplay) - IOC.bind(IOC_TYPES.GameplayController).to(GameplayController) - IOC.bind(IOC_TYPES.GamesController).to(GamesController) - IOC.bind(IOC_TYPES.BotController).to(BotController) - IOC.bind(IOC_TYPES.Database).to(Database).inSingletonScope() - IOC.bind(IOC_TYPES.AuthService).to(AuthService).inSingletonScope() - IOC.bind(IOC_TYPES.LigrettoCoreService).to(LigrettoCoreService).inSingletonScope() + IOC.bind(IOC_TYPES.IWebSocketHandler).to(WebSocketHandler) + IOC.bind(IOC_TYPES.IGameService).to(GameService) + IOC.bind(IOC_TYPES.IGameRepository).to(GameRepository) + IOC.bind(IOC_TYPES.IPlaygroundRepository).to(PlaygroundRepository) + IOC.bind(IOC_TYPES.IPlaygroundService).to(PlaygroundService) + IOC.bind(IOC_TYPES.IPlayerRepository).to(PlayerRepository) + IOC.bind(IOC_TYPES.IPlayerService).to(PlayerService) + IOC.bind(IOC_TYPES.IUserService).to(UserService) + IOC.bind(IOC_TYPES.IUserRepository).to(UserRepository) + IOC.bind(IOC_TYPES.IGameplay).to(Gameplay) + IOC.bind(IOC_TYPES.IController).to(GameplayController).whenTargetNamed('gameplayController') + IOC.bind(IOC_TYPES.IGamesController).to(GamesController) + IOC.bind(IOC_TYPES.IController).to(BotController).whenTargetNamed('botController') + IOC.bind(IOC_TYPES.IDatabase).to(Database).inSingletonScope() + IOC.bind(IOC_TYPES.IAuthService).to(AuthService).inSingletonScope() + IOC.bind(IOC_TYPES.ILigrettoCoreService).to(LigrettoCoreService).inSingletonScope() return IOC } diff --git a/apps/ligretto-gameplay-backend/src/middlewares/authMiddleware.ts b/apps/ligretto-gameplay-backend/src/middlewares/authMiddleware.ts index 752d515d..399df8cb 100644 --- a/apps/ligretto-gameplay-backend/src/middlewares/authMiddleware.ts +++ b/apps/ligretto-gameplay-backend/src/middlewares/authMiddleware.ts @@ -1,7 +1,7 @@ import type { Socket } from 'socket.io' import { IOC } from '../inversify.config' import { IOC_TYPES } from '../IOC_TYPES' -import type { AuthService } from '../services/auth' +import type { IAuthService } from '../services/auth' /** * Add userId to socket.data.user.id @@ -10,7 +10,7 @@ import type { AuthService } from '../services/auth' * @param next */ export const authMiddleware = async (socket: Socket, next: (error?: Error) => void) => { - const authService = IOC.get(IOC_TYPES.AuthService) + const authService = IOC.get(IOC_TYPES.IAuthService) const token = socket.handshake?.auth?.token if (!token) { const error = new Error('socket.handshake?.auth?.token is null') diff --git a/apps/ligretto-gameplay-backend/src/services/auth/auth.service.ts b/apps/ligretto-gameplay-backend/src/services/auth/auth.service.ts index 3aa3465b..34813018 100644 --- a/apps/ligretto-gameplay-backend/src/services/auth/auth.service.ts +++ b/apps/ligretto-gameplay-backend/src/services/auth/auth.service.ts @@ -1,10 +1,16 @@ import { injectable } from 'inversify' -import type { LoginCredentials, SignUpCredentials } from '@memebattle/cas-services' +import type { LoginCredentials, SignUpCredentials, SuccessLoginData, SuccessSignUpData } from '@memebattle/cas-services' import { createCasServices } from '@memebattle/cas-services' import { CAS_URL, CAS_PARTNER_ID, PUBLIC_KEY } from '../../config' +export interface IAuthService { + loginService(credentials: LoginCredentials): Promise + signUpService(credentials: SignUpCredentials): Promise + verifyTokenService(token: string): Promise<{ userId: string } | null> +} + @injectable() -export class AuthService { +export class AuthService implements IAuthService { private casServices = createCasServices({ casURI: CAS_URL, partnerId: CAS_PARTNER_ID, publicKey: PUBLIC_KEY }) public async loginService(credentials: LoginCredentials) { diff --git a/apps/ligretto-gameplay-backend/src/services/auth/index.ts b/apps/ligretto-gameplay-backend/src/services/auth/index.ts index 83ee508b..c8902efe 100644 --- a/apps/ligretto-gameplay-backend/src/services/auth/index.ts +++ b/apps/ligretto-gameplay-backend/src/services/auth/index.ts @@ -1 +1 @@ -export { AuthService } from './auth.service' +export { AuthService, IAuthService } from './auth.service' diff --git a/apps/ligretto-gameplay-backend/src/services/ligretto-core/index.ts b/apps/ligretto-gameplay-backend/src/services/ligretto-core/index.ts index 111df824..a9625d63 100644 --- a/apps/ligretto-gameplay-backend/src/services/ligretto-core/index.ts +++ b/apps/ligretto-gameplay-backend/src/services/ligretto-core/index.ts @@ -1 +1 @@ -export { LigrettoCoreService } from './ligretto-core.service' +export { LigrettoCoreService, ILigrettoCoreService } from './ligretto-core.service' diff --git a/apps/ligretto-gameplay-backend/src/services/ligretto-core/ligretto-core.service.ts b/apps/ligretto-gameplay-backend/src/services/ligretto-core/ligretto-core.service.ts index 88d0e04f..0b2d7fdc 100644 --- a/apps/ligretto-gameplay-backend/src/services/ligretto-core/ligretto-core.service.ts +++ b/apps/ligretto-gameplay-backend/src/services/ligretto-core/ligretto-core.service.ts @@ -4,8 +4,13 @@ import type { AxiosResponse } from 'axios' import type { Game, RoundInfo, CreateGameRequest, CreateGameResponse, SaveRoundRequest, SaveRoundResponse } from '@memebattle/ligretto-shared' import { LIGRETTO_CORE_URL } from '../../config' +export interface ILigrettoCoreService { + createGameService(): Promise + saveGameRoundService(gameId: Game['id'], round: RoundInfo): Promise +} + @injectable() -export class LigrettoCoreService { +export class LigrettoCoreService implements ILigrettoCoreService { private request = axios.create({ baseURL: LIGRETTO_CORE_URL, }) diff --git a/apps/ligretto-gameplay-backend/src/socket-io-server.ts b/apps/ligretto-gameplay-backend/src/socket-io-server.ts index 89e082af..abdbeaf5 100644 --- a/apps/ligretto-gameplay-backend/src/socket-io-server.ts +++ b/apps/ligretto-gameplay-backend/src/socket-io-server.ts @@ -1,7 +1,7 @@ import { Server } from 'socket.io' import { createServer } from 'node:http' import { LIGRETTO_GAMEPLAY_SOCKET_PORT } from './config' -import type { WebSocketHandler } from './websocket-handlers' +import type { IWebSocketHandler } from './websocket-handlers' import { IOC } from './inversify.config' import { IOC_TYPES } from './IOC_TYPES' import { promClient } from './metrics' @@ -30,7 +30,7 @@ export const io = new Server(httpServer, { }, }) -export const webSocketHandler = IOC.get(IOC_TYPES.WebSocketHandler) +export const webSocketHandler = IOC.get(IOC_TYPES.IWebSocketHandler) webSocketHandler.connect(io) diff --git a/apps/ligretto-gameplay-backend/src/websocket-handlers/handler.ts b/apps/ligretto-gameplay-backend/src/websocket-handlers/handler.ts index b91d388d..8716cb9a 100644 --- a/apps/ligretto-gameplay-backend/src/websocket-handlers/handler.ts +++ b/apps/ligretto-gameplay-backend/src/websocket-handlers/handler.ts @@ -1,30 +1,30 @@ -import { injectable, inject } from 'inversify' +import { injectable, inject, named } from 'inversify' import type { Server, Socket } from 'socket.io' import { IOC_TYPES } from '../IOC_TYPES' -import { GameplayController } from '../controllers/gameplay-controller' -import { GamesController } from '../controllers/games-controller' -import { UserService } from '../entities/user' -import { BotController } from '../controllers/bot-controller' +import { IGamesController } from '../controllers/games-controller' +import { IUserService } from '../entities/user' +import { IController } from '../controllers/controller' import { authMiddleware } from '../middlewares' import type { AnyAction } from '../types/any-action' import { socketIOConnectionsCountMetric, socketIOConnectionsCountTotalMetric } from '../metrics' -export interface WebSocketHandler { - connectionHandler(socket: Socket): void +export interface IWebSocketHandler { + connect(socketServer: Server): void } @injectable() -export class WebSocketHandler implements WebSocketHandler { - @inject(IOC_TYPES.GameplayController) private gameplayController: GameplayController - @inject(IOC_TYPES.GamesController) private gamesController: GamesController - @inject(IOC_TYPES.BotController) private botController: BotController - @inject(IOC_TYPES.UserService) private userService: UserService +export class WebSocketHandler implements IWebSocketHandler { + @inject(IOC_TYPES.IController) @named('gameplayController') private gameplayController: IController + @inject(IOC_TYPES.IGamesController) private gamesController: IGamesController + @inject(IOC_TYPES.IController) @named('botController') private botController: IController + @inject(IOC_TYPES.IUserService) private userService: IUserService connect(socketServer: Server) { socketServer.use(authMiddleware).on('connection', socket => this.connectionHandler(socket)) } - public async connectionHandler(socket: Socket): Promise { + // XXX: why is this method public? It is used only inside itself + private async connectionHandler(socket: Socket): Promise { socketIOConnectionsCountMetric.inc() socketIOConnectionsCountTotalMetric.inc() diff --git a/apps/ligretto-gameplay-backend/src/websocket-handlers/index.ts b/apps/ligretto-gameplay-backend/src/websocket-handlers/index.ts index 31cf6029..b70a6741 100644 --- a/apps/ligretto-gameplay-backend/src/websocket-handlers/index.ts +++ b/apps/ligretto-gameplay-backend/src/websocket-handlers/index.ts @@ -1 +1 @@ -export { WebSocketHandler } from './handler' +export { WebSocketHandler, IWebSocketHandler } from './handler'