Skip to content

Commit 487c623

Browse files
committed
Runtime work for continuous games
1 parent 7d29b6e commit 487c623

4 files changed

Lines changed: 121 additions & 4 deletions

File tree

javascript/features/games/game_commands.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { CommandBuilder } from 'components/command_manager/command_builder.js';
66
import { GameActivity } from 'features/games/game_activity.js';
77
import { GameRegistration } from 'features/games/game_registration.js';
8+
import { GameRuntime } from 'features/games/game_runtime.js';
89
import { Menu } from 'components/menu/menu.js';
910
import { Question } from 'components/dialogs/question.js';
1011
import { Setting } from 'entities/setting.js';
@@ -189,6 +190,32 @@ export class GameCommands {
189190
return;
190191
}
191192

193+
// If the |description| accepts continuous participation, players can join and leave as they
194+
// please. We need to handle this case in the sign-up logic as well.
195+
const activeRuntimes = this.manager_.getActiveGameRuntimes(description);
196+
197+
for (const activeRuntime of activeRuntimes) {
198+
if (activeRuntime.state != GameRuntime.kStateRunning)
199+
continue;
200+
201+
// TODO: We might want to support settings and custom options for continuous games, in
202+
// which case joining the first one is not the right thing to do. This requires a
203+
// `mapEquals` and private/public check like the block above.
204+
205+
// Take the registration fee from the |player|.
206+
this.finance_().takePlayerCash(player, description.price);
207+
208+
player.sendMessage(
209+
Message.GAME_REGISTRATION_JOINED, activeRuntime.getActivityName());
210+
211+
this.nuwani_().echo(
212+
'notice-minigame', player.name, player.id, activeRuntime.getActivityName());
213+
214+
// Actually have the |player| join the |activeRuntime|, and we're done here.
215+
await activeRuntime.addPlayer(player, description.price);
216+
return;
217+
}
218+
192219
// If a |registrationId| was given, and we reach this point, then it was invalid. Tell 'em.
193220
if (registrationId) {
194221
player.sendMessage(Message.GAME_REGISTRATION_INVALID_ID);

javascript/features/games/game_manager.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export class GameManager {
4949
server.playerManager.addObserver(this);
5050
}
5151

52+
// Gets access to the active runtimes, for testing purposes only.
53+
get activeRuntimesForTesting() { return this.runtimes_; }
54+
5255
// ---------------------------------------------------------------------------------------------
5356

5457
// Returns the activity the |player| is engaged in as a `GameActivity` instance, if any.
@@ -113,6 +116,18 @@ export class GameManager {
113116

114117
// ---------------------------------------------------------------------------------------------
115118

119+
// Returns an array with the active game runtimes for the given |description|.
120+
getActiveGameRuntimes(description) {
121+
let runtimesForGame = [];
122+
123+
for (const runtime of this.runtimes_) {
124+
if (runtime.description === description)
125+
runtimesForGame.push(runtime);
126+
}
127+
128+
return runtimesForGame;
129+
}
130+
116131
// Immediately stops all games that are either accepting sign-ups or in-progress because the
117132
// game described by |description| will no longer be available.
118133
stopAllActiveGames(description) {

javascript/features/games/game_runtime.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { showCountdownForPlayer } from 'features/games/game_countdown.js';
1212
export class GameRuntime extends GameActivity {
1313
// The states in lifetime progression the game runtime supports.
1414
static kStateUninitialized = 0;
15-
static kStateInitialized= 1;
15+
static kStateInitialized = 1;
1616
static kStateRunning = 2;
1717
static kStateFinished = 3;
1818
static kStateFinalized = 4;
@@ -44,7 +44,7 @@ export class GameRuntime extends GameActivity {
4444
get players() { return this.players_; }
4545

4646
// Getst the state the game is in. Only exposed for testing purposes.
47-
get stateForTesting() { return this.state_; }
47+
get state() { return this.state_; }
4848

4949
// Gets the virtual world that has bene allocated to this game.
5050
get virtualWorld() { return this.virtualWorld_; }

javascript/features/games/game_runtime.test.js

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ describe('GameRuntime', (it, beforeEach) => {
307307
assert.equal(
308308
manager.getPlayerActivity(russell).getActivityState(), GameActivity.kStateEngaged);
309309

310-
assert.equal(runtime.stateForTesting, GameRuntime.kStateRunning);
310+
assert.equal(runtime.state, GameRuntime.kStateRunning);
311311

312312
assert.isNotNull(vehicles[0]);
313313
assert.equal(vehicles[0].virtualWorld, runtime.virtualWorld);
@@ -329,7 +329,7 @@ describe('GameRuntime', (it, beforeEach) => {
329329
await runGameLoop();
330330

331331
// Both players have been told to leave the game, so it should now have been finalized.
332-
assert.equal(runtime.stateForTesting, GameRuntime.kStateFinalized);
332+
assert.equal(runtime.state, GameRuntime.kStateFinalized);
333333
assert.equal(manager.runtimes_.size, 0);
334334

335335
assert.equal(gunther.messages.length, 3);
@@ -392,6 +392,81 @@ describe('GameRuntime', (it, beforeEach) => {
392392
}
393393
});
394394

395+
it('should enable players to join continuous games at any time', async (assert) => {
396+
const feature = server.featureManager.loadFeature('games');
397+
const finance = server.featureManager.loadFeature('finance');
398+
399+
let activePlayers = 0;
400+
let initialized = false;
401+
402+
feature.registerGame(class extends Game {
403+
async onInitialized(settings) {
404+
if (initialized)
405+
throw new Error('The game already has been initialized.');
406+
407+
initialized = true;
408+
}
409+
410+
async onPlayerAdded(player) { ++activePlayers; }
411+
async onPlayerRemoved(player) { --activePlayers; }
412+
}, {
413+
name: 'Bubble',
414+
goal: 'Have entities',
415+
416+
command: 'bubblegame',
417+
price: 500,
418+
419+
continuous: true,
420+
minimumPlayers: 1,
421+
maximumPlayers: 4,
422+
});
423+
424+
finance.givePlayerCash(gunther, 2500);
425+
finance.givePlayerCash(russell, 2500);
426+
427+
assert.isNull(manager.getPlayerActivity(gunther));
428+
assert.isNull(manager.getPlayerActivity(russell));
429+
430+
// (1) The game should start immediately for |gunther|, even though other players are
431+
// available. This is because it's been marked as a continuous game.
432+
assert.isTrue(await gunther.issueCommand('/bubblegame'));
433+
434+
assert.isNotNull(manager.getPlayerActivity(gunther));
435+
assert.equal(
436+
manager.getPlayerActivity(gunther).getActivityState(), GameActivity.kStateEngaged);
437+
438+
assert.equal(activePlayers, 1);
439+
440+
// (2) Have Russell join the game as well. They should be added to the active game, rather
441+
// than be routed through registration and in a new game instead.
442+
assert.isTrue(await russell.issueCommand('/bubblegame'));
443+
444+
assert.isNotNull(manager.getPlayerActivity(russell));
445+
assert.equal(
446+
manager.getPlayerActivity(russell).getActivityState(), GameActivity.kStateEngaged);
447+
448+
assert.strictEqual(manager.getPlayerActivity(gunther), manager.getPlayerActivity(russell));
449+
assert.equal(activePlayers, 2);
450+
451+
// (3) Have Gunther leave the game. Russell should still be playing.
452+
assert.isTrue(await gunther.issueCommand('/leave'));
453+
454+
assert.isNull(manager.getPlayerActivity(gunther));
455+
assert.equal(activePlayers, 1);
456+
457+
assert.isNotNull(manager.getPlayerActivity(russell));
458+
assert.equal(
459+
manager.getPlayerActivity(russell).getActivityState(), GameActivity.kStateEngaged);
460+
461+
// (4) When Russell leaves the game as well, it should be stopped. Here we mimic that by
462+
// Russell disconnecting, because we're already testing the `/leave` command above.
463+
russell.disconnectForTesting();
464+
465+
// Wait until there are no more active runtimes, to verify state is cleaned up.
466+
while (manager.activeRuntimesForTesting.size)
467+
await server.clock.advance(50);
468+
});
469+
395470
it('should have a sensible description when casted to a string', assert => {
396471
const description = new GameDescription(Game, { name: 'Bubble', goal: '' });
397472
const runtime = new GameRuntime(manager, description, kDefaultSettings);

0 commit comments

Comments
 (0)