one way exits

trunk
nate smith 2023-05-25 23:03:58 -07:00
parent 3bb94c490e
commit 84cfc0f12d
7 changed files with 236 additions and 155 deletions

View File

@ -67,6 +67,37 @@ end)
The movement is still generated. The movement is still generated.
so the problem with all of this is that the beahvior of the exit is
supposedly going to change based on how it is contained. however, we only
re-read the code when the code changes. we're trying to affect game state
as part of code compilation and that's uncool.
a user would create a new exit, put all the finishing touches on it, then move
it from their person to the room the exit originates from. WITCH would not
recompile and no containership would update.
i think the dream of goes(north, "pub") is donezo. i think the next best
thing is goes(north, "foyer", "pub"). on execution, the sender is checked;
if their container is the first room then they are moved to the second
room. both cases suffer from the double containment problem. that second
containment *cannot* be updated as part of WITCH compilation.
so we're back to two options:
- one way exits
- special creation semantics that handle something like double containership
in the interest of moving on--and of putting off special top level commands
that exist outside of WITCH--i want to do one way exits for now. this sucks
because all the flavor written for an exit has to be duplicated for its pair.
some other ideas because i can't let go. what of a variation on the exits map
where each exit stores a key in its has data about where it goes. this is no
better than a dynamic handler (it's worse) and does not help the double
containership problem.
give up and do one way exits.
## server beta ## server beta
- [x] grpc server - [x] grpc server

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"log" "log"
"math/rand" "math/rand"
"strings"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v4/pgxpool" "github.com/jackc/pgx/v4/pgxpool"
@ -30,6 +31,7 @@ type DB interface {
// General // General
GetObject(owner, name string) (*Object, error) GetObject(owner, name string) (*Object, error)
GetObjectByID(ID int) (*Object, error) GetObjectByID(ID int) (*Object, error)
SearchObjectsByName(term string) ([]Object, error)
// Defaults // Defaults
Ensure() error Ensure() error
@ -41,7 +43,8 @@ type DB interface {
AvatarBySessionID(string) (*Object, error) AvatarBySessionID(string) (*Object, error)
BedroomBySessionID(string) (*Object, error) BedroomBySessionID(string) (*Object, error)
MoveInto(toMove Object, container Object) error MoveInto(toMove Object, container Object) error
Earshot(Object) ([]Object, error) Earshot(vantage Object) ([]Object, error)
Resolve(vantage Object, term string) ([]Object, error)
} }
type Account struct { type Account struct {
@ -488,6 +491,54 @@ func (db *pgDB) GetObject(owner, name string) (obj *Object, err error) {
return return
} }
func (db *pgDB) SearchObjectsByName(term string) ([]Object, error) {
ctx := context.Background()
stmt := `
SELECT id, avatar, data, owner, script
FROM objects
WHERE data['name']::varchar LIKE $1
`
rows, err := db.pool.Query(ctx, stmt, "%"+term+"%")
if err != nil {
return nil, err
}
out := []Object{}
for rows.Next() {
o := Object{}
if err = rows.Scan(
&o.ID,
&o.Avatar,
&o.Data,
&o.OwnerID,
&o.Script); err != nil {
return nil, err
}
out = append(out, o)
}
return out, nil
}
func (db *pgDB) Resolve(vantage Object, term string) ([]Object, error) {
stuff, err := db.Earshot(vantage)
if err != nil {
return nil, err
}
out := []Object{}
for _, o := range stuff {
if strings.Contains(o.Data["name"], term) {
out = append(out, o)
}
}
return out, nil
}
func (db *pgDB) GetAccountAvatar(account Account) (*Object, error) { func (db *pgDB) GetAccountAvatar(account Account) (*Object, error) {
ctx := context.Background() ctx := context.Background()
obj := &Object{ obj := &Object{

View File

@ -39,9 +39,3 @@ CREATE TABLE contains (
); );
CREATE TYPE heading AS ENUM ('north', 'south', 'east', 'west', 'above', 'below'); CREATE TYPE heading AS ENUM ('north', 'south', 'east', 'west', 'above', 'below');
CREATE TABLE exits (
startroom integer REFERENCES objects ON DELETE CASCADE,
endroom integer REFERENCES objects ON DELETE CASCADE,
direction heading NOT NULL
);

View File

@ -1,5 +1,7 @@
package witch package witch
import "fmt"
const ( const (
dirEast = "_DIR_EAST" dirEast = "_DIR_EAST"
dirWest = "_DIR_WEST" dirWest = "_DIR_WEST"
@ -13,6 +15,10 @@ type Direction struct {
raw string raw string
} }
func NewDirection(raw string) Direction {
return Direction{raw: raw}
}
func (d Direction) Reverse() Direction { func (d Direction) Reverse() Direction {
raw := "" raw := ""
switch d.raw { switch d.raw {
@ -29,11 +35,11 @@ func (d Direction) Reverse() Direction {
case dirSouth: case dirSouth:
raw = dirNorth raw = dirNorth
} }
return Direction{raw: raw} return NewDirection(raw)
} }
// NormalizeHuman takes a direction someone might type like "up" or "north" and returns the correct Direction struct // NormalizeDirection takes a direction someone might type like "up" or "north" and returns the correct Direction struct
func NormalizeHuman(humanDir string) Direction { func NormalizeDirection(humanDir string) (Direction, error) {
raw := "" raw := ""
switch humanDir { switch humanDir {
case "up": case "up":
@ -50,9 +56,11 @@ func NormalizeHuman(humanDir string) Direction {
raw = dirNorth raw = dirNorth
case "south": case "south":
raw = dirSouth raw = dirSouth
default:
return Direction{}, fmt.Errorf("did not understand direction '%s'", humanDir)
} }
return Direction{raw: raw}
return NewDirection(raw), nil
} }
// Human returns a string form of this direction like "above" or "north" // Human returns a string form of this direction like "above" or "north"
@ -74,3 +82,7 @@ func (d Direction) Human() (humanDir string) {
return humanDir return humanDir
} }
func (d Direction) Equals(o Direction) bool {
return d.raw == o.raw
}

View File

@ -1,116 +0,0 @@
package witch
/*
This file contains the definitions of functions that are injected into scope for WITCH scripts. See witch.go's ScriptContext to see how they are actually added to a LuaState.
TODO: consider making this (or witch.go) a different package entirely. the `witch` prefix for the function names in this file is a little annoying.
*/
import (
"log"
"strings"
lua "github.com/yuin/gopher-lua"
)
func witchHas(l *lua.LState) int {
l.SetGlobal("_has", l.ToTable(1))
return 0
}
func witchProvides(l *lua.LState) int {
// TODO test this manually
verbAndPattern := l.ToString(1)
cb := l.ToFunction(2)
split := strings.SplitN(verbAndPattern, " ", 2)
verb := split[0]
pattern := split[1]
return addPatternHandler(l, verb, pattern, cb)
}
func witchHears(l *lua.LState) int {
pattern := l.ToString(1)
cb := l.ToFunction(2)
return addPatternHandler(l, "say", pattern, cb)
}
func witchSees(l *lua.LState) int {
pattern := l.ToString(1)
cb := l.ToFunction(2)
return addPatternHandler(l, "emote", pattern, cb)
}
func witchGoes(l *lua.LState) int {
// TODO validate direction
// TODO convert direction constant to english
direction := l.ToString(1)
targetRoom := l.ToString(2)
cb := func(l *lua.LState) int {
log.Printf("please move sender to target room '%s'", targetRoom)
return 0
}
// TODO call addPatternHandler again for the reverse direction (make a reverse helper)
return addPatternHandler(l, "go", direction, l.NewFunction(cb))
}
func witchSeen(l *lua.LState) int {
cb := l.ToFunction(1)
return addHandler(l, "look", cb)
}
func witchMy(l *lua.LState) int {
hasT := l.GetGlobal("_has").(*lua.LTable)
val := hasT.RawGetString(l.ToString(1))
l.Push(val)
return 1
}
func witchDoes(ls *lua.LState) int {
// TODO how to feed events back into the server?
// it needs to behave like an event showing up in Commands stream
// this handler needs a reference to the gateway which has a channel for sending events that the server will see?
return 0
}
func addHandler(l *lua.LState, verb string, cb *lua.LFunction) int {
pattern := ".*"
log.Printf("adding handler: %s %s %#v", verb, pattern, cb)
handlers := l.GetGlobal("_handlers").(*lua.LTable)
verbHandlers, ok := handlers.RawGetString(verb).(*lua.LTable)
if !ok {
verbHandlers = l.NewTable()
handlers.RawSetString(verb, verbHandlers)
}
verbHandlers.RawSetString(pattern, cb)
return 0
}
func addPatternHandler(l *lua.LState, verb, pattern string, cb *lua.LFunction) int {
log.Printf("adding handler: %s %s %#v", verb, string(pattern), cb)
handlers := l.GetGlobal("_handlers").(*lua.LTable)
verbHandlers, ok := handlers.RawGetString(verb).(*lua.LTable)
if !ok {
verbHandlers = l.NewTable()
handlers.RawSetString(verb, verbHandlers)
}
verbHandlers.RawSetString(pattern, cb)
return 0
}

View File

@ -10,6 +10,7 @@ import (
"fmt" "fmt"
"log" "log"
"regexp" "regexp"
"strings"
"github.com/vilmibm/hermeticum/proto" "github.com/vilmibm/hermeticum/proto"
"github.com/vilmibm/hermeticum/server/db" "github.com/vilmibm/hermeticum/server/db"
@ -120,6 +121,7 @@ type ScriptContext struct {
func NewScriptContext(db db.DB, getSend func(string) func(*proto.ClientMessage) error) (*ScriptContext, error) { func NewScriptContext(db db.DB, getSend func(string) func(*proto.ClientMessage) error) (*ScriptContext, error) {
sc := &ScriptContext{ sc := &ScriptContext{
serverAPI: serverAPI{db: db, getSend: getSend}, serverAPI: serverAPI{db: db, getSend: getSend},
db: db,
} }
sc.incoming = make(chan VerbContext) sc.incoming = make(chan VerbContext)
@ -130,7 +132,6 @@ func NewScriptContext(db db.DB, getSend func(string) func(*proto.ClientMessage)
for { for {
vc = <-sc.incoming vc = <-sc.incoming
if vc.Target.Script != sc.script { if vc.Target.Script != sc.script {
// TODO clear this object out of the exits table
sc.script = vc.Target.Script sc.script = vc.Target.Script
l = lua.NewState() l = lua.NewState()
@ -145,16 +146,17 @@ func NewScriptContext(db db.DB, getSend func(string) func(*proto.ClientMessage)
l.SetGlobal("down", lua.LString(dirBelow)) l.SetGlobal("down", lua.LString(dirBelow))
// witch object behavior functions // witch object behavior functions
l.SetGlobal("has", l.NewFunction(witchHas)) l.SetGlobal("has", l.NewFunction(sc.wHas))
l.SetGlobal("hears", l.NewFunction(witchHears)) l.SetGlobal("hears", l.NewFunction(sc.wHears))
l.SetGlobal("sees", l.NewFunction(witchSees)) l.SetGlobal("sees", l.NewFunction(sc.wSees))
l.SetGlobal("goes", l.NewFunction(witchGoes)) l.SetGlobal("goes", l.NewFunction(sc.wGoes))
l.SetGlobal("seen", l.NewFunction(witchSeen)) l.SetGlobal("seen", l.NewFunction(sc.wSeen))
l.SetGlobal("my", l.NewFunction(witchMy)) l.SetGlobal("my", l.NewFunction(sc.wMy))
l.SetGlobal("provides", l.NewFunction(witchProvides)) l.SetGlobal("provides", l.NewFunction(sc.wProvides))
// witch helpers // witch helpers
l.SetGlobal("_handlers", l.NewTable()) l.SetGlobal("_handlers", l.NewTable())
l.SetGlobal("_ID", lua.LNumber(vc.Target.ID))
if err := l.DoString(vc.Target.Script); err != nil { if err := l.DoString(vc.Target.Script); err != nil {
log.Printf("error parsing script %s: %s", vc.Target.Script, err.Error()) log.Printf("error parsing script %s: %s", vc.Target.Script, err.Error())
@ -187,7 +189,7 @@ func NewScriptContext(db db.DB, getSend func(string) func(*proto.ClientMessage)
senderID := int(lua.LVAsNumber(sender.RawGetString("ID"))) senderID := int(lua.LVAsNumber(sender.RawGetString("ID")))
owner := l.ToString(1) owner := l.ToString(1)
name := l.ToString(2) name := l.ToString(2)
db := sc.serverAPI.DB() db := sc.db
senderObj, err := db.GetObjectByID(senderID) senderObj, err := db.GetObjectByID(senderID)
if err != nil { if err != nil {
log.Println(err.Error()) log.Println(err.Error())
@ -224,6 +226,7 @@ func NewScriptContext(db db.DB, getSend func(string) func(*proto.ClientMessage)
senderT.RawSetString("ID", lua.LNumber(vc.Sender.ID)) senderT.RawSetString("ID", lua.LNumber(vc.Sender.ID))
l.SetGlobal("sender", senderT) l.SetGlobal("sender", senderT)
l.SetGlobal("msg", lua.LString(vc.Rest)) l.SetGlobal("msg", lua.LString(vc.Rest))
l.SetGlobal("_SENDERID", lua.LNumber(vc.Sender.ID))
handlers := l.GetGlobal("_handlers").(*lua.LTable) handlers := l.GetGlobal("_handlers").(*lua.LTable)
handlers.ForEach(func(k, v lua.LValue) { handlers.ForEach(func(k, v lua.LValue) {
@ -254,3 +257,126 @@ func NewScriptContext(db db.DB, getSend func(string) func(*proto.ClientMessage)
func (sc *ScriptContext) Handle(vc VerbContext) { func (sc *ScriptContext) Handle(vc VerbContext) {
sc.incoming <- vc sc.incoming <- vc
} }
func (sc *ScriptContext) addHandler(l *lua.LState, verb, pattern string, cb *lua.LFunction) {
log.Printf("adding handler: %s %s %#v", verb, string(pattern), cb)
handlers := l.GetGlobal("_handlers").(*lua.LTable)
verbHandlers, ok := handlers.RawGetString(verb).(*lua.LTable)
if !ok {
verbHandlers = l.NewTable()
handlers.RawSetString(verb, verbHandlers)
}
verbHandlers.RawSetString(pattern, cb)
}
func (sc *ScriptContext) wMy(l *lua.LState) int {
hasT := l.GetGlobal("_has").(*lua.LTable)
val := hasT.RawGetString(l.ToString(1))
l.Push(val)
return 1
}
func (sc *ScriptContext) wHas(l *lua.LState) int {
l.SetGlobal("_has", l.ToTable(1))
return 0
}
func (sc *ScriptContext) wHears(l *lua.LState) int {
pattern := l.ToString(1)
cb := l.ToFunction(2)
sc.addHandler(l, "say", pattern, cb)
return 0
}
func (sc *ScriptContext) wSees(l *lua.LState) int {
pattern := l.ToString(1)
cb := l.ToFunction(2)
sc.addHandler(l, "emote", pattern, cb)
return 0
}
func (sc *ScriptContext) wSeen(l *lua.LState) int {
cb := l.ToFunction(1)
sc.addHandler(l, "look", ".*", cb)
return 0
}
func (sc *ScriptContext) wDoes(ls *lua.LState) int {
// TODO how to feed events back into the server?
// it needs to behave like an event showing up in Commands stream
// this handler needs a reference to the gateway which has a channel for sending events that the server will see?
return 0
}
func (sc *ScriptContext) wProvides(l *lua.LState) int {
// TODO test this manually
verbAndPattern := l.ToString(1)
cb := l.ToFunction(2)
split := strings.SplitN(verbAndPattern, " ", 2)
verb := split[0]
pattern := split[1]
sc.addHandler(l, verb, pattern, cb)
return 0
}
func (sc *ScriptContext) wGoes(l *lua.LState) int {
direction := NewDirection(l.ToString(1))
targetRoomTerm := l.ToString(2)
log.Printf("GOT DIRECTION %v", direction)
cb := func(l *lua.LState) (ret int) {
targetRoomList, err := sc.db.SearchObjectsByName(targetRoomTerm)
if err != nil {
log.Printf("failed to search for target room: %s", err.Error())
return
}
switch len(targetRoomList) {
case 0:
log.Printf("failed to find any matching target room. tell player somehow")
return
case 1:
log.Printf("found the target room")
default:
log.Printf("found too many matching target rooms. tell player somehow")
return
}
targetRoom := targetRoomList[0]
msg := l.GetGlobal("msg").String()
normalized, err := NormalizeDirection(msg)
if err != nil {
return
}
sender, err := sc.getSenderFromState(l)
if err != nil {
log.Printf("failed to find sender %s", err.Error())
return
}
if normalized.Equals(direction) {
log.Printf("MOVING SENDER TO '%s'", targetRoom.Data["name"])
// TODO error checking
sc.db.MoveInto(*sender, targetRoom)
sc.serverAPI.Tell(targetRoom.ID, sender.ID, fmt.Sprintf("you are now in %s", targetRoom.Data["name"]))
}
return
}
sc.addHandler(l, "go", ".*", l.NewFunction(cb))
return 0
}
func (sc *ScriptContext) getSenderFromState(l *lua.LState) (*db.Object, error) {
lsID := lua.LVAsNumber(l.GetGlobal("_SENDERID"))
return sc.db.GetObjectByID(int(lsID))
}

View File

@ -173,21 +173,4 @@ allows({
execute = "world", execute = "world",
}) })
-- option 1: fully manual goes("east", "gallery")
provides("go east", function(args)
if sender.where = "gallery" then
move_sender("ossuary")
end
end)
provides("go west", function(args)
if sender.where = "ossuary" then
move_sender("gallery")
end
end)
-- option 2: magical helper
-- automatically creates the two `go` handlers above
goes("east", "gallery", "ossuary")