From 0b7c9abefa6e90c3e743ec8febdabd1c51d1b68b Mon Sep 17 00:00:00 2001 From: Maciej Kudas Date: Sat, 11 Apr 2020 19:32:13 +0200 Subject: [PATCH] feat(game): Extract game modes to dedicated classes --- bin/compile.php | 2 +- bin/doc-downloader.php | 6 +- composer.lock | 20 +- src/Commands/Command.js | 29 ++ src/Game/GameManager.js | 341 +++--------------- src/Game/Modes/AbstractGameMode.js | 204 +++++++++++ src/Game/Modes/BestOfGameMode.js | 57 +++ src/Game/Modes/NoopGameMode.js | 51 +++ src/Game/Modes/RaceToGameMode.js | 219 +++++++++++ src/Game/Modes/RandomGameMode.js | 171 +++++++++ src/Utils/Utils.js | 28 +- src/Utils/constants.js | 14 + src/server.js | 85 ++--- ...0_22_30.js => stubs-2020-04-15_18_01_16.js | 40 +- 14 files changed, 882 insertions(+), 385 deletions(-) create mode 100644 src/Game/Modes/AbstractGameMode.js create mode 100644 src/Game/Modes/BestOfGameMode.js create mode 100644 src/Game/Modes/NoopGameMode.js create mode 100644 src/Game/Modes/RaceToGameMode.js create mode 100644 src/Game/Modes/RandomGameMode.js rename stubs-2020-04-12_20_22_30.js => stubs-2020-04-15_18_01_16.js (98%) diff --git a/bin/compile.php b/bin/compile.php index 59d84e8..f5fa5f0 100644 --- a/bin/compile.php +++ b/bin/compile.php @@ -35,7 +35,7 @@ function root_path(string $path, string ...$paths): string { '--js_module_root', root_path('src'), '--js', root_path('src', 'configuration.js'), '--js', root_path('src', 'server.js'), - '--js', root_path('src', '*', '*.js'), + '--js', root_path('src', '*', '**.js'), "--js='!**.test.js'", '--strict_mode_input', diff --git a/bin/doc-downloader.php b/bin/doc-downloader.php index a0ea91b..5057d0d 100644 --- a/bin/doc-downloader.php +++ b/bin/doc-downloader.php @@ -1008,15 +1008,15 @@ public function offsetUnset($offset): void { /** * @constructor * - * @param {RoomConfigObject} roomConfig + * @param {!RoomConfigObject} roomConfig * - * @return {RoomObject} + * @return {!RoomObject} * * @link https://github.com/haxball/haxball-issues/wiki/Headless-Host Documentation * @link https://html5.haxball.com/headless Headless server host */ function HBInit(roomConfig) { -}; +} CONSTRUCTOR; file_put_contents(__DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'stubs-' . date('Y-m-d_H_i_s') . '.js', implode("\n\n", $classes)); diff --git a/composer.lock b/composer.lock index 0ca5038..1fe90fa 100644 --- a/composer.lock +++ b/composer.lock @@ -426,12 +426,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "0f73cf4b4b9227eb8845723bc2a8869bc4dd6e8f" + "reference": "09afa996c68c18f49e6487b06adcb2ef27da61fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/0f73cf4b4b9227eb8845723bc2a8869bc4dd6e8f", - "reference": "0f73cf4b4b9227eb8845723bc2a8869bc4dd6e8f", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/09afa996c68c18f49e6487b06adcb2ef27da61fa", + "reference": "09afa996c68c18f49e6487b06adcb2ef27da61fa", "shasum": "" }, "conflict": { @@ -552,7 +552,7 @@ "serluck/phpwhois": "<=4.2.6", "shopware/shopware": "<5.3.7", "silverstripe/admin": ">=1.0.3,<1.0.4|>=1.1,<1.1.1", - "silverstripe/assets": ">=1,<1.3.5|>=1.4,<1.4.4", + "silverstripe/assets": ">=1,<1.4.7|>=1.5,<1.5.2", "silverstripe/cms": "<4.3.6|>=4.4,<4.4.4", "silverstripe/comments": ">=1.3,<1.9.99|>=2,<2.9.99|>=3,<3.1.1", "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3", @@ -682,17 +682,7 @@ } ], "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it", - "funding": [ - { - "url": "https://github.com/Ocramius", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/roave/security-advisories", - "type": "tidelift" - } - ], - "time": "2020-03-31T14:30:16+00:00" + "time": "2020-04-15T04:56:51+00:00" }, { "name": "symfony/var-dumper", diff --git a/src/Commands/Command.js b/src/Commands/Command.js index a2957fb..57a0440 100644 --- a/src/Commands/Command.js +++ b/src/Commands/Command.js @@ -109,4 +109,33 @@ export class Command { return !!this._handler.call(null, player, arg, message); } + + /** + * @param command + * + * @return {{arguments: !Array., options: !Object.}} + */ + static getArgsAndOptions(command) { + const regex = /\b(\w+)(?:\s*=\s*([^\s]+))?/g, + args = { + arguments: [], + options: {}, + }; + let m; + + while (null !== (m = regex.exec(command))) { + // This is necessary to avoid infinite loops with zero-width matches + if (m.index === regex.lastIndex) { + regex.lastIndex++; + } + + if (undefined === m[2]) { + args.arguments.push(m[1]); + } else { + args.options[m[1].toLowerCase()] = m[2]; + } + } + + return args; + } } diff --git a/src/Game/GameManager.js b/src/Game/GameManager.js index a6e7d83..d555b58 100644 --- a/src/Game/GameManager.js +++ b/src/Game/GameManager.js @@ -1,14 +1,18 @@ 'use strict'; -import {PlayersManager} from "../Players/PlayersManager"; +import {PlayersManager} from '../Players/PlayersManager'; import {AbstractStorage} from '../Storage/AbstractStorage'; import {ScopedStorage} from '../Storage/ScopedStorage'; -import {ASSIST_VALID_FOR, TeamID} from "../Utils/constants"; -import {popNRandomElements} from "../Utils/Utils"; +import {ASSIST_VALID_FOR, TeamID} from '../Utils/constants'; +import {AbstractGameMode} from './Modes/AbstractGameMode'; +import {BestOfGameMode} from './Modes/BestOfGameMode'; +import {NoopGameMode} from './Modes/NoopGameMode'; +import {RaceToGameMode} from './Modes/RaceToGameMode'; +import {RandomGameMode} from './Modes/RandomGameMode'; export const GAME_MODES = { BO: 'bo', - UT: 'ut', + RT: 'rt', RANDOM: 'random', }; @@ -28,30 +32,6 @@ export class GameManager { this._goals.push(data); } - get mode() { - return this._mode; - } - - get limit() { - return this._limit; - } - - get teamSize() { - return this._teamSize; - } - - get started() { - return this._started; - } - - get finished() { - return this._finished; - } - - get matchesPlayed() { - return this._matchesPlayed; - } - get ballTouches() { if (this._ballTouches.length > 1 && this._ballTouches[1].touchedAt + ASSIST_VALID_FOR < Date.now()) { this._ballTouches.pop(); @@ -61,24 +41,25 @@ export class GameManager { } /** - * @param {RoomObject} roomObject - * @param {PlayersManager} playersManager - * @param {AbstractStorage} storage + * @param {!RoomObject} roomObject + * @param {!PlayersManager} playersManager + * @param {!AbstractStorage} storage */ constructor(roomObject, playersManager, storage) { /** - * @type {RoomObject} + * @type {!RoomObject} * @private */ this._roomObject = roomObject; + /** - * @type {PlayersManager} + * @type {!PlayersManager} * @private */ this._playersManager = playersManager; /** - * @type {ScopedStorage} + * @type {!ScopedStorage} * @private */ this._storage = new ScopedStorage(storage, 'game.'); @@ -90,40 +71,10 @@ export class GameManager { this._goals = []; /** - * @type {string} - * @private - */ - this._mode = GAME_MODES.BO; - - /** - * @type {number} - * @private - */ - this._limit = 3; - - /** - * @type {number} - * @private - */ - this._teamSize = 3; - - /** - * @type {boolean} + * @type {!AbstractGameMode} * @private */ - this._started = false; - - /** - * @type {boolean} - * @private - */ - this._finished = false; - - /** - * @type {number} - * @private - */ - this._matchesPlayed = 0; + this._modeObject = new NoopGameMode(roomObject); /** * @type {Array.} @@ -131,15 +82,6 @@ export class GameManager { */ this._ballTouches = []; - /** - * @type {{red: number, blue: number}} - * @private - */ - this._victories = { - red: 0, - blue: 0, - }; - if (!this._storage.has('score-limit')) { this.setScoreLimit(3); } @@ -214,251 +156,58 @@ export class GameManager { this._ballTouches = []; } - get victories() { - return this._victories; - } - /** - * @param {string} mode - * @param {number} limit - * @param {number} teamSize + * @param {string} mode + * @param {!Object.} args * - * @throws Error + * @throws {Error} When desired game mode does not exist or could not be started */ - start(mode = GAME_MODES.BO, limit = 3, teamSize = 3) { - console.log('GAME.start', arguments); - - if (this.isInProgress()) { - throw new Error(`Nie można rozpocząć nowej gry nadzorowanej, dopóki taka jeszcze trwa. Zatrzymaj ją poprzez «!match stop» lub poczekaj do jej zakończenia.`); - } - - if (GAME_MODES.BO === mode && 1 !== limit % 2) { - throw new Error(`W trybie ${mode} limit zwycięstw musi być nieparzysty, podano ${limit}.`); - } - - if (teamSize < 1) { - throw new Error(`Rozmiar drużyny nie może być mniejszy niż jeden, podano ${teamSize}`); - } - - this._started = true; - this._finished = false; - this._mode = mode; - this._limit = limit; - this._teamSize = teamSize; - - this._roomObject.setTeamsLock(true); - - const playerList = this._roomObject.getPlayerList(); - - if (playerList.length < 2) { - throw new Error(`Gra nie została automatycznie rozpoczęta. Ilość graczy na serwerze jest mniejsza od 2.`); - } - - this._roomObject.startGame(); - } - - stop() { - console.log('GAME.stop'); - - this._started = false; - this._finished = false; - this._victories = {red: 0, blue: 0}; - this._matchesPlayed = 0; - - this._roomObject.setTeamsLock(false); - this._roomObject.stopGame(); - } - - /** - * @param {ScoresObject} scores - */ - registerVictory(scores) { - console.log('GAME.registerVictory', scores); - - if (this._started) { - ++this._matchesPlayed; - - if (scores.red > scores.blue) { - ++this._victories.red; - } else if (scores.blue > scores.red) { - ++this._victories.blue; - } else { - // Remis - --this._matchesPlayed; - } - - switch (this._mode) { + start(mode, args) { + if ('previous' !== mode) { + switch (mode) { case GAME_MODES.BO: - let winnerPoints, looserPoints; - - if (this._victories.red === this._victories.blue) { - break; - } - - if (this._victories.red > this._victories.blue) { - winnerPoints = this._victories.red; - looserPoints = this._victories.blue; - } else { - winnerPoints = this._victories.blue; - looserPoints = this._victories.red; - } - - if (this._matchesPlayed >= Math.ceil((this._limit + 1) / 2)) { - this._finished = true; - } + this._modeObject = new BestOfGameMode(this._roomObject, args); break; - case GAME_MODES.UT: - if (this._matchesPlayed === this._limit) { - this._finished = true; - } + case GAME_MODES.RT: + this._modeObject = new RaceToGameMode(this._roomObject, args); break; case GAME_MODES.RANDOM: - if (this._limit > 0 && this._matchesPlayed >= this._limit) { - this._finished = true; - } + this._modeObject = new RandomGameMode(this._roomObject, args); break; default: - throw new Error(`Unknown game mode: ${this._mode}`); - } - - if (!this._finished) { - const self = this; - - setTimeout(function () { - const playerList = self._roomObject.getPlayerList(); - - switch (self._mode) { - case GAME_MODES.BO: - case GAME_MODES.UT: - const redTeam = playerList.filter((player) => TeamID.RedTeam === player.team), - blueTeam = playerList.filter((player) => TeamID.BlueTeam === player.team); - - self._roomObject.stopGame(); - self._roomObject.sendAnnouncement("🏆 Zmieniam drużyny miejscami"); - - redTeam.forEach((player) => self._roomObject.setPlayerTeam(player.id, TeamID.BlueTeam)); - blueTeam.forEach((player) => self._roomObject.setPlayerTeam(player.id, TeamID.RedTeam)); - - const redVictories = self._victories.red; - - self._victories.red = self._victories.blue; - self._victories.blue = redVictories; - - self._roomObject.startGame(); - break; - - case GAME_MODES.RANDOM: - self._roomObject.stopGame(); - self._roomObject.sendAnnouncement("🏆 Losuję nowe drużyny"); - - const teamSize = self.teamSize > 0 && playerList.length >= self.teamSize * 2 ? - self.teamSize : - playerList.length / 2; - - playerList.filter(player => TeamID.Spectators !== player.team) - .forEach(player => self._roomObject.setPlayerTeam(player.id, TeamID.Spectators)); - - popNRandomElements(playerList, teamSize) - .forEach(player => self._roomObject.setPlayerTeam(player.id, TeamID.RedTeam)); - - popNRandomElements(playerList, teamSize) - .forEach(player => self._roomObject.setPlayerTeam(player.id, TeamID.BlueTeam)); - - if (self.teamSize <= 0 && playerList.length > 0) { - playerList.forEach(player => self._roomObject.setPlayerTeam(player.id, Math.random() < 0.5 ? - TeamID.RedTeam : - TeamID.BlueTeam)); - } - - self._roomObject.startGame(); - break; - - default: - self._roomObject.stopGame(); - break; - } - }, 2000); - } else { - switch (this._mode) { - case GAME_MODES.BO: - case GAME_MODES.UT: - this._roomObject.sendAnnouncement("🏆 Przenoszę zwycięską drużynę do czerwonych. Kto śmie stawić im czoła? 🙀"); - - const availablePlayers = [], - victoriousTeam = this._victories.red > this._victories.blue ? - TeamID.RedTeam : - TeamID.BlueTeam; - - this._roomObject.getPlayerList() - .forEach((player) => { - if (victoriousTeam === player.team) { - this._roomObject.setPlayerTeam(player.id, TeamID.RedTeam); - } else { - this._roomObject.setPlayerTeam(player.id, TeamID.Spectators); - - availablePlayers.push(player.id); - } - }); - - this._roomObject.stopGame(); - - if (availablePlayers.length > 0) { - const randomCaptain = availablePlayers[Math.floor(Math.random() * availablePlayers.length)]; - - this._roomObject.setPlayerTeam(randomCaptain, TeamID.BlueTeam); - - this._roomObject.sendAnnouncement(`🏆 Niebiescy, powitajcie nowego kapitana: ${this._roomObject.getPlayer(randomCaptain).name}!`); - } - break; - - case GAME_MODES.RANDOM: - this._roomObject.sendAnnouncement("🏆 Turniej losowych drużyn zakończony! 🎲"); - - this.end(); - break; - - default: - this._roomObject.stopGame(); - break; - } + throw new Error(`Unknown game mode: ${mode}`); } } + + this._modeObject.start(); } - end() { - console.log('GAME.end'); - - if (this._started && this._finished) { - if (this._victories.red > this._victories.blue) { - this._roomObject.sendAnnouncement(`🏆 Turniej zakończony! Zwycięża drużyna 🔴 ${this._victories.red}:${this._victories.blue} 🔵. Brawa! 👏`); - } else if (this._victories.blue > this._victories.red) { - this._roomObject.sendAnnouncement(`🏆 Turniej zakończony! Zwycięża drużyna 🔵 ${this._victories.blue}:${this._victories.red} 🔴. Brawa! 👏`); - } else { - this._roomObject.sendAnnouncement(`🏆 Turniej zakończony! Remis 🤷‍♂️`); - } + restart() { + this._modeObject.restart(); + } - this._finished = false; - this._victories = {red: 0, blue: 0}; - this._matchesPlayed = 0; - } + /** + * @param {!ScoresObject} scores + */ + registerVictory(scores) { + this._modeObject.registerVictory(scores); + } + stop() { this._goals = []; this.resetBallTouches(); } - isInProgress() { - return this._started && !this._finished; - } + end() { + this.stop(); - /** - * @return {Object} - */ - getStats() { - return {}; + this._modeObject.stop(); + + this._modeObject = new NoopGameMode(this._roomObject); } } diff --git a/src/Game/Modes/AbstractGameMode.js b/src/Game/Modes/AbstractGameMode.js new file mode 100644 index 0000000..49eb84c --- /dev/null +++ b/src/Game/Modes/AbstractGameMode.js @@ -0,0 +1,204 @@ +import {TeamID} from '../../Utils/constants'; +import {popNRandomElements, popRandomElement} from '../../Utils/Utils'; + +/** + * @abstract + */ +export class AbstractGameMode { + + /** + * @param {!RoomObject} roomObject + */ + constructor(roomObject) { + console.log('AbstractGameMode.constructor'); + + /** + * @type {!RoomObject} + * @protected + */ + this.roomObject = roomObject; + } + + /** + * Starts a game. + * + * @abstract + */ + start() { + // throw new Error('Method start() not implemented'); + } + + /** + * Register new victory for a team + * + * @param {!ScoresObject} scores + * + * @abstract + */ + registerVictory(scores) { + // throw new Error('Method registerVictory() not implemented'); + } + + /** + * Stops a game, but does not quit this game mode. + * + * @abstract + */ + stop() { + // throw new Error('Method stop() not implemented'); + } + + /** + * Restarts a game. + * + * @abstract + */ + restart() { + // throw new Error('Method restart() not implemented'); + } + + /** + * Returns true when game is started in given mode and false otherwise. + * + * @return {boolean} + * + * @abstract + */ + isInProgress() { + // throw new Error('Method isInProgress() not implemented'); + } + + /** + * Assigns players to teams randomly. + * + * @param {number} teamSize + * + * @protected + */ + _randomizeTeams(teamSize) { + const playerList = this.roomObject.getPlayerList(), + maxTeamSize = teamSize > 0 && playerList.length >= teamSize * 2 ? + teamSize : + playerList.length / 2; + + playerList.filter(player => TeamID.Spectators !== player.team) + .forEach(player => this.roomObject.setPlayerTeam(player.id, TeamID.Spectators)); + + popNRandomElements(playerList, maxTeamSize) + .forEach(player => this.roomObject.setPlayerTeam(player.id, TeamID.RedTeam)); + + popNRandomElements(playerList, maxTeamSize) + .forEach(player => this.roomObject.setPlayerTeam(player.id, TeamID.BlueTeam)); + + if (maxTeamSize <= 0 && playerList.length > 0) { + playerList.forEach(player => this.roomObject.setPlayerTeam(player.id, Math.random() < 0.5 ? + TeamID.RedTeam : + TeamID.BlueTeam)); + } + } + + /** + * Swaps players between teams. + * + * @protected + */ + _swapTeams() { + const playersList = this.roomObject.getPlayerList(), + redTeam = playersList.filter(player => TeamID.RedTeam === player.team), + blueTeam = playersList.filter(player => TeamID.BlueTeam === player.team); + + redTeam.forEach(player => this.roomObject.setPlayerTeam(player.id, TeamID.BlueTeam)); + blueTeam.forEach(player => this.roomObject.setPlayerTeam(player.id, TeamID.RedTeam)); + } + + /** + * Adds free players from the bench to teams. + * + * @param {number} teamSize + * + * @protected + */ + _fillTeamsWithFreePlayersFromBench(teamSize) { + const playersList = this.roomObject.getPlayerList(), + redTeam = playersList.filter(player => TeamID.RedTeam === player.team), + blueTeam = playersList.filter(player => TeamID.BlueTeam === player.team); + + if (redTeam.length < teamSize || blueTeam.length < teamSize) { + const chosenPlayers = popNRandomElements(playersList, playersList.length - redTeam.length - blueTeam.length); + + if (chosenPlayers.length > 0) { + this.roomObject.sendAnnouncement('Przydzielam zawodników z ławki rezerwowych do drużyn'); + + let difference = redTeam.length - blueTeam.length, + targetTeam; + + if (0 !== difference) { + targetTeam = difference > 0 ? + TeamID.BlueTeam : + TeamID.RedTeam; + + difference = Math.abs(difference); + + while (difference-- > 0 && chosenPlayers.length > 0) { + this.roomObject.setPlayerTeam(chosenPlayers.pop().id, targetTeam); + } + } + + targetTeam = Math.random() >= 0.5 ? + TeamID.RedTeam : + TeamID.BlueTeam; + + while (chosenPlayers.length > 0) { + this.roomObject.setPlayerTeam(chosenPlayers.pop().id, targetTeam); + + targetTeam = targetTeam === TeamID.RedTeam ? + TeamID.BlueTeam : + TeamID.RedTeam; + } + } + } + } + + /** + * Balances the number of players between teams. + * + * @protected + */ + _autoTeamsBalance() { + const playersList = this.roomObject.getPlayerList(), + redTeam = playersList.filter(player => TeamID.RedTeam === player.team), + blueTeam = playersList.filter(player => TeamID.BlueTeam === player.team); + + let difference = redTeam.length - blueTeam.length; + + if (Math.abs(difference) >= 2) { + this.roomObject.sendAnnouncement('Wyrównuję ilość zawodników w drużynach'); + + /** + * @type {Array.} + */ + let fromTeam; + + /** + * @type {TeamID} + */ + let toTeam; + + if (difference > 0) { + fromTeam = redTeam; + toTeam = TeamID.BlueTeam; + } else { + fromTeam = blueTeam; + toTeam = TeamID.RedTeam; + } + + difference = Math.abs(difference); + + while (difference >= 2) { + this.roomObject.setPlayerTeam(popRandomElement(fromTeam).id, toTeam); + + difference -= 2; + } + } + } +} diff --git a/src/Game/Modes/BestOfGameMode.js b/src/Game/Modes/BestOfGameMode.js new file mode 100644 index 0000000..9eb7900 --- /dev/null +++ b/src/Game/Modes/BestOfGameMode.js @@ -0,0 +1,57 @@ +import {RaceToGameMode} from './RaceToGameMode'; + +/** + * @extends {RaceToGameMode} + */ +export class BestOfGameMode extends RaceToGameMode { + + /** + * @param {!RoomObject} roomObject + * @param {!Object.} args + */ + constructor(roomObject, args) { + super(roomObject, args); + + console.log('BestOfGameMode.constructor'); + } + + /** + * @inheritDoc + */ + _parseLimit(limitArgument) { + const limit = parseInt(limitArgument ?? BestOfGameMode.DEFAULT_LIMIT, 10); + + if (limit < 0) { + throw new Error(`W trybie "Best Of" limit zwycięstw nie może być mniejszy niż 0, podano ${limit}.`); + } + + if (1 !== limit % 2) { + throw new Error(`W trybie "Best Of" limit zwycięstw musi być nieparzysty, podano ${limit}.`); + } + + return limit; + } + + /** + * @inheritDoc + */ + _parseTeamSize(teamSizeArgument) { + const teamSize = parseInt(teamSizeArgument ?? BestOfGameMode.DEFAULT_TEAM_SIZE, 10); + + if (teamSize < 1) { + throw new Error(`W trybie "Best Of" rozmiar drużyny nie może być mniejszy niż jeden, podano ${teamSize}`); + } + + return teamSize; + } + + /** + * @inheritDoc + */ + _shouldMatchBeFinished() { + return Math.max(this.victories.red, this.victories.blue) >= (this.limit + 1) / 2; + } +} + +BestOfGameMode.DEFAULT_LIMIT = 3; +BestOfGameMode.DEFAULT_TEAM_SIZE = 3; diff --git a/src/Game/Modes/NoopGameMode.js b/src/Game/Modes/NoopGameMode.js new file mode 100644 index 0000000..9bd3aa7 --- /dev/null +++ b/src/Game/Modes/NoopGameMode.js @@ -0,0 +1,51 @@ +import {AbstractGameMode} from './AbstractGameMode'; + +/** + * @extends {AbstractGameMode} + */ +export class NoopGameMode extends AbstractGameMode { + + /** + * @param {!RoomObject} roomObject + */ + constructor(roomObject) { + super(roomObject); + + console.log('NoopGameMode.constructor'); + } + + /** + * @inheritDoc + */ + start() { + // + } + + /** + * @inheritDoc + */ + registerVictory(scores) { + // + } + + /** + * @inheritDoc + */ + stop() { + // + } + + /** + * @inheritDoc + */ + restart() { + // + } + + /** + * @inheritDoc + */ + isInProgress() { + return false; + } +} diff --git a/src/Game/Modes/RaceToGameMode.js b/src/Game/Modes/RaceToGameMode.js new file mode 100644 index 0000000..a979d71 --- /dev/null +++ b/src/Game/Modes/RaceToGameMode.js @@ -0,0 +1,219 @@ +import {DISPLAY_TIME_SCORED} from '../../Utils/constants'; +import {AbstractGameMode} from './AbstractGameMode'; + +/** + * @extends {AbstractGameMode} + */ +export class RaceToGameMode extends AbstractGameMode { + + /** + * @param {!RoomObject} roomObject + * @param {!Object.} args + */ + constructor(roomObject, args) { + super(roomObject); + + console.log('RaceToGameMode.constructor'); + + /** + * @type {number} + * @protected + */ + this.limit = this._parseLimit(args['limit']); + + /** + * @type {number} + * @protected + */ + this.teamSize = this._parseTeamSize(args['teamsize']); + + /** + * @type {boolean} + * @protected + */ + this.started = false; + + /** + * @type {boolean} + * @protected + */ + this.finished = false; + + /** + * @type {number} + * @protected + */ + this.matchesPlayed = 0; + + /** + * @type {{red: number, blue: number}} + * @protected + */ + this.victories = { + red: 0, + blue: 0, + }; + } + + /** + * @inheritDoc + */ + start() { + if (this.isInProgress()) { + throw new Error('Gra została już rozpoczęta.'); + } + + this.roomObject.setTeamsLock(true); + + const playerList = this.roomObject.getPlayerList(); + + if (playerList.length < 2) { + throw new Error('Gra nie została automatycznie rozpoczęta. Ilość graczy na serwerze jest mniejsza od 2.'); + } + + this.roomObject.sendAnnouncement(`🏆 Rozpoczynam turniej: typ - Race To, limit zwycięstw - ${this.limit}, maks. rozmiar drużyny - ${this.teamSize}`); + + this.started = true; + this.finished = false; + this.victories = {red: 0, blue: 0}; + this.matchesPlayed = 0; + + this._fillTeamsWithFreePlayersFromBench(this.teamSize); + this._autoTeamsBalance(); + + this.roomObject.startGame(); + } + + /** + * @inheritDoc + */ + registerVictory(scores) { + if (this.started) { + ++this.matchesPlayed; + + if (scores.red > scores.blue) { + ++this.victories.red; + } else if (scores.blue > scores.red) { + ++this.victories.blue; + } else { + // Remis + --this.matchesPlayed; + } + + if (this.limit > 0 && this._shouldMatchBeFinished()) { + this.finished = true; + } + } + + if (!this.finished) { + setTimeout(() => { + this.roomObject.stopGame(); + this.roomObject.sendAnnouncement('🏆 Zamieniam drużyny miejscami'); + + this._swapTeams(); + this._fillTeamsWithFreePlayersFromBench(this.teamSize); + this._autoTeamsBalance(); + + this.roomObject.startGame(); + }, DISPLAY_TIME_SCORED); + } else { + this.stop(); + } + } + + /** + * @inheritDoc + */ + stop() { + if (this.started && this.finished) { + if (this.victories.red > this.victories.blue) { + this.roomObject.sendAnnouncement(`🏆 Turniej zakończony! Zwycięża drużyna 🔴 ${this.victories.red}:${this.victories.blue} 🔵. Brawa! 👏`); + } else if (this.victories.blue > this.victories.red) { + this.roomObject.sendAnnouncement(`🏆 Turniej zakończony! Zwycięża drużyna 🔵 ${this.victories.blue}:${this.victories.red} 🔴. Brawa! 👏`); + } else { + this.roomObject.sendAnnouncement('🏆 Turniej zakończony! Remis 🤷‍♂️'); + } + } + + this.started = false; + this.finished = false; + + this.roomObject.setTeamsLock(false); + this.roomObject.stopGame(); + } + + /** + * @inheritDoc + */ + restart() { + this.started = false; + this.finished = false; + + this.roomObject.stopGame(); + + this.start(); + } + + /** + * @inheritDoc + */ + isInProgress() { + return this.started && !this.finished; + } + + /** + * @param {string|undefined} limitArgument + * + * @return {number} + * + * @protected + */ + _parseLimit(limitArgument) { + const limit = parseInt(limitArgument ?? RaceToGameMode.DEFAULT_LIMIT, 10); + + if (limit < 0) { + throw new Error(`W trybie "Race To" limit zwycięstw nie może być mniejszy niż 0, podano ${limit}.`); + } + + return limit; + } + + /** + * @param {string|undefined} teamSizeArgument + * + * @return {number} + * + * @protected + */ + _parseTeamSize(teamSizeArgument) { + const teamSize = parseInt(teamSizeArgument ?? RaceToGameMode.DEFAULT_TEAM_SIZE, 10); + + if (teamSize < 1) { + throw new Error(`W trybie "Race To" rozmiar drużyny nie może być mniejszy niż jeden, podano ${teamSize}`); + } + + return teamSize; + } + + /** + * Determines whether the match should be finished or not. + * + * @return {boolean} + * @protected + */ + _shouldMatchBeFinished() { + return Math.max(this.victories.red, this.victories.blue) >= this.limit; + } + + /** + * @inheritDoc + */ + _swapTeams() { + super._swapTeams(); + + [this.victories.red, this.victories.blue] = [this.victories.blue, this.victories.red]; + } +} + +RaceToGameMode.DEFAULT_LIMIT = 3; +RaceToGameMode.DEFAULT_TEAM_SIZE = 3; diff --git a/src/Game/Modes/RandomGameMode.js b/src/Game/Modes/RandomGameMode.js new file mode 100644 index 0000000..8f8c4c0 --- /dev/null +++ b/src/Game/Modes/RandomGameMode.js @@ -0,0 +1,171 @@ +import {DISPLAY_TIME_SCORED} from '../../Utils/constants'; +import {AbstractGameMode} from './AbstractGameMode'; + +/** + * @extends {AbstractGameMode} + */ +export class RandomGameMode extends AbstractGameMode { + + /** + * @param {!RoomObject} roomObject + * @param {!Object.} args + */ + constructor(roomObject, args) { + super(roomObject); + + console.log('RandomGameMode.constructor'); + + /** + * @type {number} + * @private + */ + this._limit = parseInt(args['limit'] ?? RandomGameMode.DEFAULT_LIMIT, 10); + + if (this._limit < 0) { + throw new Error(`W trybie "Random" limit zwycięstw nie może być mniejszy niż 0, podano ${this._limit}.`); + } + + /** + * @type {number} + * @private + */ + this._teamSize = parseInt(args['teamsize'] ?? RandomGameMode.DEFAULT_TEAM_SIZE, 10); + + if (this._teamSize < 1) { + throw new Error(`W trybie "Random" rozmiar drużyny nie może być mniejszy niż jeden, podano ${this._teamSize}`); + } + + /** + * @type {boolean} + * @private + */ + this._started = false; + + /** + * @type {boolean} + * @private + */ + this._finished = false; + + /** + * @type {number} + * @private + */ + this._matchesPlayed = 0; + + /** + * @type {{red: number, blue: number}} + * @private + */ + this._victories = { + red: 0, + blue: 0, + }; + } + + /** + * @inheritDoc + */ + start() { + if (this.isInProgress()) { + throw new Error('Gra została już rozpoczęta.'); + } + + this.roomObject.setTeamsLock(true); + + const playerList = this.roomObject.getPlayerList(); + + if (playerList.length < 2) { + throw new Error('Gra nie została automatycznie rozpoczęta. Ilość graczy na serwerze jest mniejsza od 2.'); + } + + this.roomObject.sendAnnouncement(`🏆 Rozpoczynam turniej: typ - losowy, limit zwycięstw - ${this._limit}, maks. rozmiar drużyny - ${this._teamSize}`); + + this._started = true; + this._finished = false; + this._victories = {red: 0, blue: 0}; + this._matchesPlayed = 0; + + this._randomizeTeams(this._teamSize); + + this.roomObject.startGame(); + } + + /** + * @inheritDoc + */ + registerVictory(scores) { + if (this._started) { + ++this._matchesPlayed; + + if (scores.red > scores.blue) { + ++this._victories.red; + } else if (scores.blue > scores.red) { + ++this._victories.blue; + } else { + // Remis + --this._matchesPlayed; + } + + if (this._limit > 0 && this._matchesPlayed >= this._limit) { + this._finished = true; + } + } + + if (!this._finished) { + setTimeout(() => { + this.roomObject.stopGame(); + this.roomObject.sendAnnouncement('🏆 Losuję nowe drużyny'); + + this._randomizeTeams(this._teamSize); + + this.roomObject.startGame(); + }, DISPLAY_TIME_SCORED); + } else { + this.stop(); + } + } + + /** + * @inheritDoc + */ + stop() { + if (this._started && this._finished) { + if (this._victories.red > this._victories.blue) { + this.roomObject.sendAnnouncement(`🏆 Turniej zakończony! Zwycięża drużyna 🔴 ${this._victories.red}:${this._victories.blue} 🔵. Brawa! 👏`); + } else if (this._victories.blue > this._victories.red) { + this.roomObject.sendAnnouncement(`🏆 Turniej zakończony! Zwycięża drużyna 🔵 ${this._victories.blue}:${this._victories.red} 🔴. Brawa! 👏`); + } else { + this.roomObject.sendAnnouncement('🏆 Turniej zakończony! Remis 🤷‍♂️'); + } + } + + this._started = false; + this._finished = false; + + this.roomObject.setTeamsLock(false); + this.roomObject.stopGame(); + } + + /** + * @inheritDoc + */ + restart() { + this._started = false; + this._finished = false; + + this.roomObject.stopGame(); + + this.start(); + } + + /** + * @inheritDoc + */ + isInProgress() { + return this._started && !this._finished; + } +} + +RandomGameMode.DEFAULT_LIMIT = 3; +RandomGameMode.DEFAULT_TEAM_SIZE = 3; diff --git a/src/Utils/Utils.js b/src/Utils/Utils.js index 07f1703..f88a555 100644 --- a/src/Utils/Utils.js +++ b/src/Utils/Utils.js @@ -1,8 +1,12 @@ /** * Retrieves N elements from data array randomly. * - * @param {Array} data + * @template T + * + * @param {Array.} data * @param {number} number + * + * @return {Array.} */ export function popNRandomElements(data, number) { if (0 === data.length || number < 1) { @@ -22,6 +26,28 @@ export function popNRandomElements(data, number) { return randomElements; } +/** + * Retrieves N elements from data array randomly. + * + * @template T + * + * @param {Array.} data + * + * @return T|undefined + */ +export function popRandomElement(data) { + if (0 === data.length) { + return undefined; + } + + const randomElementIndex = Math.floor(Math.random() * data.length), + randomElement = data[randomElementIndex]; + + data.splice(randomElementIndex, 1); + + return randomElement; +} + /** * @param {number} time * diff --git a/src/Utils/constants.js b/src/Utils/constants.js index 1d8b6fe..c7bb370 100644 --- a/src/Utils/constants.js +++ b/src/Utils/constants.js @@ -31,3 +31,17 @@ export const TOUCHED_BALL_THRESHOLD = KICKABLE_BALL_THRESHOLD / 2.0; * @const {number} */ export const ASSIST_VALID_FOR = 5 * 1000; + +/** + * How long does "Red/Blue Scores!" text is being displayed (in milliseconds). + * + * @const {number} + */ +export const DISPLAY_TIME_SCORED = 2.5 * 1000; + +/** + * How long does "Red/Blue Scores! Red/Blue team is victorious" text is being displayed (in milliseconds). + * + * @const {number} + */ +export const DISPLAY_TIME_SCORED_AND_VICTORIOUS = 5 * 1000; diff --git a/src/server.js b/src/server.js index 8a2806b..57d1c36 100644 --- a/src/server.js +++ b/src/server.js @@ -347,48 +347,35 @@ Dostępne opcje: new Command("match", (player, arg) => { if (undefined === arg) { - ROOM.sendAnnouncement("Podaj wymagane argumenty. Wpisz «!help match» aby dowiedzieć się więcej.", player.id); + ROOM.sendAnnouncement('Podaj wymagane argumenty. Wpisz «!help match» aby dowiedzieć się więcej.', player.id); return false; } - const regex = /\b(\w+)(?:\s*=\s*([^\s]+))?/g; - const args = {}; - let m; + const args = Command.getArgsAndOptions(arg); - while ((m = regex.exec(arg)) !== null) { - // This is necessary to avoid infinite loops with zero-width matches - if (m.index === regex.lastIndex) { - regex.lastIndex++; - } - - args[m[1].toLowerCase()] = m[2]; - } + switch (args.arguments[0].toLowerCase()) { + case 'start': + try { + GAME.start((args.arguments[1] ?? 'previous').toLowerCase(), args.options); + } catch (e) { + ROOM.sendAnnouncement(`[BŁĄD] ${e.message}`, player.id); + } + break; - console.log(arg, args); + case 'stop': + ROOM.sendAnnouncement(`🏆 Przerywam turniej na żądanie gracza 👉 ${player.name} 👈! 😡`); + GAME.end(); + break; - if (args.hasOwnProperty("start")) { - try { - GAME.start( - (args['mode'] ?? GAME.mode).toLowerCase(), - parseInt(args['limit'] ?? GAME.limit, 10), - parseInt(args['teamsize'] ?? GAME.teamSize, 10), - ); + case 'restart': + ROOM.sendAnnouncement('🏆 Restartuję turniej... 🤦‍♂️'); + GAME.restart(); + break; - ROOM.sendAnnouncement(`🏆 Rozpoczynam turniej: typ - ${GAME.mode}, limit zwycięstw - ${GAME.limit}, maks. rozmiar drużyny - ${GAME.teamSize}`); - } catch (e) { - ROOM.sendAnnouncement(`[BŁĄD] ${e.message}`, player.id); - } - } else if (args.hasOwnProperty("stop")) { - ROOM.sendAnnouncement(`🏆 Przerywam turniej na żądanie ${player.name}! 😡`); - GAME.stop(); - } else if (args.hasOwnProperty("restart")) { - ROOM.sendAnnouncement("🏆 Restartuję turniej... 🤦‍♂️"); - - COMMANDS.execute("match", player, "stop", ""); - COMMANDS.execute("match", player, `start mode=${GAME.mode} limit=${GAME.limit}`, ""); - } else { - ROOM.sendAnnouncement(`Nierozpoznany argument: ${arg}`, player.id); + default: + ROOM.sendAnnouncement(`Nierozpoznany argument: ${arg}`, player.id); + break; } return false; @@ -401,7 +388,7 @@ Aby dowiedzieć się więcej na temat konkretnej akcji, wpisz «!help match [akc Przykłady: !match start !match start mode=bo limit=3 - !match start mode=ut limit=5 teamSize=2 + !match start mode=rt limit=5 teamSize=2 !match restart !match stop @@ -422,13 +409,15 @@ Aby dowiedzieć się więcej na temat konkretnego trybu gry, wpisz «!help match Przykłady: !match start bo limit=3 - !match start ut limit=5 teamSize=2 + !match start rt limit=5 teamSize=2 !match start random limit=5 teamSize=0 + !match start previous Dostępne «tryby» gry: - bo Best Of - rozegranie do n gier (wygrywa drużyna, która jako pierwsza zdobędzie ponad połowę zwycięstw). - ut Up To - rozegranie n gier (wygrywa drużyna, która jako pierwsza wygra n meczy). - random W każdym meczu losowe drużyny.`; + bo Best Of - rozegranie do n gier (wygrywa drużyna, która jako pierwsza zdobędzie ponad połowę zwycięstw). + rt Race To - rozegranie n gier (wygrywa drużyna, która jako pierwsza wygra n meczy). + random W każdym meczu losowe drużyny. + previous Rozpoczyna mecz z poprzednimi ustawieniami (ten tryb może być też pominięty w poleceniu i zostanie wybrany automatycznie).`; } switch (args[1].toLowerCase()) { @@ -444,13 +433,13 @@ Dostępne opcje: limit=3 Limit rozegranych meczy. teamSize=3 Rozmiar drużyny`; - case GAME_MODES.UT: - return `!match start ${GAME_MODES.UT} «opcja1» «opcja2» -Rozpoczyna turniej w trybie Up To - rozegranie n gier (wygrywa drużyna, która jako pierwsza wygra n meczy). + case GAME_MODES.RT: + return `!match start ${GAME_MODES.RT} «opcja1» «opcja2» +Rozpoczyna turniej w trybie Race To - rozegranie n gier (wygrywa drużyna, która jako pierwsza wygra n meczy). Przykłady: - !match start ut limit=3 teamSize=3 - !match start ut limit=0 teamSize=3 + !match start rt limit=3 teamSize=3 + !match start rt limit=0 teamSize=3 Dostępne opcje: limit=3 Limit rozegranych meczy. @@ -608,15 +597,13 @@ ROOM.onGameStop = function () { return; } - console.log('onGameStop', GAME.getStats()); - const goalInfos = GAME.goals; if (goalInfos.length > 0) { - const goalsSummary = ["Podsumowanie goli z meczu:"]; + const goalsSummary = ['Podsumowanie goli z meczu:']; for (const goal of goalInfos) { - goalsSummary.push(` ${TeamID.RedTeam === goal.byTeam ? "🔴" : "🔵"} [${getTime(goal.scoredAt)}] Gol gracza ${PLAYERS.get(goal.goalBy).name}${null !== goal.assistBy ? ` przy asyście gracza ${PLAYERS.get(goal.assistBy).name}` : ''}`); + goalsSummary.push(` ${TeamID.RedTeam === goal.byTeam ? '🔴' : '🔵'} [${getTime(goal.scoredAt)}] Gol gracza ${PLAYERS.get(goal.goalBy).name}${null !== goal.assistBy ? ` przy asyście gracza ${PLAYERS.get(goal.assistBy).name}` : ''}`); } ROOM.sendAnnouncement(goalsSummary.join("\n")); @@ -624,7 +611,7 @@ ROOM.onGameStop = function () { ROOM.sendAnnouncement("Podsumowanie goli z meczu:\nNik nic nie strzelił 😮"); } - GAME.end(); + GAME.stop(); }; ROOM.onPositionsReset = function () { diff --git a/stubs-2020-04-12_20_22_30.js b/stubs-2020-04-15_18_01_16.js similarity index 98% rename from stubs-2020-04-12_20_22_30.js rename to stubs-2020-04-15_18_01_16.js index cfcff58..eb8a09e 100644 --- a/stubs-2020-04-12_20_22_30.js +++ b/stubs-2020-04-15_18_01_16.js @@ -43,7 +43,7 @@ class RoomObject { *
// Check if disc 4 belongs to collision group "ball":
 	 * var discProps = room.getDiscProperties(4);
 	 * var hasBallFlag = (discProps.cGroup & room.CollisionFlags.ball) != 0;
-	 *
+	 * 
 	 * // Add "wall" to the collision mask of disc 5 without changing any other of it's flags:
 	 * var discProps = room.getDiscProperties(5);
 	 * room.setDiscProperties(5, {cMask: discProps.cMask | room.CollisionFlags.wall});
@@ -659,8 +659,8 @@ class ScoresObject { */ const TeamID = { Spectators: 0, - RedTeam: 1, - BlueTeam: 2, + RedTeam: 1, + BlueTeam: 2, }; /** @@ -777,11 +777,11 @@ class DiscPropertiesObject { *

The flags are ball, red, blue, redKO, blueKO, wall, all, kick, score, c0, c1, c2 and c3

*

Example usage:

*
var cf = room.CollisionFlags;
- *
+ * 
  * // Check if disc 4 belongs to collision group "ball":
  * var discProps = room.getDiscProperties(4);
  * var hasBallFlag = (discProps.cGroup & cf.ball) != 0;
- *
+ * 
  * // Add "wall" to the collision mask of disc 5 without changing any other of it's flags:
  * var discProps = room.getDiscProperties(5);
  * room.setDiscProperties(5, {cMask: discProps.cMask | cf.wall});
@@ -789,30 +789,30 @@ class DiscPropertiesObject { * @enum {number} */ const CollisionFlagsObject = { - ball: 1, - red: 2, - blue: 4, - redKO: 8, + ball: 1, + red: 2, + blue: 4, + redKO: 8, blueKO: 16, - wall: 32, - all: 63, - kick: 64, - score: 128, - c0: 268435456, - c1: 536870912, - c2: 1073741824, - c3: -2147483648, + wall: 32, + all: 63, + kick: 64, + score: 128, + c0: 268435456, + c1: 536870912, + c2: 1073741824, + c3: -2147483648, }; /** * @constructor * - * @param {RoomConfigObject} roomConfig + * @param {!RoomConfigObject} roomConfig * - * @return {RoomObject} + * @return {!RoomObject} * * @link https://github.com/haxball/haxball-issues/wiki/Headless-Host Documentation * @link https://html5.haxball.com/headless Headless server host */ function HBInit(roomConfig) { -}; +} \ No newline at end of file