Partial TypeScript rewrite - can generate grammatical parsings, cannot validate with game state; no rules engine

main
Ashelyn Dawn 4 years ago
parent 75048c3003
commit 230eef2585

111
package-lock.json generated

@ -1676,6 +1676,112 @@
"@types/istanbul-lib-report": "*"
}
},
"@types/jest": {
"version": "26.0.5",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.5.tgz",
"integrity": "sha512-heU+7w8snfwfjtcj2H458aTx3m5unIToOJhx75ebHilBiiQ39OIdA18WkG4LP08YKeAoWAGvWg8s+22w/PeJ6w==",
"requires": {
"jest-diff": "^25.2.1",
"pretty-format": "^25.2.1"
},
"dependencies": {
"@jest/types": {
"version": "25.5.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz",
"integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==",
"requires": {
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^1.1.1",
"@types/yargs": "^15.0.0",
"chalk": "^3.0.0"
}
},
"@types/yargs": {
"version": "15.0.5",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz",
"integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==",
"requires": {
"@types/yargs-parser": "*"
}
},
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"diff-sequences": {
"version": "25.2.6",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz",
"integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg=="
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"jest-diff": {
"version": "25.5.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz",
"integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==",
"requires": {
"chalk": "^3.0.0",
"diff-sequences": "^25.2.6",
"jest-get-type": "^25.2.6",
"pretty-format": "^25.5.0"
}
},
"jest-get-type": {
"version": "25.2.6",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz",
"integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig=="
},
"pretty-format": {
"version": "25.5.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz",
"integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==",
"requires": {
"@jest/types": "^25.5.0",
"ansi-regex": "^5.0.0",
"ansi-styles": "^4.0.0",
"react-is": "^16.12.0"
}
},
"supports-color": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
"integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"@types/json-schema": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz",
@ -12729,6 +12835,11 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"typescript": {
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
"integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw=="
},
"unherit": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz",

@ -6,13 +6,18 @@
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"@types/jest": "^26.0.5",
"@types/node": "^14.0.24",
"@types/react": "^16.9.43",
"@types/react-dom": "^16.9.8",
"auto-bind": "^4.0.0",
"immer": "^7.0.5",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-markdown": "^4.3.1",
"react-scripts": "3.4.1",
"redux": "^4.0.5"
"redux": "^4.0.5",
"typescript": "^3.9.7"
},
"scripts": {
"dev": "react-scripts start",

@ -1,10 +1,11 @@
import React, {useRef, useEffect} from 'react';
import React, {useRef, useEffect, useState} from 'react';
import ReactMarkdown from 'react-markdown'
import styles from './App.module.css';
function App({onCommand, messages, state}) {
function App({onCommand, messages, game}) {
const inputRef = useRef()
const playAreaRef = useRef()
const [state, setState] = useState({})
function onSubmit(ev) {
if(ev) ev.preventDefault();
@ -36,6 +37,14 @@ function App({onCommand, messages, state}) {
return () => playArea.removeEventListener('click', onClick)
}, [])
useEffect(() => {
game.onChange(setState)
game.getState()
game.saveDraft()
}, [game])
const {directions, ...printedState} = state
return (
<div className={styles.app}>
<div ref={playAreaRef} className={styles.playArea}>
@ -56,7 +65,7 @@ function App({onCommand, messages, state}) {
</div>
<div className={styles.infoArea}>
<pre>
{JSON.stringify(state, null, 2)}
{JSON.stringify(printedState, jsonReplacer, 2)}
</pre>
</div>
</div>
@ -64,3 +73,16 @@ function App({onCommand, messages, state}) {
}
export default App;
function jsonReplacer(key, value) {
if(value instanceof Set)
return Array.from(value)
if(value instanceof Map)
return Array.from(value).reduce((obj, [key, value]) => {
obj[key] = value;
return obj;
}, {});
return value
}

@ -0,0 +1,68 @@
import {enableMapSet, createDraft, finishDraft, Draft} from 'immer'
import GameState, { Room, Door, Item, ObjectType } from './types/GameState'
enableMapSet()
type ChangeListener = (state : GameState) => void
export default class Game {
private gameState : GameState = {directions: new Map(), rooms: new Map(), doors: new Map(), items: new Map()}
private draft : Draft<GameState> | null = null
private onChangeListeners : ChangeListener [] = []
constructor() {
console.log('adding directions')
let state = this.getState()
state.directions.set('north', {type: ObjectType.Direction, name: 'north', aliases: ['n']})
state.directions.set('east', {type: ObjectType.Direction, name: 'east', aliases: ['e']})
state.directions.set('south', {type: ObjectType.Direction, name: 'south', aliases: ['s']})
state.directions.set('west', {type: ObjectType.Direction, name: 'west', aliases: ['w']})
state.directions.set('up', {type: ObjectType.Direction, name: 'up', aliases: ['u']})
state.directions.set('down', {type: ObjectType.Direction, name: 'down', aliases: ['d']})
this.saveDraft()
}
onChange(callback : ChangeListener) {
this.onChangeListeners.push(callback)
}
beginDraft() {
if(this.draft)
console.warn('Destroying already created gamestate draft')
this.draft = createDraft(this.gameState)
}
saveDraft() {
if(!this.draft)
throw new Error('Game has no open draft state')
this.gameState = finishDraft(this.draft)
this.draft = null
for(const callback of this.onChangeListeners)
callback(this.gameState)
}
getState() : Draft<GameState> {
if(!this.draft)
this.beginDraft()
return this.draft!
}
addRoom(room : Room) {
let state = this.getState()
state.rooms.set(room.name, room)
}
addDoor(door: Door) {
let state = this.getState()
state.doors.set(door.name, door)
}
addItem(item: Item) {
let state = this.getState()
state.items.set(item.name, item)
}
}

@ -0,0 +1,52 @@
import Game from "./Game";
import RulesEngine from './RulesEngine'
import ParsedCommand from "./types/ParsedCommand";
import Verb from './types/Verb';
export default class Parser {
private game : Game
private engine : RulesEngine
private verbs : Verb [] = []
constructor(gameState : Game, engine : RulesEngine) {
this.game = gameState
this.engine = engine
}
handleCommand(rawCommand : string) {
const grammaticalParsings : ParsedCommand[] = this.parseCommandString(rawCommand)
console.log(grammaticalParsings)
// const validParsings : ParsedCommand[] = this.game.filterCommandsForValidity(grammaticalParsings)
}
parseCommandString(rawCommand: string): ParsedCommand[] {
const words = rawCommand.toLocaleLowerCase().split(' ').filter(chunk => chunk !== '')
let parsings : ParsedCommand[] = []
for(const verb of this.verbs) {
parsings = [...parsings, ...verb.attemptParse(words)]
}
return parsings
}
understand(name : string) : VerbBuilder {
const verb = new Verb(name)
this.verbs.push(verb)
return new VerbBuilder(verb)
}
}
class VerbBuilder {
private verb : Verb
constructor(verb : Verb) {
this.verb = verb
}
as(template : string) : VerbBuilder {
this.verb.understand(template)
return this
}
}

@ -0,0 +1,40 @@
import React from 'react'
import ReactDOM from 'react-dom'
import '../index.css';
import App from '../components/App/App';
import Game from "./Game";
import Parser from "./Parser";
import GameEvent, { GameEventCommand } from './types/GameEvent'
export default class Renderer {
private parser : Parser
private game : Game
private output : GameEvent[] = []
constructor(parser : Parser, game : Game) {
this.parser = parser
this.game = game
}
start() {
this.render()
}
private handleCommand(command : string) {
this.output.push(new GameEventCommand(command))
this.render()
this.parser.handleCommand(command)
}
private render() {
ReactDOM.render(
<React.StrictMode>
<App messages={this.output} game={this.game} onCommand={this.handleCommand.bind(this)}/>
</React.StrictMode>,
document.getElementById('root')
)
}
}

@ -0,0 +1,4 @@
export default class RulesEngine {
// constructor() {
// }
}

@ -0,0 +1,26 @@
export enum GameEventType {
Message = 'message',
Command = 'command'
}
export default interface GameEvent {
getType() : GameEventType;
}
export class GameEventMessage implements GameEvent {
private message : string
getType() : GameEventType { return GameEventType.Message }
constructor(message : string) {
this.message = message
}
}
export class GameEventCommand implements GameEvent {
private rawCommand : string
getType() : GameEventType { return GameEventType.Command }
constructor(rawCommand : string) {
this.rawCommand = rawCommand
}
}

@ -0,0 +1,36 @@
type GameState = {
readonly directions: Map<ObjectID, GameObject>,
readonly rooms: Map<ObjectID, GameObject>,
readonly doors: Map<ObjectID, GameObject>,
readonly items: Map<ObjectID, GameObject>
}
export default GameState
export enum ObjectType {
Item = 'item',
Room = 'room',
Door = 'door',
Direction = 'direction'
}
type ObjectID = string
type GameObject = {
readonly type : ObjectType,
readonly name : ObjectID,
readonly aliases : string[]
}
export type Room = GameObject & {
readonly neighbors : Map<ObjectID, ObjectID>
}
export type Door = Room & {
readonly locked : boolean,
readonly key : ObjectID
}
export type Item = GameObject & {
readonly location: ObjectID | 'inventory'
}

@ -0,0 +1,6 @@
enum NounPosition {
Subject,
Object
}
export default NounPosition

@ -0,0 +1,53 @@
import { ObjectType } from "./GameState"
import NounPosition from "./NounPosition"
import Verb from "./Verb"
export enum TokenType {
Expression = 'expression',
Literal = 'literal'
}
export class ParsedToken {
readonly type : TokenType
constructor(type : TokenType) {
this.type = type
}
}
export class ParsedTokenLiteral extends ParsedToken {
readonly word : string
constructor(word : string) {
super(TokenType.Literal)
this.word = word
}
}
export class ParsedTokenExpression extends ParsedToken {
readonly itemType : ObjectType
readonly sentencePosition : NounPosition
readonly name : string
constructor(type : ObjectType, position : NounPosition, name : string) {
super(TokenType.Expression)
this.itemType = type
this.sentencePosition = position
this.name = name
}
}
export default class ParsedCommand {
readonly verb : Verb
private tokens : ParsedToken[] = []
constructor(verb : Verb) {
this.verb = verb
}
prepend(token : ParsedToken) {
const newCommand = new ParsedCommand(this.verb)
newCommand.tokens = [token, ...this.tokens]
return newCommand
}
}

@ -0,0 +1,164 @@
import ParsedCommand, {TokenType, ParsedTokenLiteral, ParsedTokenExpression} from './ParsedCommand'
import NounPosition from './NounPosition'
import { ObjectType } from './GameState'
export default class Verb {
static expressionRegex = /^\[([a-z|]+)\]$/
readonly name : string
private templates : Template[] = []
constructor(name : string) {
this.name = name
}
understand(templateString : string) {
const words : string[] = templateString.split(' ')
const tokens : TemplateToken[] = words.map((word : string) => {
if(!word.includes('[') && !word.includes(']'))
return new TemplateTokenLiteral(word)
if(!Verb.expressionRegex.test(word))
throw new Error(`Invalid template token "${word}"`)
const expressionString = (word.match(Verb.expressionRegex) || [])[1]
const compoundExpression = expressionString.includes('|')
const typeExpression = compoundExpression ? expressionString.split('|')[0] : expressionString
const positionExpression = compoundExpression ? expressionString.split('|')[1] : 'subject'
let type : ObjectType
if(typeExpression === 'item')
type = ObjectType.Item
else if(typeExpression === 'door')
type = ObjectType.Door
else if(typeExpression === 'room')
type = ObjectType.Room
else if(typeExpression === 'direction')
type = ObjectType.Direction
else
throw new Error(`Unknown object type "${typeExpression}"`)
let position : NounPosition
if(positionExpression === 'subject')
position = NounPosition.Subject
else if (positionExpression === 'object')
position = NounPosition.Object
else
throw new Error(`Unknown noun position "${positionExpression}"`)
return new TemplateTokenExpression(type, position)
})
// Check that we do not have two object expressions with the same position
const nounPositions = new Set<NounPosition>()
for(const token of tokens.filter(token => token.type === TokenType.Expression)){
const expression = token as TemplateTokenExpression
if(nounPositions.has(expression.nounPosition))
throw new Error(`Error parsing command template "${templateString}" - more than one ${expression.nounPosition} expression`)
nounPositions.add(expression.nounPosition)
}
this.templates.push(new Template(this, tokens))
}
attemptParse(words : string[]) : ParsedCommand[] {
let parsings : ParsedCommand[] = []
for(const template of this.templates){
parsings = [...parsings, ...template.parse(words)]
}
return parsings
}
}
class TemplateToken {
readonly type : TokenType
constructor(type : TokenType) {
this.type = type
}
}
class TemplateTokenLiteral extends TemplateToken {
readonly word : string
constructor(word : string) {
super(TokenType.Literal)
this.word = word
}
}
class TemplateTokenExpression extends TemplateToken {
readonly nounPosition : NounPosition
readonly itemType : ObjectType
constructor(type: ObjectType, position: NounPosition) {
super(TokenType.Expression)
this.nounPosition = position
this.itemType = type
}
}
class Template {
readonly verb : Verb
readonly tokens : TemplateToken[]
constructor(verb : Verb, tokens : TemplateToken[]) {
this.verb = verb
this.tokens = tokens
}
parse(words: string[]): ParsedCommand[] {
return this.attemptParse(this.tokens, words)
}
private attemptParse(tokens : TemplateToken[], words : string[]) : ParsedCommand[] {
// Base case: we matched every token and every word
if(tokens.length < 1 && words.length < 1)
return [new ParsedCommand(this.verb)]
// Base case: we have unmatched tokens or words
if(tokens.length < 1 || words.length < 1)
return []
// If we reached this point we still have tokens AND words to match
const nextToken = tokens[0]
if(nextToken.type === TokenType.Literal) {
const token = nextToken as TemplateTokenLiteral
let nextWord = words[0]
// If it doesn't match, no valid parsings
if(nextWord !== token.word)
return []
const thisToken = new ParsedTokenLiteral(nextWord)
return this.attemptParse(tokens.slice(1), words.slice(1)).map(parsing => parsing.prepend(thisToken))
} else if(nextToken.type === TokenType.Expression) {
const token = nextToken as TemplateTokenExpression
let parsings : ParsedCommand[] = []
// For all possible lengths of words we could capture in this expression
for(let n = 1; n <= words.length; n++) {
// Figure out what would be in the expression
const thisToken = new ParsedTokenExpression(token.itemType, token.nounPosition, words.slice(0, n).join(' '))
const potentialParsings = this.attemptParse(tokens.slice(1), words.slice(n)).map(parsing => parsing.prepend(thisToken))
// Add command parsings to the array we've been building
parsings = [...parsings, ...potentialParsings]
}
return parsings
} else {
throw new Error(`Unknown token type "${nextToken.type}"`)
}
}
}

@ -1,40 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App/App';
import Parser from './Parser'
import RulesEngine from './RuleEngine'
import GameState from './GameState';
// Rules and Parser
const parser = new Parser()
const engine = new RulesEngine()
const gameState = new GameState()
let messages = []
parser.setGameState(gameState)
parser.setEngine(engine);
parser.afterCommand(afterCommand);
parser.start()
function onCommand(command) {
messages = [...messages, {type: 'command', command}]
parser.handleCommand(command)
}
function afterCommand(newMessages) {
messages = [...messages, ...newMessages.map(message => ({type: 'message', message}))]
render()
}
function render() {
// Set up UI
ReactDOM.render(
<React.StrictMode>
<App messages={messages} state={gameState.getState()} onCommand={onCommand}/>
</React.StrictMode>,
document.getElementById('root')
);
}
render()

@ -0,0 +1,82 @@
import Game from './engine/Game'
import Parser from './engine/Parser'
import Renderer from './engine/Renderer'
import RulesEngine from './engine/RulesEngine'
import { ObjectType, Room, Door } from './engine/types/GameState'
let game = new Game()
let rules = new RulesEngine()
let parser = new Parser(game, rules)
let renderer = new Renderer(parser, game)
parser.understand('look')
.as('look')
.as('describe')
parser.understand('lookDirection')
.as('look [direction]')
parser.understand('go')
.as('go [direction]')
.as('[direction]')
parser.understand('take')
.as('take [item]')
.as('get [item]')
.as('pick up [item]')
.as('grab [item]')
.as('snatch [item]')
.as('steal [item]')
parser.understand('unlockDoor')
.as('unlock [door|subject] with [item|object]')
.as('unlock [door]')
.as('use [item|object] to unlock [door|subject]')
parser.understand('openDoor')
.as('open [door]')
.as('open [door|subject] with [item|object]')
const entry : Room = {
type: ObjectType.Room,
name: 'entry',
aliases: [],
neighbors: new Map()
}
const door : Door = {
type: ObjectType.Door,
name: 'door',
aliases: ['white door'],
neighbors: new Map(),
locked: true,
key: 'brass key'
}
const office : Room = {
type: ObjectType.Room,
name: 'office',
aliases: [],
neighbors: new Map()
}
entry.neighbors.set('east', 'door')
office.neighbors.set('west', 'door')
door.neighbors.set('east', 'office')
door.neighbors.set('west', 'entry')
game.addRoom(entry)
game.addRoom(office)
game.addDoor(door)
game.addItem({
type: ObjectType.Item,
name: 'brass key',
aliases: ['key'],
location: 'entry'
})
game.saveDraft()
renderer.start()

@ -0,0 +1 @@
/// <reference types="react-scripts" />

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
},
"include": [
"src"
]
}
Loading…
Cancel
Save