Skip to content

Commit

Permalink
Change shuffle variation to do no controlled randomization (#26)
Browse files Browse the repository at this point in the history
* make shuffle variation real shuffle

* docs
  • Loading branch information
viniciusgerevini committed Jan 12, 2024
1 parent 1cd9db2 commit 1126ab5
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 32 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Check [LANGUAGE.md](./LANGUAGE.md) for latest documentation.
### Changed

- Return End of Dialogue object instead of undefined. `{ type: 'end' }`.
- Variation `shuffle` will do real randomization with no guarantee all items will be visited.


## 2.1.0 (2022-07-02)
Expand Down
4 changes: 3 additions & 1 deletion LANGUAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -762,7 +762,9 @@ The following example will show each item following a random sequence. Once all
)
```

Variations can be nested and can contain other elements, like options and diverts:
As opposed to `shuffle sequence`, `shuffle cycle` and `shuffle once`, the standalone `shuffle` option will work as regular randomization with no guarantee all items will be visited.

Variations can be nested and may contain other elements, like options and diverts:

```
npc: How is the day today?
Expand Down
2 changes: 2 additions & 0 deletions interpreter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

- Dialogues now return an object when ended. This impacts how you determined if the dialogue has ended.
- Changed identifier for blocks when persisting so changing block order does not impact already saved files. This will impact variations and single use options on all existing save files.
- Standalone `shuffle` variation does not behave like `shuffle cycle` anymore. Now there is no guarantee all items will be returned. Real random.

### Added

Expand All @@ -17,6 +18,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

- Increment assigment now have default values. i.e. If you run `set a += 1` when `a` is not set, it will be set to 1. Before it would break because value was null.
- Return End of Dialogue object instead of undefined
- Make standalone `shuffle` work really randomly. Variation does not behave like `shuffle cycle` anymore. Now there is no guarantee all items will be returned.

### Fixed

Expand Down
31 changes: 29 additions & 2 deletions interpreter/src/interpreter-alternatives.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ describe("Interpreter: variations", () => {
expect((dialogue.getContent() as DialogueLine).text).toEqual('end');
});

test.each(['shuffle', 'shuffle cycle'])('%s: show each alternative out of order and then repeat again when finished.', (mode) => {
const content = parse(`( ${mode}\n - Hello!\n - Hi!\n - Hey!\n)\nend\n`);
it('shuffle cycle: show each alternative out of order and then repeat again when finished', () => {
const content = parse("( shuffle cycle\n - Hello!\n - Hi!\n - Hey!\n)\nend\n");
const dialogue = Interpreter(content);

let usedOptions: string[] = [];
Expand All @@ -108,6 +108,33 @@ describe("Interpreter: variations", () => {
expect(usedOptions.sort()).toEqual(secondRunUsedOptions.sort());
});

describe("real shuffle", () => {
let mathRandomMock: jest.SpiedFunction<typeof Math.random>;

beforeEach(() => {
mathRandomMock = jest.spyOn(global.Math, 'random');
});

afterEach(() => {
jest.spyOn(global.Math, 'random').mockRestore();
});

it('shuffle: return one of the variations without guarantee all will be returned', () => {
const content = parse(`( shuffle\n - Hello!\n - Hi!\n - Hey!\n)\nend\n`);
const dialogue = Interpreter(content);

let randomReturn = [0.1, 0.1, 0.9];
let expectedReturnOrder = ["Hello!", "Hello!", "Hey!"];
for (let i of [0, 1, 2]) {
mathRandomMock.mockReturnValue(randomReturn[i]);
dialogue.start();
const variation = (dialogue.getContent() as DialogueLine).text
expect(variation).toEqual(expectedReturnOrder[i]);
}
});
});


it('works with conditional variations', () => {
const content = parse(`(sequence \n - Hello!\n - { someVar } Hi!\n - Hey!\n)\nYep!\n`);
const dialogue = Interpreter(content);
Expand Down
62 changes: 33 additions & 29 deletions interpreter/src/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,34 @@ export function Interpreter(
'error': (node: any) => { throw new Error(`Unkown node type "${node.type}"`) },
};

const shuffleWithMemory = (variations: VariationsNode & WorkingNode, mode: string ): number => {
const SHUFFLE_VISITED_KEY = `${variations._index}_shuffle_visited`;
const LAST_VISITED_KEY = `${variations._index}_last_index`;
let visitedItems: string[] = mem.getInternalVariable(SHUFFLE_VISITED_KEY, []);
const remainingOptions: (ContentNode & WorkingNode)[] = variations.content.filter((a: ContentNode & WorkingNode) => !visitedItems.includes(a._index!));

if (remainingOptions.length === 0) {
if (mode === 'once') {
return -1;
}
if (mode === 'cycle') {
mem.setInternalVariable(SHUFFLE_VISITED_KEY, []);
return shuffleWithMemory(variations, mode);
}
return mem.getInternalVariable(LAST_VISITED_KEY, -1);
}

const random = Math.floor(Math.random() * remainingOptions.length);
const index = variations.content.indexOf(remainingOptions[random]);

visitedItems.push(remainingOptions[random]._index!);

mem.setInternalVariable(LAST_VISITED_KEY, index);
mem.setInternalVariable(SHUFFLE_VISITED_KEY, visitedItems);

return index;
};

const variationHandlers: { [type: string]: Function } = {
'cycle': (variations: VariationsNode & WorkingNode) => {
let current = mem.getInternalVariable(`${variations._index}`, -1);
Expand Down Expand Up @@ -231,41 +259,17 @@ export function Interpreter(
}
return current;
},
'shuffle': (variations: VariationsNode & WorkingNode, mode = 'cycle' ): number => {
const SHUFFLE_VISITED_KEY = `${variations._index}_shuffle_visited`;
const LAST_VISITED_KEY = `${variations._index}_last_index`;
let visitedItems: string[] = mem.getInternalVariable(SHUFFLE_VISITED_KEY, []);
const remainingOptions: (ContentNode & WorkingNode)[] = variations.content.filter((a: ContentNode & WorkingNode) => !visitedItems.includes(a._index!));

if (remainingOptions.length === 0) {
if (mode === 'once') {
return -1;
}
if (mode === 'cycle') {
mem.setInternalVariable(SHUFFLE_VISITED_KEY, []);
return variationHandlers['shuffle'](variations, mode);
}
return mem.getInternalVariable(LAST_VISITED_KEY, -1);
}

const random = Math.floor(Math.random() * remainingOptions.length);
const index = variations.content.indexOf(remainingOptions[random]);

visitedItems.push(remainingOptions[random]._index!);

mem.setInternalVariable(LAST_VISITED_KEY, index);
mem.setInternalVariable(SHUFFLE_VISITED_KEY, visitedItems);

return index;
'shuffle': (variations: VariationsNode & WorkingNode): number => {
return Math.floor(Math.random() * variations.content.length);
},
'shuffle sequence': (variations: VariationsNode) => {
return variationHandlers['shuffle'](variations, 'sequence');
return shuffleWithMemory(variations, 'sequence');
},
'shuffle once': (variations: VariationsNode) => {
return variationHandlers['shuffle'](variations, 'once');
return shuffleWithMemory(variations, 'once');
},
'shuffle cycle': (variations: VariationsNode) => {
return variationHandlers['shuffle'](variations, 'cycle');
return shuffleWithMemory(variations, 'cycle');
}
};

Expand Down

0 comments on commit 1126ab5

Please sign in to comment.