verb seek complete

This commit is contained in:
Shoofle 2024-10-10 22:54:22 -04:00
parent 12f1283e4e
commit 534ffeaf8e
11 changed files with 425 additions and 43 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
the_big_db.db the_big_db.db
dump*.json
# dependencies # dependencies
node_modules node_modules

View File

@ -19,7 +19,7 @@ function Live({editing, ...props}) {
const [ currentNumber, setCurrentNumber ] = useState(1); const [ currentNumber, setCurrentNumber ] = useState(1);
const [ connecting, setConnecting ] = useState(true); const [ connecting, setConnecting ] = useState(true);
const { sendMessage, lastMessage, readyState } = useWebSocket(`${wsUrl}/embody`, { const { sendMessage, lastMessage, lastJsonMessage, readyState } = useWebSocket(`${wsUrl}/embody`, {
onClose: () => setConnecting(false) onClose: () => setConnecting(false)
}, connecting); }, connecting);
@ -35,23 +35,34 @@ function Live({editing, ...props}) {
} }
}, [lastMessage]); }, [lastMessage]);
function handleSendMessage() { // spread this return value on <a/> elements in order to make them navigate
console.log("sending a message..."); const commandLinkClick = (e) => {
sendMessage("button got clicked"); if (e.target.tagName != "A") { return; }
} if (!e.target.href.includes(window.location.origin)) { return; }
e.preventDefault();
const localLink = e.target.href.replace(window.location.origin, "");
const targetString = localLink.replace("/", "");
const targetNumber = Number(targetString);
if (targetNumber != 0 && targetNumber != NaN) {
const warpMessage = `warp #${targetString}`;
console.log(`clicked a link, executing "warp #${targetString}"`);
sendMessage(warpMessage);
return;
}
navigate(e.target.href.replace(window.location.origin, ""));
};
return ( return (
<> <>
<Page editing={editing} number={currentNumber} {...props}/> <Page editing={editing} number={currentNumber} linkClick={commandLinkClick} {...props} />
<MessageFeed messages={messageHistory}> <MessageFeed messages={messageHistory}>
<button disabled={readyState!=ReadyState.OPEN} onClick={handleSendMessage}>send a message to the server!</button>
<button disabled={connecting} onClick={() => setConnecting(true)}>Reconnect</button> <button disabled={connecting} onClick={() => setConnecting(true)}>Reconnect</button>
<button onClick={()=> setMessageHistory([])}>Clear History</button> <button onClick={()=> setMessageHistory([])}>Clear History</button>
</MessageFeed> </MessageFeed>
<Sidebar pagenumber={currentNumber} hidden="true" sendWord={(word) => setCommand((command + " " + word).trim())}> <Sidebar pagenumber={currentNumber} hidden="true" sendWord={(word) => setCommand((command + " " + word).trim())}>
{!editing && <li><button onClick={() => navigate(`/${currentNumber}/edit`)}>edit</button></li>}
{editing && <li><button disabled={postMutation.isPending} onClick={submitChanges}>save</button></li>}
{editing && <li><button onClick={() => navigate(`/${currentNumber}`)}>return</button></li>}
</Sidebar> </Sidebar>
<CommandEntry onSubmit={sendMessage}/> <CommandEntry onSubmit={sendMessage}/>
</> </>

View File

@ -2,7 +2,6 @@ import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { apiUrl, fetchPage, fetchPageAtEdit, postPage, deletePage } from '../apiTools.jsx'; import { apiUrl, fetchPage, fetchPageAtEdit, postPage, deletePage } from '../apiTools.jsx';
import { useFixLinks } from '../clientStuff.jsx';
import { useLoggedIn } from '../AuthProvider.jsx'; import { useLoggedIn } from '../AuthProvider.jsx';
import Page from './Page.jsx'; import Page from './Page.jsx';
@ -10,10 +9,21 @@ import './Pages.css';
function GhostPage({editing, ...props}) { function GhostPage({editing, ...props}) {
const { pagenumber, editid } = useParams(); const { pagenumber, editid } = useParams();
const navigate = useNavigate();
return ( const linkClick = (e) => {
<Page number={pagenumber} editid={editid} editing={editing} {...props}/> if (e.target.tagName != "A") { return; }
); if (!e.target.href.includes(window.location.origin)) { return; }
e.preventDefault();
navigate(e.target.href.replace(window.location.origin, ""));
};
return <Page
number={pagenumber}
editid={editid}
editing={editing}
linkClick={linkClick}
{...props}/>;
} }
export default GhostPage; export default GhostPage;

View File

@ -6,11 +6,10 @@ import { useLoggedIn } from '../AuthProvider.jsx';
import './Pages.css'; import './Pages.css';
function Page({ editing, number, editid=null}) { function Page({ editing, number, editid=null, linkClick=()=>{} }) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const navigate = useNavigate(); const navigate = useNavigate();
const loggedIn = useLoggedIn(); const loggedIn = useLoggedIn();
const noLoad = useFixLinks();
const fetchQuery = useQuery({ // fetch the currrent values const fetchQuery = useQuery({ // fetch the currrent values
queryKey: ['page', number, editid], queryKey: ['page', number, editid],
@ -61,7 +60,7 @@ function Page({ editing, number, editid=null}) {
<div className="main-column"> <div className="main-column">
<header> <header>
<h1> <h1>
<a href="/" {...noLoad}>🌳</a> <a href="/" onClick={linkClick}>🌳</a>
{number}.&nbsp; {number}.&nbsp;
<span <span
contentEditable={editing} contentEditable={editing}
@ -80,7 +79,7 @@ function Page({ editing, number, editid=null}) {
: :
<div <div
dangerouslySetInnerHTML={{__html: html}} dangerouslySetInnerHTML={{__html: html}}
{...noLoad} /> onClick={linkClick} />
) )
: :
"..." "..."

View File

@ -0,0 +1,11 @@
const sqlite = require("better-sqlite3");
const db = new sqlite('the_big_db.db', { verbose: console.log });
const fs = require('node:fs');
const dump = {
pages: db.prepare('select * from pages').all(),
users: db.prepare('select * from users').all()
};
fs.writeFile(`dump_${new Date().toISOString()}.json`, JSON.stringify(dump), (err) => console.log(err) );

View File

@ -0,0 +1,187 @@
// wipe and load the database from a dump file created by dump_blahblah.js
const hljs = require('highlight.js/lib/core');
hljs.registerLanguage('lua', require('highlight.js/lib/languages/lua'));
const fs = require('node:fs');
const sqlite = require("better-sqlite3");
const showdown = require("showdown");
const db = new sqlite('the_big_db.db', { verbose: console.log });
const converter = new showdown.Converter();
const dump_file_name = process.argv[2]
//get argument out of 'node db_scripts/initialize_db-blah-blah.js dumpfile'
let data = "{}";
try {
data = fs.readFileSync(dump_file_name, 'utf8');
} catch (error) {
console.log(error);
return;
}
//open that file as text (unicode i guess?)
console.log("opened file");
const filejson = JSON.parse(data);
//parse it as json
const pages = filejson["pages"];
const createPages = db.prepare(`
create table if not exists pages (
id integer primary key,
number integer,
title varchar(255),
description text,
html text,
lua text,
time timestamp default current_timestamp,
author integer,
type integer
)
`);
/* an object has:
id = primary key
number = object identifier number
title / NAME = a short display name for an object, or the title of a page
description / TEXT? = the markdown description of an object, or the lua script of the source code of a verb
html / HTML_MARKDOWN? = the markdown for that object, rendered into html for display as a page
HTML_LUA? = the lua source code of the object, rendered into html with syntax highlighting?
time = when this edit was saved
author = the user id of whoever authored this revision
TYPE = whether this represents an object or a verb
*/
function migratePages(pages) {
console.log("moving old table to a temporary");
console.log(db.prepare(`alter table pages rename to old_pages`).run())
console.log("creating new page table")
console.log(createPages.run());
console.log('iterating over dump pages');
const insertPage = db.prepare(`insert into pages
(id, number, title, description, html, lua, time, author, type)
values (:id, :number, :title, :description, :html, :lua, :time, :author, :type)`);
pages.forEach((pageData) => {
let {id, number, description} = pageData;
console.log(`rendering page number ${number}.${id}`)
description = description.replace(/<br>/g, "\n");
const renderedPage = converter.makeHtml(description);
const highlightedLua = hljs.highlight(description, {language: 'lua'}).value;
args = { ...pageData, description: description, html: renderedPage, lua: highlightedLua, type: "noun"};
console.log("inserting args", args);
insertPage.run(args);
});
console.log("getting rid of old table");
console.log(db.prepare("drop table old_pages").run());
//throw new Error("blahhh i'm a dracula");
}
try {
db.transaction(migratePages)(filejson["pages"]);
} catch (error) {
console.log(error);
}
const createAttributesQuery = db.prepare(`
create table if not exists attributes (
number integer unique,
contents json
)
`);
/* an attribute store has:
number = the object number it's attached to
contents = a `text` object holding a json store of attributes defined on object #number, containing:
LOCATION? = the object which contains this object? should this be in the DB or in the json store?
PROTOTYPE = the number of the prototype for this object, to which we should look for attributes not found here
ATTRIBUTES? = json object of attached, script-controlled attributes.
CONTENTS = json array of objectss contained inside this one
VERBS = json array of objecsts which are verbs on this one
*/
function createAttributes() {
console.log("creating new attribute table")
console.log(createAttributesQuery.run());
console.log('iterating over all pages');
const insertAttribute = db.prepare(`insert into attributes
(number, contents) values (:number, :contents)`);
db.prepare(`select * from pages`).all().forEach((pageData) => {
let { number } = pageData;
let args = {contents: JSON.stringify({}), number: number};
console.log("inserting args", args);
insertAttribute.run(args);
});
//throw new Error("blahhh i'm a dracula");
}
try {
db.transaction(createAttributes)();
} catch (error) {
console.log(error);
}
const createUsers = db.prepare(`
create table if not exists users (
id integer primary key,
name varchar(64) unique,
password varchar(128),
character integer
)
`);
/* a user has:
id = primary key
name = name string
password = argon2 hash of their password
character = object in the game world representing your character
*/
function migrateUsers(users) {
console.log("moving old users");
console.log(db.prepare("alter table users rename to old_users").run());
console.log("creating new users table");
console.log(createUsers.run());
const insertUser = db.prepare("insert into users (name, password) values (:name, :password)")
users.forEach((user) => {
console.log(insertUser.run(user));
});
console.log("clearing old table");
console.log(db.prepare("drop table old_users").run());
//throw new Error("i'm dracula 2");
}
try {
db.transaction(migrateUsers)(filejson["users"]);
} catch (error) {
console.log(error);
}

View File

@ -22,6 +22,34 @@ create table if not exists pages (
author integer author integer
) )
`); `);
/* an object has:
id = primary key
number = object identifier number
title / NAME = a short display name for an object, or the title of a page
description / TEXT? = the markdown description of an object, or the lua script of the source code of a verb
html / HTML_MARKDOWN? = the markdown for that object, rendered into html for display as a page
HTML_LUA? = the lua source code of the object, rendered into html with syntax highlighting?
time = when this edit was saved
author = the user id of whoever authored this revision
TYPE = whether this represents an object or a verb
*/
/* an attribute store has:
number = the object number it's attached to
contents = a `text` object holding a json store of attributes defined on object #number, containing:
LOCATION? = the object which contains this object? should this be in the DB or in the json store?
PROTOTYPE = the number of the prototype for this object, to which we should look for attributes not found here
ATTRIBUTES? = json object of attached, script-controlled attributes.
CONTENTS = json array of objectss contained inside this one
VERBS = json array of objecsts which are verbs on this one
*/
const createUsers = db.prepare(` const createUsers = db.prepare(`
create table if not exists users ( create table if not exists users (
@ -31,7 +59,13 @@ create table if not exists users (
password varchar(128) password varchar(128)
) )
`); `);
/* a user has:
id = primary key
name = name string
password = argon2 hash of their password
character = object in the game world representing your character
*/
function migratePages() { function migratePages() {
console.log("moving old table to a temporary"); console.log("moving old table to a temporary");

View File

@ -10,19 +10,27 @@ function interpret(context, player, command) {
// may be null if it's executing from a script? // may be null if it's executing from a script?
// player: the id of the player object who typed in the command. // player: the id of the player object who typed in the command.
// may? be null if it's executing from a script? used for output. // may? be null if it's executing from a script? used for output.
// so arguably
// command: the full string typed in by the player // command: the full string typed in by the player
const socket = sockets.get(player) const socket = sockets.get(player)
const words = command.trim().split(' '); const words = command.trim().split(' ');
socket?.send(`interpreting and executing the received command: ${command}`); try {
socket?.send(`interpreting and executing the received command: ${command}`);
const verbs = findAvailableVerbs(context, player, command); //for the moment we'll only allow statements of the form "go north" with implicit subject: the player
socket?.send(`found these verbs: ${Array.from(verbs.keys()).join(', ')}`) const [first, second, third, ...rest] = words;
// first word is either a subject or a verb. either way there must be a verb. const verb = findVerb(first, context, player);
const [first, second, third, ...rest] = words;
console.log(`executing verb ${verb} towards ${player} on ${player} ${second} ${third}`);
executeVerb(player, verb, player, second, third, ...rest)
} catch (error) {
socket?.send(`got an error! ${error.mesage}`);
}
/*
// if the second word is a verb, but the first word is also a verb, what do we do? // if the second word is a verb, but the first word is also a verb, what do we do?
if (second in verbs) { if (second in verbs) {
// for now, assume that if the second word is a verb, it's the verb we want. // for now, assume that if the second word is a verb, it's the verb we want.
@ -31,16 +39,54 @@ function interpret(context, player, command) {
// if the second word is not a verb, then the first word is the verb, and the player is the implied subject. // if the second word is not a verb, then the first word is the verb, and the player is the implied subject.
executeVerb(player, verbs.get(first), player, second, third, ...rest) executeVerb(player, verbs.get(first), player, second, third, ...rest)
} }
*/
} }
function findAvailableVerbs(location, actor, command) { function findVerb(word, location, actor) {
// returns the number of a verb which matches the requested word.
// check for verbs on actor // check for verbs on actor
// check for verbs on objects in location let verb = findVerbOnObject(word, actor);
// check for verbs on location if (verb) return verb;
const out = new Map();
out.set("look", 29); const actorContents = getAttribute(actor, "contents");
out.set("warp", 31); for (const obj of (actorContents || [])) {
return out; verb = findVerbOnObject(word, obj);
if (verb) return verb;
}
const locationContents = getAttribute(location, "contents");
for (const obj of (locationContents || [])) {
verb = findVerbOnObject(word, obj);
if (verb) return verb;
}
verb = findVerbOnObject(word, location);
if (verb) return verb;
return undefined;
}
function findVerbOnObject(word, object) {
// check for verbs on a single object, and its chain of parents. return a matching verbId, or undefined
if (!word) return undefined;
let focus = object;
while (focus) {
const focusVerbs = getAttribute(focus, "verbs");
for (const verbId of (focusVerbs || [])) {
const fullVerb = lookUpObject(verbId);
if (!fullVerb) continue;
// test our word against
if (word.toLowerCase() == fullVerb.title.toLowerCase()) {
return verbId;
}
}
focus = getAttribute(focus, "parent");
}
return undefined;
} }
function executeVerb(outputObject, verbId, subject, object, ...rest) { function executeVerb(outputObject, verbId, subject, object, ...rest) {
@ -56,28 +102,99 @@ function executeVerb(outputObject, verbId, subject, object, ...rest) {
return fullVerb.fn(outputObject, subject, object, ...rest); return fullVerb.fn(outputObject, subject, object, ...rest);
} }
const objectQuery = db.prepare('select * from pages where id=?'); const objectQuery = db.prepare('select * from pages where number=? order by time desc');
function lookUpObject(id) { function lookUpObject(number) {
return hardCodedObjects(number) || objectQuery.get(number);
}
const attributeQuery = db.prepare(`select * from attributes where number=?`);
function lookUpObjectAttributes(number) {
return JSON.parse(attributeQuery.get(number).contents);
}
function hardCodedObjects(id) {
// return objectQuery.get(id); // return objectQuery.get(id);
if (id == 30) return {name: "shoofle", contents: "this is a shoofle", location: 1};
if (id == 29) return { if (id == 29) return {
name: "look", title: "look",
verb: false, verb: true,
contents: "send description of direct object to subject's socket", description: "send description of direct object to subject's socket",
fn: (logReceiver, subject, object, ...rest) => { fn: (logReceiver, subject, object, ...rest) => {
sockets.get(logReceiver)?.send(`you looked around! subject ${subject} object: ${object} args: ${rest}`); sockets.get(logReceiver)?.send(`you looked around! subject ${subject} object: ${object} args: ${rest}`);
console.log(`${subject} looked at ${object} with args ${rest}, and output was directed to ${logReceiver}`); console.log(`${subject} looked at ${object} with args ${rest}, and output was directed to ${logReceiver}`);
} }
} }
if (id == 31) return { if (id == 31) return {
name: "warp", title: "warp",
verb: true, verb: true,
contents: "this built-in function changes the player's location, as well as telling their client to move", description: "this built-in function changes the player's location, as well as telling their client to move",
fn: (logReceiver, subject, object, ...rest) => { fn: (logReceiver, subject, object, ...rest) => {
// TODO: change subject's location const objectNum = verifyObjectReference(object);
const subjectNum = verifyObjectReference(subject);
setAttribute(subjectNum, "location", objectNum);
sockets.get(logReceiver)?.send(`location change to: ${object}`); sockets.get(logReceiver)?.send(`location change to: ${object}`);
} }
} }
} }
module.exports = { interpret, sockets, lookUpObject };
const pullAttribute = db.prepare(`select * from attributes where number=?`);
function getAttribute(obj, attributeName) {
const attributeStore = pullAttribute.get(verifyObjectReference(obj));
const contents = JSON.parse(attributeStore.contents);
if (contents.hasOwnProperty(attributeName)) {
return contents[attributeName];
}
if (contents.hasOwnProperty("parent")) {
return getAttribute(contents.parent, attributeName);
}
return undefined;
}
function hasOwnAttribute(obj, attributeName) {
const attributeStore = pullAttribute.get(verifyObjectReference(obj));
const contents = JSON.parse(attributeStore.contents);
return contents.hasOwnProperty(attributeName);
}
const insertAttribute = db.prepare(`update or replace attributes set contents=:contents where number=:number`);
function setAttribute(obj, attributeName, value) {
const attributeStore = pullAttribute.get(verifyObjectReference(obj));
const contents = JSON.parse(attributeStore.contents);
contents[attributeName] = value;
insertAttribute.run({...attributeStore, contents: JSON.stringify(contents)});
}
function deleteAttribute(obj, attributeName) {
const attributeStore = pullAttribute.get(verifyObjectReference(obj));
const contents = JSON.parse(attributeStore.contents);
delete contents["attributeName"];
insertAttribute.run({...attributeStore, contents: JSON.stringify(contents)});
}
function verifyObjectReference(obj) {
try {
if (typeof(obj) === "string") {
return Number(obj.replace("#", ""));
} else if (typeof(obj) === "number") {
return obj;
} else if (typeof(obj.number) == "number") {
return obj.number;
}
} catch (error) {
throw new Error("Tried to get an attribute from something which wasn't an object");
}
}
module.exports = { interpret, sockets, lookUpObject, getAttribute, setAttribute, deleteAttribute, hasOwnAttribute };

View File

@ -21,6 +21,7 @@
"graphology-layout": "^0.6.1", "graphology-layout": "^0.6.1",
"graphology-layout-force": "^0.2.4", "graphology-layout-force": "^0.2.4",
"graphology-layout-forceatlas2": "^0.10.1", "graphology-layout-forceatlas2": "^0.10.1",
"highlight.js": "^11.10.0",
"jsdom": "^25.0.0", "jsdom": "^25.0.0",
"nodemon": "^3.1.5", "nodemon": "^3.1.5",
"showdown": "^2.1.0" "showdown": "^2.1.0"
@ -1128,6 +1129,14 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/highlight.js": {
"version": "11.10.0",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.10.0.tgz",
"integrity": "sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/html-encoding-sniffer": { "node_modules/html-encoding-sniffer": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",

View File

@ -26,6 +26,7 @@
"graphology-layout": "^0.6.1", "graphology-layout": "^0.6.1",
"graphology-layout-force": "^0.2.4", "graphology-layout-force": "^0.2.4",
"graphology-layout-forceatlas2": "^0.10.1", "graphology-layout-forceatlas2": "^0.10.1",
"highlight.js": "^11.10.0",
"jsdom": "^25.0.0", "jsdom": "^25.0.0",
"nodemon": "^3.1.5", "nodemon": "^3.1.5",
"showdown": "^2.1.0" "showdown": "^2.1.0"

View File

@ -7,7 +7,8 @@ const db = new sqlite('the_big_db.db', { verbose: console.log });
const { loginRequired } = require('../authStuff.js'); const { loginRequired } = require('../authStuff.js');
const { interpret, sockets, lookUpObject } = require('../interpreter.js'); const { interpret, sockets, lookUpObject,
getAttribute, setAttribute, hasOwnAttribute, deleteAttribute } = require('../interpreter.js');
@ -15,6 +16,7 @@ app.ws('/embody', (ws, req) => {
// const { playerObject } = db.prepare('select playerObject from users where id=?').get(req.session.userId) // const { playerObject } = db.prepare('select playerObject from users where id=?').get(req.session.userId)
const playerObjectId = 30; // mocked out for now! const playerObjectId = 30; // mocked out for now!
sockets.set(playerObjectId, ws); sockets.set(playerObjectId, ws);
ws.send(`location change to: #${getAttribute(playerObjectId, "location")}`);
ws.on('message', (msg) => { ws.on('message', (msg) => {
const location = lookUpObject(playerObjectId).location; const location = lookUpObject(playerObjectId).location;