the modules · from entry 2325 · bafyreif26e62m6x5ag2wozarv5wmi2gxpekqe7nzqmunvjhwzfzxdt542i
Avatar
Avatar.obend 514 lines
1edition ObjectiveBend 1
2import ./Abi.obend as Abi
3import ./List.obend as Lists
4import ./Document.obend as Document
5import ./Plan.obend as Plans
6import ./Form.obend as Form
7import ./Card.obend as Card
8import ./Relation.obend as Relations
9import ./Rows.obend as Rows
10import ./Scenes.obend as Scenes
11import ./Text.obend as Text
12import ./World.obend as World
13type Done = Scenes.Done
14# A note in the inbox: who it is from (a principal, or the object whose rule told of a change),
15# their handle when the host knew it, the line, the height its turn read, `n`, the inbox's
16# count then, and `uri`, the post it mentions the avatar in ("" for any other note). Keyed
17# {at, from, n}, so key order (at first) is the order they came; the inbox keeps 64, the host
18# dropping the oldest.
19record Note:
20 from: String
21 handle: String
22 text: String
23 at: Nat
24 n: Nat
25 uri: String
26# What another hand did to a thing this avatar's principal made (`heard`): the thing, the verb (a
27# rain, a write, a grown verb, an inspiriting, a door hung to it), who did it and their handle then,
28# the height, and `n`, the count then. Keyed {at, n}; the newest 64 are kept.
29record Heard:
30 thing: String
31 verb: String
32 by: String
33 handle: String
34 at: Nat
35 n: Nat
36# The object a verified principal gets; its id is the principal's DID string. What it is told
37# lands in `inbox`: rule notes, tide notes, and the posts that mention it; what others did to the
38# things it made lands in `heard`, while `hear` is on. `seen` is the entry its principal has read
39# through: the notes and the heard after it are the new ones.
40record State:
41 handle: String
42 # The index maintained by room presence transactions.
43 at: Plans.Reference
44 holding: Lists.List<Plans.Reference>
45 inbox: Relations.Relation<Note>
46 heard: Relations.Relation<Heard>
47 hear: Bool
48 seen: Nat
49 tongue: String
50# Only its own methods write it: a write no method made is refused (`methods`), and a rule is
51# placed on it by its principal alone (the object's id is the principal's DID). The predicate, not
52# a text law, so the host's default law (its creator alone reprograms or amends it) stands beside it.
53def law(old: State, new: State, request: Abi.Request) -> Abi.Verdict:
54 if request.kind == 3n || (request.kind == 0n && request.method == "") then Abi.Verdict.refused({clause: "methods", reading: "Only its own methods write it; a proposed write is refused."}) else if request.kind == 4n && request.context.principal != request.context.object then Abi.Verdict.refused({clause: "rules", reading: "Only its principal places a rule on it."}) else Abi.Verdict.admitted({})
55# The avatar as edition 10 keeps it (a carry lays this package over it, `migrate`): no tongue yet.
56record State10:
57 handle: String
58 at: Plans.Reference
59 holding: Lists.List<Plans.Reference>
60 inbox: Relations.Relation<Note>
61 heard: Relations.Relation<Heard>
62 hear: Bool
63 seen: Nat
64def migrate(old: State10) -> State:
65 {handle: old.handle, at: old.at, holding: old.holding, inbox: old.inbox, heard: old.heard, hear: old.hear, seen: old.seen, tongue: ""}
66def initial() -> State:
67 {handle: "", at: Plans.nobody(), holding: Lists.List.nil(), inbox: Relations.empty(), heard: Relations.empty(), hear: true, seen: 0n, tongue: ""}
68def relations() -> Relations.Decls:
69 Lists.append(Lists.append(Relations.Decls.nil(), {field: "inbox", key: Text.words("at from n"), limit: 64n}), {field: "heard", key: Text.words("at n"), limit: 64n})
70# An avatar answers only to its own principal: its id is the principal's DID.
71def mine(context: Abi.Context) -> Bool:
72 context.principal == context.object
73# Who the avatar is, as a refusal and its card name it: the town as such, else its handle, else the shortened DID.
74def owner(state: State, context: Abi.Context) -> String:
75 Card.shown(context.object, state.handle, context)
76sum Why:
77 notMine: {who: String, doing: String}
78 notAThing: {doing: String}
79 noAnswer: {from: String}
80 alreadySeen: {mark: Nat}
81 noExit: {label: String}
82 roomSilent: {}
83 nowhere: {}
84 notTold: {}
85 notTown: {}
86def refusal(w: Why) -> Card.Refusal:
87 match w:
88 case notMine(n): {clause: "notMine", reading: "Only {n.who} {n.doing}."}
89 case notAThing(n): {clause: "notAThing", reading: "Only a thing can be {n.doing}."}
90 case noAnswer(n): {clause: "noAnswer", reading: "No answer from {n.from}"}
91 case alreadySeen(s): {clause: "alreadySeen", reading: "Already read through entry {natText(s.mark)}."}
92 case noExit(n): {clause: "noExit", reading: "There is no exit named {n.label}"}
93 case roomSilent(_): {clause: "roomSilent", reading: "The room did not answer."}
94 case nowhere(_): {clause: "nowhere", reading: "Nowhere yet; go to a room first: delvetalk me go / to: <room>."}
95 case notTold(_): {clause: "heard", reading: "Only a thing tells its maker what another hand did to it."}
96 case notTown(_): {clause: "notTown", reading: "Only the town tells an avatar the posts that mentioned it."}
97def refused(w: Why) -> Done:
98 Done.refused(refusal(w))
99# A refusal, answered by name (Card.refusing), and nothing written.
100def told(w: Why, context: Abi.Context) -> Activity<Done>:
101 Card.refusing(refused(w), refusal(w), context)
102def answerRefused(refusal: Card.Refusal, context: Abi.Context) -> Activity<Card.Replied>:
103 Card.refusing(Card.Replied.refused(refusal), refusal, context)
104# --- The inbox ---------------------------------------------------------------------
105# A note is anyone's line, or a fired rule's: a rule's spell `delvetalk me note / text: ...` runs
106# under the rule's owner on the owner's avatar. A note someone sends by spell is answered; one an
107# object started (a rule, a tide's tick) is not.
108def note(state: State, input: {text: String}, context: Abi.Context) -> Activity<Done>:
109 match noted({from: context.principal, handle: context.handle, text: input.text, at: context.height, n: Relations.count(state.inbox), uri: ""}):
110 case done(_): if context.caller == "" then Card.tell(Done.done({}), Document.text("Noted on {owner(state, context)}'s avatar.\n")) else Done.done({})
111 case _: Done.done({})
112def noted(note: Note) -> Activity<Done>:
113 let written(_) = write {inbox: insert note}
114 Done.done({})
115# A post that mentions the avatar's resident, addressed to it as the author's turn: a note from
116# the author, the post its `uri`, and no answer (no offer).
117def mentioned(state: State, input: Card.Reply, context: Abi.Context) -> Activity<Card.Replied>:
118 let written(_) = write {inbox: insert {from: context.principal, handle: context.handle, text: textTake(input.text, 280n), at: context.height, n: Relations.count(state.inbox), uri: input.post}}
119 Card.Replied.silent({})
120# The posts elsewhere that mentioned its resident, told by the town (the bridge's notes of the hour, a turn of the
121# town's for each resident it heard of): each a note from the post's author under their handle, its `uri` the post,
122# the first 280 characters kept. Only the town tells them (`context.town`); anyone else's are refused, nothing written.
123record Mention:
124 from: String
125 handle: String
126 text: String
127 post: String
128def mentions(state: State, input: {notes: Lists.List<Mention>}, context: Abi.Context) -> Activity<Done>:
129 if context.town == "" || context.principal != context.town || context.caller != "" then told(Why.notTown({}), context) else telling(input.notes, Relations.count(state.inbox), context)
130def telling(notes: Lists.List<Mention>, n: Nat, context: Abi.Context) -> Activity<Done>:
131 match notes:
132 case nil(_): Done.done({})
133 case cons(c): tellingOne(c.head, c.tail, n, context)
134def tellingOne(note: Mention, rest: Lists.List<Mention>, n: Nat, context: Abi.Context) -> Activity<Done>:
135 let written(_) = write {inbox: insert {from: note.from, handle: note.handle, text: textTake(note.text, 280n), at: context.height, n: n, uri: note.post}}
136 telling(rest, n + 1n, context)
137# A thing tells its maker what another hand did to it, by a send run as that hand: a line written on a
138# record (a write, a rain, a pour, a grown verb), a grown verb used or an inspiriting on a THING, a door
139# a record hangs to it. Only a thing tells (the turn's sender), and only of a thing this avatar's
140# principal made (a record's `maker`, else its `owner`); a hand's `heard` is refused by name. With
141# `hear` off it is taken as nothing. The sending object is the thing the notice names.
142def heard(state: State, input: {thing: String, verb: String}, context: Abi.Context) -> Activity<Done>:
143 if context.sender == "" || context.sender != input.thing then told(Why.notTold({}), context) else if state.hear == false || context.principal == context.object then Done.done({}) else making(state, input, context)
144def making(state: State, input: {thing: String, verb: String}, context: Abi.Context) -> Activity<Done>:
145 match world.viewField::<String>({object: {world: context.world, object: input.thing}, field: "maker"}):
146 case viewed(v): if v.state == "" then owning(state, input, context) else madeBy(v.state, state, input, context)
147 case _: owning(state, input, context)
148def owning(state: State, input: {thing: String, verb: String}, context: Abi.Context) -> Activity<Done>:
149 match world.viewField::<String>({object: {world: context.world, object: input.thing}, field: "owner"}):
150 case viewed(v): madeBy(v.state, state, input, context)
151 case _: Done.done({})
152def madeBy(maker: String, state: State, input: {thing: String, verb: String}, context: Abi.Context) -> Activity<Done>:
153 if maker != context.object then Done.done({}) else hearing(state, input, context)
154def hearing(state: State, input: {thing: String, verb: String}, context: Abi.Context) -> Activity<Done>:
155 let written(_) = write {heard: insert {thing: input.thing, verb: input.verb, by: context.principal, handle: context.handle, at: context.height, n: Relations.count(state.heard)}}
156 Done.done({})
157# --- Reading -----------------------------------------------------------------------
158# Its principal marks the entry it has read through; the notes after it are the new ones.
159def seen(state: State, input: {at: Nat}, context: Abi.Context) -> Activity<Done>:
160 if mine(context) == false then told(Why.notMine({who: owner(state, context), doing: "marks it read"}), context) else if input.at < state.seen then told(Why.alreadySeen({mark: state.seen}), context) else marked(state, input.at, context)
161def marked(state: State, at: Nat, context: Abi.Context) -> Activity<Done>:
162 let written(_) = write {seen: set at}
163 Card.tell(Done.done({}), Document.Document.sequence({items: Document.Documents.cons({head: Document.text("Read through entry {natText(at)}; nothing before it is new again.\n"), tail: Document.Documents.cons({head: render(extend(state, {seen: at}), context), tail: Document.Documents.nil()})})}))
164# The card, asked for: since you were here, then what it holds and the new notes. Writes nothing.
165def since(state: State, context: Abi.Context) -> Activity<Done>:
166 Card.tell(Done.done({}), render(state, context))
167# --- Holding -------------------------------------------------------------------------
168# Things call hold and release on their holder's avatar. Both are idempotent and never
169# abort a thing's turn, so a thing that has already staged its own writes cannot be left half
170# done. The thing is the calling object. Only the avatar's own principal's turn holds (a
171# pick-up is the taker's, an accepted offer the accepter's): another's thing is answered
172# notMine and recorded nowhere.
173def thing(context: Abi.Context) -> Plans.Reference:
174 {world: context.world, object: context.caller}
175def hold(state: State, context: Abi.Context) -> Activity<Done>:
176 if context.caller == "" then refused(Why.notAThing({doing: "held"})) else if mine(context) == false then refused(Why.notMine({who: owner(state, context), doing: "takes what it holds"})) else if Lists.any(state.holding, fn(h: Plans.Reference) -> Bool: Plans.same(h, thing(context))) then Done.done({}) else held(context)
177def held(context: Abi.Context) -> Activity<Done>:
178 let written(_) = write {holding: append thing(context)}
179 Done.done({})
180def release(state: State, context: Abi.Context) -> Activity<Done>:
181 if context.caller == "" then refused(Why.notAThing({doing: "released"})) else if Lists.any(state.holding, fn(h: Plans.Reference) -> Bool: Plans.same(h, thing(context))) then released(context) else Done.done({})
182def released(context: Abi.Context) -> Activity<Done>:
183 let written(_) = write {holding: removeItem thing(context)}
184 Done.done({})
185# Accept what someone offered: the thing's transfer (judged by its own law) moves custody to
186# this avatar, and this avatar records it in its own holding, in the same turn.
187def accept(state: State, input: {thing: String}, context: Abi.Context) -> Activity<Done>:
188 if mine(context) == false then told(Why.notMine({who: owner(state, context), doing: "accepts for it"}), context) else accepting({world: context.world, object: input.thing}, context)
189def accepting(thing: Plans.Reference, context: Abi.Context) -> Activity<Done>:
190 match world.call::<Done>({object: thing, method: "transfer", argument: Plans.nothing()}):
191 case returned(r): accepted(r.result, thing, context)
192 case _: told(Why.noAnswer({from: thing.object}), context)
193def accepted(result: Done, thing: Plans.Reference, context: Abi.Context) -> Activity<Done>:
194 match result:
195 case refused(r): Card.refusing(result, r, context)
196 case _: recorded(thing)
197def recorded(thing: Plans.Reference) -> Activity<Done>:
198 let written(_) = write {holding: append thing}
199 Card.tell(Done.done({}), Document.text("Accepted {thing.object}; you hold it.\n"))
200# --- Moving --------------------------------------------------------------------------
201sum Moved:
202 moved: {to: Plans.Reference}
203 refused: Card.Refusal
204def moveRefused(w: Why, context: Abi.Context) -> Activity<Moved>:
205 movedRefused(refusal(w), context)
206def movedRefused(refusal: Card.Refusal, context: Abi.Context) -> Activity<Moved>:
207 Card.refusing(Moved.refused(refusal), refusal, context)
208# Room presence is authoritative. `at` indexes it for the card and scoped commands; a room's
209# entry or departure calls `locate` in the same turn. The room answers `present` from its staged
210# state, so the index includes an entry or END that has not committed yet.
211def locate(state: State, context: Abi.Context) -> Activity<Bool>:
212 if mine(context) == false || context.caller == "" then refuse("refused location: Only a room in its resident's turn indexes presence.") else roomLocated(state, {world: context.world, object: context.caller}, context)
213def roomLocated(state: State, room: Plans.Reference, context: Abi.Context) -> Activity<Bool>:
214 match world.viewField::<Relations.Relation<Rows.Presence>>({object: room, field: "presence"}):
215 case viewed(_): presenceLocated(state, room, context)
216 case denied(_): refuse("refused location: The calling room has no readable presence.")
217 case refused(_): refuse("refused location: The calling room has no readable presence.")
218def presenceLocated(state: State, room: Plans.Reference, context: Abi.Context) -> Activity<Bool>:
219 match world.call::<Bool>({object: room, method: "present", argument: Plans.nothing()}):
220 case returned(r): if r.result then relocating(state.at, room, context) else if Plans.same(state.at, room) then located(Plans.nobody()) else true
221 case denied(_): refuse("refused location: The calling room did not answer its presence.")
222 case refused(_): refuse("refused location: The calling room did not answer its presence.")
223def relocating(from: Plans.Reference, to: Plans.Reference, context: Abi.Context) -> Activity<Bool>:
224 if Plans.isNobody(from) || Plans.same(from, to) then located(to) else departingFrom(from, to, context)
225def departingFrom(from: Plans.Reference, to: Plans.Reference, context: Abi.Context) -> Activity<Bool>:
226 match world.call::<Bool>({object: from, method: "present", argument: Plans.nothing()}):
227 case returned(r): if r.result then leavingFor(from, to, context) else located(to)
228 case denied(_): refuse("refused location: The previous room did not answer its presence.")
229 case refused(_): refuse("refused location: The previous room did not answer its presence.")
230def leavingFor(from: Plans.Reference, to: Plans.Reference, context: Abi.Context) -> Activity<Bool>:
231 match world.call::<Done>({object: from, method: "leave", argument: Plans.nothing()}):
232 case returned(r): locationLeft(r.result, to)
233 case refused(r): refuse("refused {r.clause}: {r.reading}")
234 case denied(_): refuse("refused location: The previous room did not admit departure.")
235def locationLeft(result: Done, to: Plans.Reference) -> Activity<Bool>:
236 match result:
237 case refused(r): refuse("refused {r.clause}: {r.reading}")
238 case done(_): located(to)
239 case entered(_): located(to)
240 case moved(_): located(to)
241 case left(_): located(to)
242def located(at: Plans.Reference) -> Activity<Bool>:
243 let written(_) = write {at: set at}
244 true
245# Walk through a door of the room it is in: read its doors (`cites`) and enter the room the
246# door cites. Entry performs departure and indexes presence in the same transaction.
247def move(state: State, input: {exit: String}, context: Abi.Context) -> Activity<Moved>:
248 if mine(context) == false then moveRefused(Why.notMine({who: owner(state, context), doing: "moves it"}), context) else if Plans.isNobody(state.at) then moveRefused(Why.nowhere({}), context) else looked(input.exit, state, context)
249def looked(label: String, state: State, context: Abi.Context) -> Activity<Moved>:
250 match world.viewField::<Card.Doorways>({object: state.at, field: "cites"}):
251 case viewed(v): through(label, Lists.find(v.state, fn(e: Card.Doorway) -> Bool: e.label == label), context)
252 case denied(_): moveRefused(Why.noExit({label: label}), context)
253 case refused(_): moveRefused(Why.noExit({label: label}), context)
254def through(label: String, found: Lists.Maybe<Card.Doorway>, context: Abi.Context) -> Activity<Moved>:
255 match found:
256 case none(_): moveRefused(Why.noExit({label: label}), context)
257 case some(s): entering(s.value.to, context)
258def entering(to: Plans.Reference, context: Abi.Context) -> Activity<Moved>:
259 match world.call::<Done>({object: to, method: "enter", argument: Plans.nothing()}):
260 case returned(r): entered(r.result, to, context)
261 case denied(_): moveRefused(Why.roomSilent({}), context)
262 case refused(_): moveRefused(Why.roomSilent({}), context)
263def entered(result: Done, to: Plans.Reference, context: Abi.Context) -> Activity<Moved>:
264 match result:
265 case refused(r): movedRefused(r, context)
266 case done(_): Moved.moved({to: to})
267 case entered(_): Moved.moved({to: to})
268 case moved(_): Moved.moved({to: to})
269 case left(_): Moved.moved({to: to})
270# Go to a room by its id, from anywhere, through that room's entry transaction.
271def go(state: State, input: {to: String}, context: Abi.Context) -> Activity<Moved>:
272 if mine(context) == false then moveRefused(Why.notMine({who: owner(state, context), doing: "moves it"}), context) else entering({world: context.world, object: input.to}, context)
273# --- The card ------------------------------------------------------------------------
274form move:
275 exit: text 1..64
276form go:
277 to: text 1..160
278form note:
279 text: text 1..280
280form accept:
281 thing: text 1..160
282form seen:
283 at: natural 0..1000000000
284form since:
285# Things call hold and release; rooms index presence with locate.
286def methods() -> Lists.List<String>:
287 Text.words("hold release ~heard ~locate ~mentions")
288# Its principal's lenses: the handle the card shows (`delvetalk me set / handle: ...`), and whether
289# it hears what others do to the things they made (`delvetalk me set / hear: no`), and the language
290# its principal states (`delvetalk me set / tongue: ja`), one of the town's tongues, which it answers in.
291def lenses() -> Lists.List<Form.Field>:
292 Lists.append(Lists.append(Lists.append(Lists.List.nil(), {name: "handle", kind: Form.Kind.text({min: 1n, max: 64n})}), {name: "hear", kind: Form.Kind.choice({options: Lists.append(Lists.append(Form.Names.nil(), "yes"), "no")})}), {name: "tongue", kind: Form.Kind.choice({options: tongues()})})
293# The tongues the town paints, checks and speaks hob's lines in (spec/Delvetalk/Host/Tongue.lean `tongues`).
294def tongues() -> Form.Names:
295 Lists.append(Lists.append(Lists.append(Lists.append(Lists.append(Lists.append(Form.Names.nil(), "en"), "ja"), "zh"), "es"), "ru"), "ar")
296def set(state: State, input: {field: String, value: Form.Value}, context: Abi.Context) -> Activity<Done>:
297 if mine(context) == false then told(Why.notMine({who: owner(state, context), doing: "changes it"}), context) else if input.field == "hear" then setHear(Card.valueText(input.value) == "yes") else if input.field == "tongue" then setTongue(Card.valueText(input.value)) else setHandle(Card.valueText(input.value))
298def setHandle(handle: String) -> Activity<Done>:
299 let written(_) = write {handle: set handle}
300 Card.tell(Done.done({}), Document.text("Set handle to {handle}.\n"))
301def setTongue(tongue: String) -> Activity<Done>:
302 let written(_) = write {tongue: set tongue}
303 Card.tell(Done.done({}), Document.text("Set tongue to {tongue}.\n"))
304def setHear(hear: Bool) -> Activity<Done>:
305 let written(_) = write {hear: set hear}
306 Card.tell(Done.done({}), Document.text(if hear then "Hearing again what others do to what you made.\n" else "No longer hearing what others do to what you made: delvetalk me set / hear: yes\n"))
307# Scoped resolution: the avatar's own principal's prose with no spell that starts with an
308# action word (`acquire the stone`, `bump counter`, `look`) names its object by the first word
309# that is a thing lying in the room the avatar last entered (by its name, then by its id's last
310# segment), else an object of that name (garden, rooms); the avatar sends the object the matching
311# form as a spell, the rest of the line filling its first field. Several things of that name are
312# answered with their ids; `look` shows the room as its reader sees it.
313# Prose from anyone else is a mention: a note in the inbox, answered with nothing. Anything else is
314# the card default.
315def receive(state: State, input: Card.Reply, context: Abi.Context) -> Activity<Card.Replied>:
316 if Card.quietly(Card.unanswered(input, forms())) then (if mine(context) then scoped(input, Text.words(firstLine(input.text)), context, state) else mentioned(state, input, context)) else answered(input, context, state)
317def answered(input: Card.Reply, context: Abi.Context, state: State) -> Activity<Card.Replied>:
318 Card.answer(input, context, forms(), render(state, context))
319def firstLine(text: String) -> String:
320 textTake(text, textBreak(text, "\n"))
321def bare(word: String) -> String:
322 Card.stripped(word, textLength(word))
323def scoped(input: Card.Reply, words: Lists.List<String>, context: Abi.Context, state: State) -> Activity<Card.Replied>:
324 match words:
325 case nil(_): answered(input, context, state)
326 case cons(c): if bare(c.head) == "look" then looking(state, context) else placing(input, bare(c.head), c.tail, context, state)
327def looking(state: State, context: Abi.Context) -> Activity<Card.Replied>:
328 match world.card({object: state.at}):
329 case carded(c): Card.tell(Card.Replied.done({action: "look"}), c.document)
330 case _: answerRefused(refusal(Why.nowhere({})), context)
331record Named:
332 object: Plans.Reference
333 name: String
334record Asked:
335 input: Card.Reply
336 action: String
337 rest: Lists.List<String>
338def placing(input: Card.Reply, action: String, rest: Lists.List<String>, context: Abi.Context, state: State) -> Activity<Card.Replied>:
339 match world.viewField::<Relations.Relation<Plans.Reference>>({object: state.at, field: "things"}):
340 case viewed(v): naming({input: input, action: action, rest: rest}, Relations.rows(v.state), Lists.List.nil(), context, state)
341 case _: doors({input: input, action: action, rest: rest}, rest, context, state)
342# Each thing lying here by the first line of its card (its name).
343def naming(asked: Asked, things: Lists.List<Plans.Reference>, named: Lists.List<Named>, context: Abi.Context, state: State) -> Activity<Card.Replied>:
344 match things:
345 case nil(_): nouns(asked, asked.rest, named, context, state)
346 case cons(c): carding(asked, c.head, c.tail, named, context, state)
347def carding(asked: Asked, thing: Plans.Reference, things: Lists.List<Plans.Reference>, named: Lists.List<Named>, context: Abi.Context, state: State) -> Activity<Card.Replied>:
348 match world.card({object: thing}):
349 case carded(c): naming(asked, things, Lists.List.cons({head: {object: thing, name: firstLine(Document.plain(c.document))}, tail: named}), context, state)
350 case _: naming(asked, things, Lists.List.cons({head: {object: thing, name: ""}, tail: named}), context, state)
351def suffix(id: String, word: String) -> Bool:
352 id == word || textLength(id) > textLength(word) && textDrop(id, textLength(id) - textLength(word) - 1n) == "/{word}"
353def matching(named: Lists.List<Named>, word: String) -> Lists.List<Named>:
354 if Lists.any(named, fn(n: Named) -> Bool: n.name == word) then Lists.filter(named, fn(n: Named) -> Bool: n.name == word) else Lists.filter(named, fn(n: Named) -> Bool: suffix(n.object.object, word))
355# The first word naming a thing here decides; the words after it fill the form.
356def nouns(asked: Asked, words: Lists.List<String>, named: Lists.List<Named>, context: Abi.Context, state: State) -> Activity<Card.Replied>:
357 match words:
358 case nil(_): doors(asked, asked.rest, context, state)
359 case cons(c): chosen(asked, c.tail, matching(named, bare(c.head)), named, context, state)
360def chosen(asked: Asked, after: Lists.List<String>, found: Lists.List<Named>, named: Lists.List<Named>, context: Abi.Context, state: State) -> Activity<Card.Replied>:
361 match found:
362 case nil(_): nouns(asked, after, named, context, state)
363 case cons(c): if Lists.length(c.tail) > 0n then answerRefused({clause: "ambiguous", reading: "which one: {textJoin(Lists.map(found, fn(n: Named) -> String: n.object.object), \", \")}?"}, context) else acting(asked, c.head.object, after, context, state)
364# Else an object named by a word (the commonplace's doors are named by their ids).
365def doors(asked: Asked, words: Lists.List<String>, context: Abi.Context, state: State) -> Activity<Card.Replied>:
366 match words:
367 case nil(_): answered(asked.input, context, state)
368 case cons(c): doorNamed(asked, {world: context.world, object: bare(c.head)}, c.tail, context, state)
369def doorNamed(asked: Asked, door: Plans.Reference, after: Lists.List<String>, context: Abi.Context, state: State) -> Activity<Card.Replied>:
370 match world.inspect({object: door, source: false}):
371 case inspected(i): if Lists.any(i.methods, fn(f: Form.Form) -> Bool: f.action == asked.action) then sending(asked, door, after, i.methods) else doors(asked, after, context, state)
372 case _: doors(asked, after, context, state)
373def acting(asked: Asked, object: Plans.Reference, after: Lists.List<String>, context: Abi.Context, state: State) -> Activity<Card.Replied>:
374 match world.inspect({object: object, source: false}):
375 case inspected(i): if Lists.any(i.methods, fn(f: Form.Form) -> Bool: f.action == asked.action) then sending(asked, object, after, i.methods) else answerRefused({clause: "noAction", reading: "{object.object} does not take {asked.action}; delvetalk {object.object} ?"}, context)
376 case _: answered(asked.input, context, state)
377# The form as a spell sent to the object's receive, under the speaker; its card answers.
378def sending(asked: Asked, object: Plans.Reference, after: Lists.List<String>, forms: Lists.List<Form.Form>) -> Activity<Card.Replied>:
379 match world.send({object: object, method: "receive", argument: {text: spellFor(asked.action, object, textJoin(after, " "), forms), post: asked.input.post}}):
380 case _: Card.Replied.done({action: asked.action})
381def spellFor(action: String, object: Plans.Reference, rest: String, forms: Lists.List<Form.Form>) -> String:
382 match Lists.find(forms, fn(f: Form.Form) -> Bool: f.action == action):
383 case some(f): "delvetalk {object.object} {action}{fieldLine(f.value.fields, rest)}"
384 case none(_): "delvetalk {object.object} {action}"
385def fieldLine(fields: Form.Fields, rest: String) -> String:
386 match fields:
387 case cons(c): if rest == "" then "" else "\n{c.head.name}: {rest}"
388 case nil(_): ""
389def holdingLine(item: Plans.Reference) -> Document.Document:
390 Document.text("Holding: {item.object}\n")
391# `#<entry> <who>: <the first line, cut at 100>`
392def noteLine(item: Note, context: Abi.Context) -> Document.Document:
393 Document.text("#{natText(item.at)} {Card.shown(item.from, item.handle, context)}: {textTake(firstLine(item.text), 100n)}\n")
394def fresh(state: State) -> Lists.List<Note>:
395 Lists.reverse(Lists.filter(Relations.rows(state.inbox), fn(n: Note) -> Bool: n.at > state.seen))
396# To its principal the card leads with what others did to what they made since they were here, one
397# line a thing, newest first; then where it is, what it holds and the new notes. Eight of each list,
398# then a count; one budget across the card (Card.limit(context)): each list gets what the ones before
399# it leave.
400def render(state: State, context: Abi.Context) -> Document.Document:
401 let since = sinceFor(state, context)
402 let head = Document.text("{owner(state, context)}'s avatar, {whereLine(state, context)}.\n{bookLine(state, context)}")
403 let held = Card.clippedWithin(Lists.map(state.holding, holdingLine), 8n, Card.left(context, sized(since) + Document.size(head) + 200n))
404 let notes = inboxFor(state, context, sized(since) + Document.size(head) + sized(held))
405 Document.Document.sequence({items: Lists.concat(Lists.concat(since, Document.Documents.cons({head: head, tail: held})), notes)})
406# --- Since you were here ------------------------------------------------------------
407def freshHeard(state: State) -> Lists.List<Heard>:
408 Lists.reverse(Lists.filter(Relations.rows(state.heard), fn(h: Heard) -> Bool: h.at > state.seen))
409# Each value once, in the order it first stands.
410def distinct(items: Lists.List<String>) -> Lists.List<String>:
411 Lists.fold(items, Lists.List.nil(), fn(x: String) -> Lists.List<String> -> Lists.List<String>: fn(rest: Lists.List<String>) -> Lists.List<String>: Lists.List.cons({head: x, tail: Lists.filter(rest, fn(y: String) -> Bool: y != x)}))
412def sinceFor(state: State, context: Abi.Context) -> Document.Documents:
413 if Card.reads(context.object, context) == false then Document.Documents.nil() else if state.hear == false then Document.Documents.cons({head: Document.text("Not hearing what others do to what you made: delvetalk me set / hear: yes\n"), tail: Document.Documents.nil()}) else heardFor(state, freshHeard(state), context)
414def heardFor(state: State, news: Lists.List<Heard>, context: Abi.Context) -> Document.Documents:
415 if Lists.length(news) == 0n then Document.Documents.nil() else sinceLines(state, news, context)
416def sinceLines(state: State, news: Lists.List<Heard>, context: Abi.Context) -> Document.Documents:
417 let top = Document.text("Since you were here (entry {natText(state.seen)}):\n")
418 let lines = Lists.map(distinct(Lists.map(news, fn(h: Heard) -> String: h.thing)), fn(thing: String) -> Document.Document: thingLine(thing, Lists.filter(news, fn(h: Heard) -> Bool: h.thing == thing)))
419 Document.Documents.cons({head: top, tail: Card.clippedWithin(lines, 8n, Card.left(context, Document.size(top) + 400n))})
420# ` bell/1: 3 rains from kestrel.delve.town and 1 other; 1 door from yue.delve.town`
421def thingLine(thing: String, rows: Lists.List<Heard>) -> Document.Document:
422 let parts = Lists.map(distinct(Lists.map(rows, fn(h: Heard) -> String: h.verb)), fn(verb: String) -> String: verbPart(verb, Lists.filter(rows, fn(h: Heard) -> Bool: h.verb == verb)))
423 let said = textJoin(parts, "; ")
424 Document.text(" {thing}: {said}\n")
425def verbPart(verb: String, rows: Lists.List<Heard>) -> String:
426 let count = Lists.length(rows)
427 let others = Lists.length(distinct(Lists.map(rows, fn(h: Heard) -> String: h.by))) - 1n
428 let rest = if others == 0n then "" else if others == 1n then " and 1 other" else " and {natText(others)} others"
429 let plural = if count == 1n then "" else "s"
430 "{natText(count)} {verb}{plural} from {handOf(rows)}{rest}"
431def handOf(rows: Lists.List<Heard>) -> String:
432 match rows:
433 case cons(c): if c.head.handle == "" then Card.handle(c.head.by) else c.head.handle
434 case nil(_): ""
435# Where it is: what it last entered; nowhere yet, its principal is shown the spell that goes.
436def whereLine(state: State, context: Abi.Context) -> String:
437 if Plans.isNobody(state.at) == false then "in {state.at.object}" else if Card.reads(context.object, context) then "nowhere yet; go: delvetalk me go / to: <a room>" else "nowhere yet"
438# The one line that opens its book (`book`): its principal's by `me`, anyone else's by its handle.
439# The spell that reads its principal's book, by the handle a spell names it by: none for the town, which no spell names.
440def bookLine(state: State, context: Abi.Context) -> String:
441 if Card.reads(context.object, context) then "Its book: delvetalk me book\n" else if state.handle == "" || Card.town(context.object, context) then "" else "Its book: delvetalk {state.handle} book\n"
442def sized(lines: Document.Documents) -> Nat:
443 Document.size(Document.Document.sequence({items: lines}))
444# The notes are the avatar's principal's alone: the new ones after its mark, newest first, and
445# the spell that moves the mark. Anyone else sees how many there are.
446# The newest entry among the new notes and the new heard (0 when there are none): what `seen` moves to.
447def latest(state: State) -> Nat:
448 Lists.fold(Lists.concat(Lists.map(fresh(state), fn(n: Note) -> Nat: n.at), Lists.map(freshHeard(state), fn(h: Heard) -> Nat: h.at)), 0n, fn(at: Nat) -> Nat -> Nat: fn(most: Nat) -> Nat: if at > most then at else most)
449def inboxFor(state: State, context: Abi.Context, used: Nat) -> Document.Documents:
450 if Card.reads(context.object, context) then newFor(state, fresh(state), latest(state), context, used) else Document.Documents.cons({head: Document.text(if Relations.count(state.inbox) == 1n then "1 note\n" else "{natText(Relations.count(state.inbox))} notes\n"), tail: Document.Documents.nil()})
451# The new notes, then the spell that marks them and the heard read.
452def newFor(state: State, news: Lists.List<Note>, at: Nat, context: Abi.Context, used: Nat) -> Document.Documents:
453 let spell = Document.text(" delvetalk me seen / at: {natText(at)}\n")
454 let marked = if at == 0n then Document.Documents.nil() else Document.Documents.cons({head: spell, tail: Document.Documents.nil()})
455 match news:
456 case nil(_): Document.Documents.cons({head: Document.text("No new notes since entry {natText(state.seen)}.\n"), tail: marked})
457 case cons(_): newNotes(state, news, marked, context, used + Document.size(spell))
458def newNotes(state: State, news: Lists.List<Note>, marked: Document.Documents, context: Abi.Context, used: Nat) -> Document.Documents:
459 let top = Document.text("{natText(Lists.length(news))} new since entry {natText(state.seen)}:\n")
460 Document.Documents.cons({head: top, tail: Lists.concat(Card.clippedWithin(Lists.map(news, fn(n: Note) -> Document.Document: noteLine(n, context)), 8n, Card.left(context, used + Document.size(top))), marked)})
461# --- The book --------------------------------------------------------------------------
462# Its principal's commonplace book (docs/VOICE.md "The book, the history, the map"): what they made, each with
463# what others did to it (`heard`: rains, doors, uses, by whom), the verbs they grew and on what, the rules and
464# wants they placed, the lines they wrote and where, the places they visited; counts and hands, never a score.
465# The host hands it what the store shows of them as the reader may view it (`World.Deeds`, Surfaces.deeds) for
466# `delvetalk me book`, `delvetalk <handle> book` and the page /o/<handle>/book, and writes nothing: it is read,
467# a page within a reply (Card.room), the next page's spell under it.
468def book(state: State, context: Abi.Context, deeds: World.Deeds) -> Document.Document:
469 let who = owner(state, context)
470 let spell = if Card.reads(context.object, context) then "delvetalk me book" else "delvetalk {who} book"
471 let lines = bookLines(state, deeds.rows)
472 let body = if Lists.length(lines) == 0n then Lists.List.cons({head: "Nothing in it yet. The doors: delvetalk map\n", tail: Lists.List.nil()}) else lines
473 Card.pageOf(Card.paged(Lists.List.cons({head: "{who}'s commonplace book.\n", tail: body}), bookRoom(context)), deeds.page, spell)
474# A page's room: a reply's characters less the line under it.
475def bookRoom(context: Abi.Context) -> Nat:
476 Card.room(context) - 160n
477def deedsOf(rows: Lists.List<World.Deed>, what: String) -> Lists.List<World.Deed>:
478 Lists.filter(rows, fn(d: World.Deed) -> Bool: d.what == what)
479def bookLines(state: State, rows: Lists.List<World.Deed>) -> Lists.List<String>:
480 let made = deedsOf(rows, "made")
481 let heard = Lists.filter(Relations.rows(state.heard), fn(h: Heard) -> Bool: Lists.any(made, fn(d: World.Deed) -> Bool: d.object == h.thing))
482 Lists.concat(Lists.concat(Lists.concat(madeLines(made, heard), grownLines(deedsOf(rows, "grew"))), Lists.concat(placedLines(Lists.filter(rows, fn(d: World.Deed) -> Bool: d.what == "rule" || d.what == "want")), visitedLines(deedsOf(rows, "visited")))), wroteLines(Lists.reverse(deedsOf(rows, "wrote"))))
483def entryOf(at: Nat) -> String:
484 if at == 0n then "" else ", entry {natText(at)}"
485# `Made 3; 4 other hands acted on 2 of them:` then each thing, what it is, when, and what others did to it.
486def madeLines(made: Lists.List<World.Deed>, heard: Lists.List<Heard>) -> Lists.List<String>:
487 let touched = Lists.length(Lists.filter(made, fn(d: World.Deed) -> Bool: Lists.any(heard, fn(h: Heard) -> Bool: h.thing == d.object)))
488 let hands = Lists.length(distinct(Lists.map(heard, fn(h: Heard) -> String: h.by)))
489 let others = if hands == 1n then "1 other hand" else "{natText(hands)} other hands"
490 let head = if hands == 0n then "Made {natText(Lists.length(made))}:\n" else "Made {natText(Lists.length(made))}; {others} acted on {natText(touched)} of them:\n"
491 if Lists.length(made) == 0n then Lists.List.nil() else Lists.List.cons({head: head, tail: Lists.map(made, fn(d: World.Deed) -> String: madeLine(d, Lists.filter(heard, fn(h: Heard) -> Bool: h.thing == d.object)))})
492def madeLine(d: World.Deed, rows: Lists.List<Heard>) -> String:
493 let parts = textJoin(Lists.map(distinct(Lists.map(rows, fn(h: Heard) -> String: h.verb)), fn(verb: String) -> String: verbPart(verb, Lists.filter(rows, fn(h: Heard) -> Bool: h.verb == verb))), "; ")
494 let done = if parts == "" then "" else ": {parts}"
495 let coined = if d.text == "" then "" else ", coined by {d.text}"
496 " {d.object}, a {d.name}{coined}{entryOf(d.at)}{done}\n"
497def grownLines(grew: Lists.List<World.Deed>) -> Lists.List<String>:
498 if Lists.length(grew) == 0n then Lists.List.nil() else Lists.List.cons({head: "Grew {natText(Lists.length(grew))}:\n", tail: Lists.map(grew, fn(d: World.Deed) -> String: " {d.name} on {d.object}{entryOf(d.at)}{reading(d.text)}\n")})
499def placedLines(placed: Lists.List<World.Deed>) -> Lists.List<String>:
500 if Lists.length(placed) == 0n then Lists.List.nil() else Lists.List.cons({head: "Placed {natText(Lists.length(placed))}:\n", tail: Lists.map(placed, fn(d: World.Deed) -> String: " {d.what} {d.name} on {d.object}{entryOf(d.at)}{reading(d.text)}\n")})
501# A verb's, rule's or want's reading after its name, quoted, when it has one.
502def reading(text: String) -> String:
503 if text == "" then "" else ": “{Card.cut(text, 80n)}”"
504# The lines, newest first, each where it was written and its words cut to 100.
505def wroteLines(wrote: Lists.List<World.Deed>) -> Lists.List<String>:
506 let count = if Lists.length(wrote) == 1n then "1 line" else "{natText(Lists.length(wrote))} lines"
507 if Lists.length(wrote) == 0n then Lists.List.nil() else Lists.List.cons({head: "Wrote {count}, newest first:\n", tail: Lists.map(wrote, fn(d: World.Deed) -> String: " on {d.object}{entryOf(d.at)}{verbNote(d.name)}: “{Card.cut(firstLine(d.text), 100n)}”\n")})
508def verbNote(verb: String) -> String:
509 if verb == "" || verb == "write" then "" else ", a {verb}"
510def visitedLines(visited: Lists.List<World.Deed>) -> Lists.List<String>:
511 let places = textJoin(Lists.map(visited, placeWord), ", ")
512 if Lists.length(visited) == 0n then Lists.List.nil() else Lists.List.cons({head: "Visited: {places}.\n", tail: Lists.List.nil()})
513def placeWord(d: World.Deed) -> String:
514 if d.name == "here" then "{d.object} (here)" else d.object