Skip to content
Permalink
Browse files
Games Deathmatch API: Allow games with any # of lives
  • Loading branch information
RussellLVP committed Aug 5, 2020
1 parent c063bee commit 3a33ccaae1bc0be1e5d29ab823cfdb21a8a16e82
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 141 deletions.
@@ -9,8 +9,7 @@ import { SpawnWeaponsSetting } from 'features/games_deathmatch/settings/spawn_we
export class DeathmatchDescription {
// Available values for some of the enumeration-based deathmatch options.
static kMapMarkerOptions = ['Enabled', 'Team only', 'Disabled'];
static kObjectiveOptions =
['Last man standing', 'Best of...', 'First to..', 'Time limit...', 'Continuous'];
static kObjectiveOptions = ['Continuous', 'Number of lives...', 'Time limit...'];
static kTeamOptions = [ 'Balanced teams', 'Free for all', 'Randomized teams' ];

// Whether players should be subject to lag compensation during this game.
@@ -66,23 +65,19 @@ export class DeathmatchDescription {
throw new Error(`[${this.name}] Invalid value given for the objective type.`);

switch (options.objective.type) {
case 'Last man standing':
case 'Continuous':
break;

case 'Best of...':
if (!options.objective.hasOwnProperty('rounds'))
throw new Error(`[${this.name}] The objective.rounds option is missing.`);
break;
case 'Number of lives...':
if (!options.objective.hasOwnProperty('lives'))
throw new Error(`[${this.name}] The objective.lives option is missing.`);

case 'First to...':
if (!options.objective.hasOwnProperty('kills'))
throw new Error(`[${this.name}] The objective.kills option is missing.`);
break;

case 'Time limit...':
if (!options.objective.hasOwnProperty('seconds'))
throw new Error(`[${this.name}] The objective.seconds option is missing.`);

break;
}

@@ -92,8 +87,7 @@ export class DeathmatchDescription {
type: settings.getValue('games/deathmatch_objective_default'),

// We don't know what the given |type| is, so just duplicate the value for each...
rounds: settings.getValue('games/deathmatch_objective_value_default'),
kills: settings.getValue('games/deathmatch_objective_value_default'),
lives: settings.getValue('games/deathmatch_objective_value_default'),
seconds: settings.getValue('games/deathmatch_objective_value_default'),
};
}
@@ -22,8 +22,7 @@ describe('DeathmatchDescription', it => {
type: settings.getValue('games/deathmatch_objective_default'),

// The specific values are duplicated for the default value.
rounds: settings.getValue('games/deathmatch_objective_value_default'),
kills: settings.getValue('games/deathmatch_objective_value_default'),
lives: settings.getValue('games/deathmatch_objective_value_default'),
seconds: settings.getValue('games/deathmatch_objective_value_default'),
});

@@ -54,7 +53,7 @@ describe('DeathmatchDescription', it => {
const description = new DeathmatchDescription(/* description= */ null, {
lagCompensation: true,
mapMarkers: 'Team only',
objective: { type: 'Best of...', rounds: 3 },
objective: { type: 'Time limit...', seconds: 180 },
skin: 121,
spawnArmour: true,
spawnWeapons: [ { weapon: 16, ammo: 50 } ],
@@ -65,7 +64,7 @@ describe('DeathmatchDescription', it => {

assert.isTrue(description.lagCompensation);
assert.equal(description.mapMarkers, 'Team only');
assert.deepEqual(description.objective, { type: 'Best of...', rounds: 3 });
assert.deepEqual(description.objective, { type: 'Time limit...', seconds: 180 });
assert.equal(description.skin, 121);
assert.isTrue(description.spawnArmour);
assert.deepEqual(description.spawnWeapons, [ { weapon: 16, ammo: 50 } ]);
@@ -25,11 +25,9 @@ export class DeathmatchGame extends GameBase {
static kMapMarkersDisabled = 'Disabled';

// The objective which defines the winning conditions of this game.
static kObjectiveLastManStanding = 'Last man standing';
static kObjectiveBestOf = 'Best of...';
static kObjectiveFirstTo = 'First to...';
static kObjectiveTimeLimit = 'Time limit...';
static kObjectiveContinuous = 'Continuous';
static kObjectiveLives = 'Number of lives...';
static kObjectiveTimeLimit = 'Time limit...';

// Indicates which team a player can be part of. Individuals are always part of team 0, whereas
// players can be part of either Team Alpha or Team Bravo in team-based games.
@@ -120,7 +118,7 @@ export class DeathmatchGame extends GameBase {
this.#objective_ = new ContinuousObjective();
break;

case DeathmatchGame.kObjectiveLastManStanding:
case DeathmatchGame.kObjectiveLives:
this.#objective_ = new LivesObjective();
break;

@@ -9,9 +9,9 @@ import { getGameInstance, runGameLoop } from 'features/games/game_test_helpers.j

describe('LivesObjective', it => {
const kObjectiveIndex = 5;
const kObjectiveLastManStandingIndex = 0;
const kObjectiveLivesIndex = 0;

it('should be possible to have games with a time limit', async (assert) => {
it('should be possible to have games with a number of lives', async (assert) => {
const feature = server.featureManager.loadFeature('games_deathmatch');
const settings = server.featureManager.loadFeature('settings');

@@ -34,7 +34,8 @@ describe('LivesObjective', it => {
// Mimic the flow where the game's objective is decided by the player through the menu,
// which is what will get least in-game coverage when doing manual testing.
gunther.respondToDialog({ listitem: kObjectiveIndex }).then(
() => gunther.respondToDialog({ listitem: kObjectiveLastManStandingIndex })).then(
() => gunther.respondToDialog({ listitem: kObjectiveLivesIndex })).then(
() => gunther.respondToDialog({ inputtext: '2' /* lives */ })).then(
() => gunther.respondToDialog({ listitem: 0 /* start the game */ }));

assert.isTrue(await gunther.issueCommand('/bubble custom'));
@@ -47,14 +48,22 @@ describe('LivesObjective', it => {
assert.doesNotThrow(() => getGameInstance());
assert.instanceOf(getGameInstance().objectiveForTesting, LivesObjective);

// TODO: Support multiple lives.
// The game ends when either Russell or Gunther dies twice. We kill Gunther first.
gunther.die();

await runGameLoop(); // allow the game to finish, if it would.
assert.doesNotThrow(() => getGameInstance());

// Kill Russell, after which the game will be in sudden death mode.
russell.die();

// The game ends when either Russell or Gunther dies. We kill Gunther, so this should mark
// Russell as the winner of the game, and Gunther as the loser.
await runGameLoop(); // allow the game to finish, if it would.
assert.doesNotThrow(() => getGameInstance());

// Now kill Gunther again. This will mark Russell as the winner of the match.
gunther.die();

await runGameLoop(); // allow the game to finish

assert.throws(() => getGameInstance());
});
});
@@ -26,11 +26,8 @@ export class ObjectiveSetting extends GameCustomSetting {
// with the specialization given by the player, e.g. best of X.
getCustomizationDialogValue(currentValue) {
switch (currentValue.type) {
case 'Best of...':
return format('Best of %d rounds', currentValue.kills);

case 'First to...':
return format('First to %d kills', currentValue.kills);
case 'Number of lives...':
return format('Lives (%d)', currentValue.lives);

case 'Time limit...':
return format('Time limit (%s)', timeDifferenceToString(currentValue.seconds));
@@ -49,17 +46,9 @@ export class ObjectiveSetting extends GameCustomSetting {

const options = [
[
'Last man standing',
ObjectiveSetting.prototype.applyOption.bind(this, settings, 'Last man standing')
],/*
[
'Best of...',
ObjectiveSetting.prototype.handleBestOfSetting.bind(this, settings, player)
'Number of lives...',
ObjectiveSetting.prototype.handleNumberOfLivesSetting.bind(this, settings, player)
],
[
'First to...',
ObjectiveSetting.prototype.handleFirstToSetting.bind(this, settings, player)
],*/
[
'Time limit...',
ObjectiveSetting.prototype.handleTimeLimitSetting.bind(this, settings, player)
@@ -83,45 +72,25 @@ export class ObjectiveSetting extends GameCustomSetting {
// Applies the given |option| as the desired objective.
applyOption(settings, option) { settings.set('deathmatch/objective', { type: option }); }

// Handles the case where the objective should be "Best of X rounds".
async handleBestOfSetting(settings, player) {
const rounds = await Question.ask(player, {
question: 'Game objective',
message: `Please enter the number of rounds for the game.`,
constraints: {
validation: isNumberInRange.bind(null, 2, 50),
explanation: 'The number of rounds must be between 2 and 50.',
abort: 'You need to give a reasonable number of rounds for the game.',
}
});

if (!rounds)
return null;

settings.set('deathmatch/objective', {
type: 'Best of...',
kills: parseInt(rounds, 10),
});
}

// Handles the case where the objective should be "First to X kills".
async handleFirstToSetting(settings, player) {
const kills = await Question.ask(player, {
// Handles the case where the objective should be "Number of lives...". This allows the person
// creating the game to choose a sensible number of lives for each participant.
async handleNumberOfLivesSetting(settings, player) {
const lives = await Question.ask(player, {
question: 'Game objective',
message: `Please enter the number of kills for the game.`,
message: `Please enter the number of lives each participant has.`,
constraints: {
validation: isNumberInRange.bind(null, 2, 50),
explanation: 'The number of kills must be between 2 and 50.',
abort: 'You need to give a reasonable number of kills for the game.',
validation: isNumberInRange.bind(null, 1, 50),
explanation: 'The number of lives must be between 1 and 50.',
abort: 'You need to give a reasonable number of lives for the game.',
}
});

if (!kills)
if (!lives)
return null;

settings.set('deathmatch/objective', {
type: 'First to...',
kills: parseInt(kills, 10),
type: 'Number of lives...',
lives: parseInt(lives, 10),
});
}

@@ -132,7 +101,7 @@ export class ObjectiveSetting extends GameCustomSetting {
message: `Please enter the game's time limit in seconds.`,
constraints: {
validation: isNumberInRange.bind(null, 30, 1800),
explanation: 'The time limit must be between 30 seconds and 1800 seconds, which ' +
explanation: 'The time limit must be between 30 seconds and 1,800 seconds, which ' +
'is 30 minutes.',
abort: 'You need to give a reasonable time limit for the game.',
}
@@ -163,12 +132,9 @@ export class ObjectiveSetting extends GameCustomSetting {
return '';

switch (option) {
case 'Best of...':
return format('{FFFF00}%d rounds', currentValue.kills);
case 'Number of lives...':
return format('{FFFF00}%d lives', currentValue.lives);

case 'First to...':
return format('{FFFF00}%d kills', currentValue.kills);

case 'Time limit...':
return format('{FFFF00}%s', timeDifferenceToString(currentValue.seconds));
}
@@ -30,7 +30,7 @@ describe('GameCustomSetting', (it, beforeEach) => {
settingsFrozen: GameDescription.kDefaultSettings,
settings: [
new Setting(
'deathmatch', 'objective', new ObjectiveSetting, { type: 'Last man standing' },
'deathmatch', 'objective', new ObjectiveSetting, { type: 'Continuous' },
'Objective')
],
});
@@ -57,7 +57,7 @@ describe('GameCustomSetting', (it, beforeEach) => {
],
[
'Objective',
'Last man standing',
'Continuous',
]
]);

@@ -69,72 +69,30 @@ describe('GameCustomSetting', (it, beforeEach) => {

assert.isTrue(settings.has('deathmatch/objective'));
assert.typeOf(settings.get('deathmatch/objective'), 'object');
assert.deepEqual(settings.get('deathmatch/objective'), { type: 'Last man standing' });

// (3) Gunther is able to start a Last man standing game.
gunther.respondToDialog({ listitem: 2 /* Objective */ }).then(
() => gunther.respondToDialog({ listitem: 0 /* Last man standing */ })).then(
() => gunther.respondToDialog({ listitem: 0 /* Start the game! */ }));

settings = await commands.determineSettings(description, gunther, params);
assert.isNotNull(settings);

assert.isTrue(settings.has('deathmatch/objective'));
assert.deepEqual(settings.get('deathmatch/objective'), { type: 'Last man standing' });

assert.equal(gunther.getLastDialogAsTable().rows.length, 3);
assert.deepEqual(gunther.getLastDialogAsTable().rows[2], [
'Objective',
'Last man standing',
]);

if (false) {
// (4) Gunther is able to start a Best of... game.
gunther.respondToDialog({ listitem: 2 /* Objective */ }).then(
() => gunther.respondToDialog({ listitem: 1 /* Best of... */ })).then(
() => gunther.respondToDialog({ inputtext: 'banana' /* invalid */ })).then(
() => gunther.respondToDialog({ response: 1 /* try again */ })).then(
() => gunther.respondToDialog({ inputtext: '9999' /* invalid */ })).then(
() => gunther.respondToDialog({ response: 1 /* try again */ })).then(
() => gunther.respondToDialog({ inputtext: '42' /* valid */ })).then(
() => gunther.respondToDialog({ listitem: 0 /* Start the game! */ }));

settings = await commands.determineSettings(description, gunther, params);
assert.isNotNull(settings);

assert.isTrue(settings.has('deathmatch/objective'));
assert.deepEqual(settings.get('deathmatch/objective'), { type: 'Best of...', kills: 42 });

assert.equal(gunther.getLastDialogAsTable().rows.length, 3);
assert.deepEqual(gunther.getLastDialogAsTable().rows[2], [
'Objective',
'{FFFF00}Best of 42 rounds',
]);
assert.deepEqual(settings.get('deathmatch/objective'), { type: 'Continuous' });

// (5) Gunther is able to start a First to... game.
// (3) Gunther is able to start a # of lives game.
gunther.respondToDialog({ listitem: 2 /* Objective */ }).then(
() => gunther.respondToDialog({ listitem: 2 /* First to... */ })).then(
() => gunther.respondToDialog({ inputtext: 'banana' /* invalid */ })).then(
() => gunther.respondToDialog({ response: 1 /* try again */ })).then(
() => gunther.respondToDialog({ inputtext: '9999' /* invalid */ })).then(
() => gunther.respondToDialog({ response: 1 /* try again */ })).then(
() => gunther.respondToDialog({ inputtext: '31' /* valid */ })).then(
() => gunther.respondToDialog({ listitem: 0 /* Number of lives... */ })).then(
() => gunther.respondToDialog({ inputtext: '5' /* five lives */ })).then(
() => gunther.respondToDialog({ listitem: 0 /* Start the game! */ }));

settings = await commands.determineSettings(description, gunther, params);
assert.isNotNull(settings);

assert.isTrue(settings.has('deathmatch/objective'));
assert.deepEqual(settings.get('deathmatch/objective'), { type: 'First to...', kills: 31 });
assert.deepEqual(settings.get('deathmatch/objective'), {
type: 'Number of lives...',
lives: 5,
});

assert.equal(gunther.getLastDialogAsTable().rows.length, 3);
assert.deepEqual(gunther.getLastDialogAsTable().rows[2], [
'Objective',
'{FFFF00}First to 31 kills',
'{FFFF00}Lives (5)',
]);
}

// (6) Gunther is able to start a Time limit... game.
// (4) Gunther is able to start a Time limit... game.
gunther.respondToDialog({ listitem: 2 /* Objective */ }).then(
() => gunther.respondToDialog({ listitem: 1 /* Time limit... */ })).then(
() => gunther.respondToDialog({ inputtext: 'banana' /* invalid */ })).then(
@@ -157,7 +115,7 @@ if (false) {
'{FFFF00}Time limit (12 minutes)',
]);

// (7) Gunther is able to start a Continuous game.
// (5) Gunther is able to start a Continuous game.
gunther.respondToDialog({ listitem: 2 /* Objective */ }).then(
() => gunther.respondToDialog({ listitem: 2 /* Continuous */ })).then(
() => gunther.respondToDialog({ listitem: 0 /* Start the game! */ }));
@@ -171,10 +129,10 @@ if (false) {
assert.equal(gunther.getLastDialogAsTable().rows.length, 3);
assert.deepEqual(gunther.getLastDialogAsTable().rows[2], [
'Objective',
'{FFFF00}Continuous',
'Continuous', // default value
]);

// (8) Gunther should be able to opt out of the dialog immediately.
// (6) Gunther should be able to opt out of the dialog immediately.
gunther.respondToDialog({ listitem: 2 /* Objective */ }).then(
() => gunther.respondToDialog({ listitem: 1 /* Time limit... */ })).then(
() => gunther.respondToDialog({ inputtext: 'banana' /* invalid */ })).then(
@@ -185,6 +143,6 @@ if (false) {
assert.isNotNull(settings);

assert.isTrue(settings.has('deathmatch/objective'));
assert.deepEqual(settings.get('deathmatch/objective'), { type: 'Last man standing' });
assert.deepEqual(settings.get('deathmatch/objective'), { type: 'Continuous' });
});
});

0 comments on commit 3a33cca

Please sign in to comment.