one way exits
parent
3bb94c490e
commit
84cfc0f12d
31
roadmap.md
31
roadmap.md
|
@ -67,6 +67,37 @@ end)
|
|||
|
||||
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
|
||||
|
||||
- [x] grpc server
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
|
@ -30,6 +31,7 @@ type DB interface {
|
|||
// General
|
||||
GetObject(owner, name string) (*Object, error)
|
||||
GetObjectByID(ID int) (*Object, error)
|
||||
SearchObjectsByName(term string) ([]Object, error)
|
||||
|
||||
// Defaults
|
||||
Ensure() error
|
||||
|
@ -41,7 +43,8 @@ type DB interface {
|
|||
AvatarBySessionID(string) (*Object, error)
|
||||
BedroomBySessionID(string) (*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 {
|
||||
|
@ -488,6 +491,54 @@ func (db *pgDB) GetObject(owner, name string) (obj *Object, err error) {
|
|||
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) {
|
||||
ctx := context.Background()
|
||||
obj := &Object{
|
||||
|
|
|
@ -39,9 +39,3 @@ CREATE TABLE contains (
|
|||
);
|
||||
|
||||
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
|
||||
);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package witch
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
dirEast = "_DIR_EAST"
|
||||
dirWest = "_DIR_WEST"
|
||||
|
@ -13,6 +15,10 @@ type Direction struct {
|
|||
raw string
|
||||
}
|
||||
|
||||
func NewDirection(raw string) Direction {
|
||||
return Direction{raw: raw}
|
||||
}
|
||||
|
||||
func (d Direction) Reverse() Direction {
|
||||
raw := ""
|
||||
switch d.raw {
|
||||
|
@ -29,11 +35,11 @@ func (d Direction) Reverse() Direction {
|
|||
case dirSouth:
|
||||
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
|
||||
func NormalizeHuman(humanDir string) Direction {
|
||||
// NormalizeDirection takes a direction someone might type like "up" or "north" and returns the correct Direction struct
|
||||
func NormalizeDirection(humanDir string) (Direction, error) {
|
||||
raw := ""
|
||||
switch humanDir {
|
||||
case "up":
|
||||
|
@ -50,9 +56,11 @@ func NormalizeHuman(humanDir string) Direction {
|
|||
raw = dirNorth
|
||||
case "south":
|
||||
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"
|
||||
|
@ -74,3 +82,7 @@ func (d Direction) Human() (humanDir string) {
|
|||
|
||||
return humanDir
|
||||
}
|
||||
|
||||
func (d Direction) Equals(o Direction) bool {
|
||||
return d.raw == o.raw
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -10,6 +10,7 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/vilmibm/hermeticum/proto"
|
||||
"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) {
|
||||
sc := &ScriptContext{
|
||||
serverAPI: serverAPI{db: db, getSend: getSend},
|
||||
db: db,
|
||||
}
|
||||
sc.incoming = make(chan VerbContext)
|
||||
|
||||
|
@ -130,7 +132,6 @@ func NewScriptContext(db db.DB, getSend func(string) func(*proto.ClientMessage)
|
|||
for {
|
||||
vc = <-sc.incoming
|
||||
if vc.Target.Script != sc.script {
|
||||
// TODO clear this object out of the exits table
|
||||
sc.script = vc.Target.Script
|
||||
l = lua.NewState()
|
||||
|
||||
|
@ -145,16 +146,17 @@ func NewScriptContext(db db.DB, getSend func(string) func(*proto.ClientMessage)
|
|||
l.SetGlobal("down", lua.LString(dirBelow))
|
||||
|
||||
// witch object behavior functions
|
||||
l.SetGlobal("has", l.NewFunction(witchHas))
|
||||
l.SetGlobal("hears", l.NewFunction(witchHears))
|
||||
l.SetGlobal("sees", l.NewFunction(witchSees))
|
||||
l.SetGlobal("goes", l.NewFunction(witchGoes))
|
||||
l.SetGlobal("seen", l.NewFunction(witchSeen))
|
||||
l.SetGlobal("my", l.NewFunction(witchMy))
|
||||
l.SetGlobal("provides", l.NewFunction(witchProvides))
|
||||
l.SetGlobal("has", l.NewFunction(sc.wHas))
|
||||
l.SetGlobal("hears", l.NewFunction(sc.wHears))
|
||||
l.SetGlobal("sees", l.NewFunction(sc.wSees))
|
||||
l.SetGlobal("goes", l.NewFunction(sc.wGoes))
|
||||
l.SetGlobal("seen", l.NewFunction(sc.wSeen))
|
||||
l.SetGlobal("my", l.NewFunction(sc.wMy))
|
||||
l.SetGlobal("provides", l.NewFunction(sc.wProvides))
|
||||
|
||||
// witch helpers
|
||||
l.SetGlobal("_handlers", l.NewTable())
|
||||
l.SetGlobal("_ID", lua.LNumber(vc.Target.ID))
|
||||
|
||||
if err := l.DoString(vc.Target.Script); err != nil {
|
||||
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")))
|
||||
owner := l.ToString(1)
|
||||
name := l.ToString(2)
|
||||
db := sc.serverAPI.DB()
|
||||
db := sc.db
|
||||
senderObj, err := db.GetObjectByID(senderID)
|
||||
if err != nil {
|
||||
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))
|
||||
l.SetGlobal("sender", senderT)
|
||||
l.SetGlobal("msg", lua.LString(vc.Rest))
|
||||
l.SetGlobal("_SENDERID", lua.LNumber(vc.Sender.ID))
|
||||
|
||||
handlers := l.GetGlobal("_handlers").(*lua.LTable)
|
||||
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) {
|
||||
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))
|
||||
}
|
||||
|
|
|
@ -173,21 +173,4 @@ allows({
|
|||
execute = "world",
|
||||
})
|
||||
|
||||
-- option 1: fully manual
|
||||
|
||||
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")
|
||||
goes("east", "gallery")
|
||||
|
|
Loading…
Reference in New Issue