Skip to content
Permalink
Browse files
Implement unscrambling reaction tests
Part of #718
  • Loading branch information
RussellLVP committed Jun 21, 2020
1 parent 8114d69 commit 40010bd7782740bb4261531d6f2622e6d0d15f61
Showing 7 changed files with 227 additions and 2 deletions.
@@ -52,11 +52,12 @@
"fightclub-winner": "<color:13>*** %s (Id:%d) has won the fight against %s (Id:%d) with a score of %d : %d.",
"fightclub-draw": "<color:13>*** The fight between %s (Id:%d) and %s (Id:%d) ended in a draw.",

"reaction-calculate": "<color:6>*** First player to calculate %s wins %$!",
"reaction-remember": "<color:6>*** Players have been asked to remember a number in order to win %$!",
"reaction-remember-2": "<color:6>*** Players have been asked to repeat the number they were asked to remember to win %$!",
"reaction-repeat": "<color:6>*** First player to type %s wins %$!",
"reaction-calculate": "<color:6>*** First player to calculate %s wins %$!",
"reaction-result": "<color:6>*** %s (Id:%d) has won the reaction test in %d seconds!",
"reaction-unscramble": "<color:6>*** First player to unscramble \"%s\" wins %$!",

"merchant": "<color:3>*** The merchant wants to purchase a %{1}s for %{0}$."
}
@@ -500,6 +500,7 @@
"REACTION_TEST_ANNOUNCE_REMEMBER": "{FFFF00}You've been asked to remember the number %d...",
"REACTION_TEST_ANNOUNCE_REMEMBER_2": "{FFFF00}The first one to repeat the number you had to remember wins %$!",
"REACTION_TEST_ANNOUNCE_REPEAT": "{FFFF00}The first one who repeats \"%s\" wins %$!",
"REACTION_TEST_ANNOUNCE_UNSCRAMBLE": "{FFFF00}The first one who unscrambles \"%s\" wins %$!",
"REACTION_TEST_ANNOUNCE_WINNER_FIRST": "{FFFF00}%s has won the reaction test in %.2f seconds. This is their first win!",
"REACTION_TEST_ANNOUNCE_WINNER_SECOND": "{FFFF00}%s has won the reaction test in %.2f seconds. They've won once before.",
"REACTION_TEST_ANNOUNCE_WINNER": "{FFFF00}%s has won the reaction test in %.2f seconds. They've won %d times before.",
@@ -6,6 +6,7 @@ import { CalculationStrategy } from 'features/reaction_tests/strategies/calculat
import { Feature } from 'components/feature_manager/feature.js';
import { RandomStrategy } from 'features/reaction_tests/strategies/random_strategy.js';
import { RememberStrategy } from 'features/reaction_tests/strategies/remember_strategy.js';
import { UnscrambleStrategy } from 'features/reaction_tests/strategies/unscramble_strategy.js';

import * as achievements from 'features/collectables/achievements.js';

@@ -65,6 +66,7 @@ export default class ReactionTests extends Feature {
CalculationStrategy,
RandomStrategy,
RememberStrategy,
UnscrambleStrategy,
];

// Immediately schedule the first reaction test to start.
@@ -48,6 +48,6 @@ export class RandomStrategy extends Strategy {

// Verifies whether the |message| is, or contains, the answer to this reaction test.
verify(message) {
return message.toUpperCase() === this.answer_;
return message.toUpperCase().startsWith(this.answer_);
}
}
@@ -0,0 +1,141 @@
// Copyright 2020 Las Venturas Playground. All rights reserved.
// Use of this source code is governed by the MIT license, a copy of which can
// be found in the LICENSE file.

import { Strategy } from 'features/reaction_tests/strategy.js';

import { random } from 'base/random.js';

// File that contains the word definitions for scrambled word games.
const kWordDefinitionFile = 'data/scrambled_words.json';

// Utility function to determine whether the given |charCode| should be scrambled.
function shouldScramble(charCode) {
return (charCode >= 48 && charCode <= 57) || /* numbers */
(charCode >= 65 && charCode <= 90) || /* upper case */
(charCode >= 97 && charCode <= 122); /* lower case */
}

