Skip to content

Commit

Permalink
improve interpreter
Browse files Browse the repository at this point in the history
  • Loading branch information
RSamaium committed Nov 5, 2023
1 parent 9e5c8ba commit f0a3bd6
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 9 deletions.
41 changes: 41 additions & 0 deletions packages/plugins/interpreter/src/blocks/gold.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { RpgPlayer } from "@rpgjs/server";
import type { Block } from "../types/block";
import { Group } from "../types/group";

export type ChangeGold = {
id: 'gold'
value: number
}

export default (formatMessage): Block => ({
id: 'gold',
group: Group.Param,
title: formatMessage({
defaultMessage: 'Show Choices',
description: 'Choices block',
id: 'block.choice'
}),
description: formatMessage({
defaultMessage: 'Display a choice',
description: 'Choices block description',
id: 'block.choice.description'
}),
display: ['gold'],
schema: {
type: 'object',
properties: {
value: {
type: 'number',
title: formatMessage({
defaultMessage: 'Value',
description: 'Set Value',
}),
}
},
required: ['value']
},
async execute(player: RpgPlayer, dataBlock: ChangeGold) {
player.gold += dataBlock.value
return undefined
}
})
7 changes: 5 additions & 2 deletions packages/plugins/interpreter/src/blocks/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import choice from './choice'
import text from './text'
import gold from './gold'
import type { Choice } from './choice'
import type { Text } from './text'
import type { ChangeGold } from './gold'

export type Flow = {
[blockId: string]: Choice | Text
[blockId: string]: Choice | Text | ChangeGold
}

export default [
choice,
text
text,
gold
]
32 changes: 25 additions & 7 deletions packages/plugins/interpreter/src/interpreter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Observable, of, switchMap, from, throwError, timeout, map } from "rxjs"
import type { Block, BlockExecuteReturn } from "./types/block"
import type { Block, BlockExecuteReturn, Parameters } from "./types/block"
import type { Edge } from "./types/edge"
import { formatMessage } from "./format-message"
import blocks, { Flow } from "./blocks"
Expand All @@ -13,18 +13,21 @@ export class RpgInterpreter {
private recursionLimit = 1000; // set the recursion limit
private currentRecursionDepth = 0; // track the current recursion depth
private executionTimeout = 5000; // set the execution timeout in milliseconds
private parametersSchema?: Parameters

constructor(
private dataBlocks: Flow,
private edges: Edge,
options: {
recursionLimit?: number;
executionTimeout?: number;
parametersSchema?: (formatMessage?) => Parameters
} = {},
_formatMessage = formatMessage
) {
this.recursionLimit = options.recursionLimit || this.recursionLimit;
this.executionTimeout = options.executionTimeout || this.executionTimeout;
this.parametersSchema = options.parametersSchema?.(_formatMessage)
blocks.forEach(block => {
const _blocks = block(_formatMessage)
this.blocksById[_blocks.id] = _blocks
Expand All @@ -44,18 +47,33 @@ export class RpgInterpreter {
*
* @return {Observable<any>} The RPG interpreter execution as an Observable.
*/
start({ player, blockId }: { player: RpgPlayer, blockId: string }): Observable<any> {
const validate = this.validateFlow(blockId)
if (validate === null) {
return this.executeFlow(player, blockId)
start({ player, blockId, parameters }: { player: RpgPlayer, blockId: string, parameters?}): Observable<any> {
const validateParams = this.validateParameters(parameters)
if (validateParams === null) {
const validate = this.validateFlow(blockId)
if (validate === null) {
return this.executeFlow(player, blockId, parameters)
}
return throwError(() => validate)
}
return throwError(() => validate)
return throwError(() => validateParams)
}

getHistory() {
return this.executionHistory
}

validateParameters(parameters: any): ZodError | null {
if (this.parametersSchema) {
const schemaZod = z.object(jsonSchemaToZod(this.parametersSchema));
const parse = schemaZod.safeParse(parameters);
if (!parse.success) {
return parse.error
}
}
return null
}

/**
* Validate a block.
*
Expand Down Expand Up @@ -183,7 +201,7 @@ export class RpgInterpreter {
*
* @return {Observable<any>} The flow execution as an Observable.
*/
private executeFlow(player: RpgPlayer, blockId: string): Observable<any> {
private executeFlow(player: RpgPlayer, blockId: string, parameters?: Parameters): Observable<any> {
// Prevent recursion beyond the limit
if (this.currentRecursionDepth++ > this.recursionLimit) {
return throwError(() => new Error('Recursion limit exceeded'));
Expand Down
2 changes: 2 additions & 0 deletions packages/plugins/interpreter/src/types/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export interface Block {
execute: (player: RpgPlayer, dataBlock: any) => BlockExecute;
}

export interface Parameters extends SchemaInterface {}

interface DisplayItem {
key: string;
hasHandles: boolean;
Expand Down
1 change: 1 addition & 0 deletions packages/plugins/interpreter/src/types/group.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum Group {
UI = 'ui',
Logic = 'logic',
Param = 'param',
}
3 changes: 3 additions & 0 deletions packages/plugins/interpreter/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { z, ZodType } from "zod";
const percent = z.number().min(0).max(100)

const typeMap: Record<string, (property?: any, key?: string) => z.ZodType<any, any>> = {
'code': () => z.string(),
'color': () => z.string().regex(/^#(?:[0-9a-fA-F]{3}){1,2}$/, 'Invalid color format'),
'date': () => z.date(),
'password': () => z.string(),
'email': () => z.string().email(),
'text': () => z.string(),
Expand Down
64 changes: 64 additions & 0 deletions packages/plugins/interpreter/tests/interpreter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ describe('Interpreter', () => {
player = {
showText: vi.fn().mockResolvedValue(undefined),
showChoices: vi.fn().mockResolvedValue({ text: 'test', value: 0 }),
gold: 0
}
})

Expand Down Expand Up @@ -207,6 +208,69 @@ describe('Interpreter', () => {
}))).rejects.toThrow('Timeout has occurred');
})

describe('With Parameters', () => {
const fnSchema = (formatMessage) => {
return {
type: 'object',
properties: {
gold: {
type: 'number',
title: formatMessage({
defaultMessage: 'Value',
description: 'Set Value',
}),
}
},
required: ['gold']
}
}

test('Flow with Parameters, Wrong Param', async () => {
const interpreter = new RpgInterpreter({
block1: {
id: 'text',
text: 'Hello World 1'
}
}, {}, {
parametersSchema: fnSchema
})

await expect(lastValueFrom(interpreter.start({
player,
blockId: 'block1'
}))).rejects.toThrow(
expect.objectContaining({
errors: expect.arrayContaining([
expect.objectContaining({
code: 'invalid_type'
})
])
})
)
})

test('Flow with Parameters, True Param', async () => {
const interpreter = new RpgInterpreter({
block1: {
id: 'gold',
value: 10
}
}, {}, {
parametersSchema: fnSchema
})

await lastValueFrom(interpreter.start({
player,
blockId: 'block1',
parameters: {
gold: 100
}
}))

expect(player.gold).toEqual(10)
})
})

afterEach(() => {
vi.restoreAllMocks()
})
Expand Down

0 comments on commit f0a3bd6

Please sign in to comment.