// This strategy works by scrambling the characters in a list of predefined phrases, and requiring
// players to guess the right word. The actual list is defined in |kWordDefinitionFile|.
export class UnscrambleStrategy extends Strategy {
// How many players should be on the server in order to consider this strategy?
static kMinimumPlayerCount = 1;

answer_ = null;
scrambled_ = null;
settings_ = null;
words_ = new Set();

constructor(settings) {
super();

this.settings_ = settings;
}

// Gets the answer to the current reaction test. May be NULL.
get answer() { return this.answer_; }

// Gets the scrambled answer, primarily for testing purposes.
get scrambled() { return this.scrambled_; }

// Initializes the list of words that players should unscramble. Will be called when a test is
// starting, to avoid doing this too often while running tests.
ensureWordListInitialized() {
if (this.words_.size)
return;

const words = JSON.parse(readFile(kWordDefinitionFile));
for (const word of words) {
if (word.startsWith('__'))
continue; // considered as a note/comment, ignore it

this.words_.add(word);
}
}

// Starts a new test provided by this strategy. The question must be determined, and it should
// be announced to people in-game and available through Nuwani accordingly.
start(announceFn, nuwani, prize) {
this.ensureWordListInitialized();

// Pick a random phrase from the loaded word list.
this.answer_ = ([ ...this.words_ ][ random(this.words_.size) ]).toUpperCase();
this.scrambled_ = this.scramble(this.answer_);

// Announce the test to all in-game participants.
announceFn(Message.REACTION_TEST_ANNOUNCE_UNSCRAMBLE, this.scrambled_, prize);

// Announce the test to everyone reading along through Nuwani.
nuwani().echo('reaction-unscramble', this.scrambled_, prize);
}

// Scrambles the given |phrase| and returns the result. We decide which characters are valid to
// be scrambled, then scramble them, re-compose the word, and return it as a string.
scramble(phrase) {
if (phrase.trim() !== phrase)
console.log(`[reaction test] warning: "${phrase}" has excess spacing.`);

const kStaticPercentage =
this.settings_().getValue('playground/reaction_test_unscramble_fixed');

const words = phrase.split(' ');
const result = [];

for (const word of words) {
const characters = [];
const fixed = new Set();

// (1) Decide which characters are supposed to be fixed.
for (let index = 0; index < word.length; ++index) {
if (random(100) < kStaticPercentage)
fixed.add(index);
}

// (1) Collect all the characters in the |word|.
for (let index = 0; index < word.length; ++index) {
if (!shouldScramble(word.charCodeAt(index)) || fixed.has(index))
continue;

characters.push(word.charAt(index));
}

const shuffled = this.shuffle(characters);
const composed = [];

// (2) Re-compose the scrambled word.
for (let index = 0; index < word.length; ++index) {
if (shouldScramble(word.charCodeAt(index)) && !fixed.has(index))
composed.push(shuffled.shift());
else
composed.push(word.charAt(index));
}

result.push(composed.join(''));
}

// (3) Join the composed word together, then translate to upper case.
return result.join(' ');
}

// Shuffles the given |array| with the Fisher-Yates algorithm:
// https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
shuffle(array) {
let counter = array.length;

while (counter > 0) {
const index = random(counter--);
const temp = array[counter];

array[counter] = array[index];
array[index] = temp;
}

return array;
}

// Verifies whether the |message| is, or contains, the answer to this reaction test.
verify(message) {
return message.toUpperCase().startsWith(this.answer_);
}
}
@@ -0,0 +1,79 @@
// Copyright 2020 Las Venturas Playground. All rights reserved.
// Use of this source code is governed by the MIT license, a copy of which can
// be found in the LICENSE file.

import { UnscrambleStrategy } from 'features/reaction_tests/strategies/unscramble_strategy.js';

import { range } from 'base/range.js';
import { symmetricDifference } from 'base/set_extensions.js';

describe('UnscrambleStrategy', (it, beforeEach) => {
let announceFn = null;
let nuwani = null;
let settings = null;

beforeEach(() => {
const driver = server.featureManager.loadFeature('reaction_tests');

announceFn = driver.__proto__.announceToPlayers.bind(driver);
nuwani = server.featureManager.loadFeature('nuwani');
settings = server.featureManager.loadFeature('settings');
});

it('is able to scramble words while maintaining the correct answer', assert => {
const strategy = new UnscrambleStrategy(() => settings);

// (1) Test the shuffling function.
const values = range(100);
const shuffled = strategy.shuffle(values);

assert.equal(values.length, shuffled.length);
assert.equal(symmetricDifference(new Set(values), new Set(shuffled)).size, 0);

// (2) Test the scrambling function.
const input = 'This abcdef is xyz123 a test';
const scrambled = strategy.scramble(input);

assert.equal(input.length, scrambled.length);
assert.equal(symmetricDifference(new Set(values), new Set(shuffled)).size, 0);
});

it('is able to change difficulty level through static letters', assert => {
const strategy = new UnscrambleStrategy(() => settings);

// (1) Don't scramble the words at all.
settings.setValue('playground/reaction_test_unscramble_fixed', 100);

strategy.start(announceFn, () => nuwani, 1234);
assert.equal(strategy.answer, strategy.scrambled);

// (2) Heavily scramble the words, with on fixed letters.
settings.setValue('playground/reaction_test_unscramble_fixed', 0);

strategy.start(announceFn, () => nuwani, 1234);
assert.notEqual(strategy.answer, strategy.scrambled);
});

it('announces new tests to in-game players and Nuwani users', assert => {
const gunther = server.playerManager.getById(/* Gunther= */ 0);
const strategy = new UnscrambleStrategy(() => settings);

assert.equal(gunther.messages.length, 0);
assert.equal(nuwani.messagesForTesting.length, 0);

strategy.start(announceFn, () => nuwani, 1234);

assert.equal(gunther.messages.length, 1);
assert.equal(
gunther.messages[0],
Message.format(Message.REACTION_TEST_ANNOUNCE_UNSCRAMBLE, strategy.scrambled, 1234));

assert.equal(nuwani.messagesForTesting.length, 1);
assert.deepEqual(nuwani.messagesForTesting[0], {
tag: 'reaction-unscramble',
params: [ strategy.scrambled, 1234 ]
});

strategy.stop();
});
});

Some generated files are not rendered by default. Learn more.

0 comments on commit 40010bd

Please sign in to comment.