A rumor that spreads
A villager sees something at midnight. By the fifth day, half the village believes a worse story, and the truth reaches only those who were there.
| Villager | Day 1 | Day 2 | Day 3 | Day 4 | Day 5 |
|---|---|---|---|---|---|
| Tamthe miller’s apprentice | took1.00 | took1.00 | took1.00 | took1.00 | took1.00 |
| Bessthe baker | · | stole0.90 | stole0.90 | stole0.90 | dropped |
| Wilthe smith, an elder | · | took0.60 | took0.60 | took0.60 | took0.60 |
| Idathe innkeeper | · | · | sold0.81 | sold0.81 | sold0.81 |
| Rookthe ferryman | · | · | · | sold0.77 | sold0.77 |
| Marenthe herbalist, an elder | · | · | stole0.45 | stole0.45 | dropped |
| Oswinthe reeve | · | · | · | · | · |
Every cell is an answer from the engine: what whoHolds returns for each version of the story, on each day, from the program below.
The idea
Gossip has two halves. Who talks to whom, how much they trust each other and how a story bends in the telling are rules of your game. Who heard what, from whom and since when is a record. Keep the rules in your code and the record in Umwelt, and every question about the rumor has an exact answer.
Your game decides
- who meets whom each day
- how likely a story is to grow in the telling
- how confidence falls with trust
- that nobody gossips to the reeve’s face
- who drops the story when they see the loft
Umwelt records
- each telling, as a Holding
told-bythe teller - the bent version, with
distortedClaim - trust, as Dispositions your rule reads back
- the chain back to the one who saw it
- every belief canon contradicts
The village
Thornmere, the night before the rain. Oswin, the reeve, carries the seed grain from the granary up to the dry loft so it will not rot.
Tam, the miller’s apprentice, sees him carry the sacks out at midnight. That is all Tam sees.
tambesswilidarookmarenoswineldersCanon: what really happened
Canon says the reeve took the grain, that it is kept in the loft, and, just as plainly, that he neither stole it nor sold it. Tam gets one Holding: what he saw. The elders are a group, and membership is a Fact.
// What happened: the reeve moved the seed grain to the dry loft before the rain.
const took = claim.entity('oswin', 'took', 'seed-grain');
const stole = claim.entity('oswin', 'stole', 'seed-grain');
const sold = claim.entity('oswin', 'sold', 'seed-grain', { qualifiers: { to: 'the river traders' } });
const inLoft = claim.entity('seed-grain', 'kept-in', 'loft');
const ladder = [took, stole, sold];
await client.commit({
projectId, attemptId: 'night-1',
event: { id: 'night-1', type: 'night', title: 'Sacks in the dark', position: at(1), participants: ['oswin', 'tam'], provenance },
records: [
fact(took, { id: 'canon-took' }),
fact(inLoft, { id: 'canon-loft' }),
fact(negate(stole), { id: 'canon-not-stolen' }),
fact(negate(sold), { id: 'canon-not-sold' }),
knows('oswin', inLoft, witnessed('night-1'), { id: 'oswin-loft' }),
knows('tam', took, witnessed('night-1'), { id: 'tam-saw', text: 'I saw the reeve carry the seed grain out of the granary at midnight.' }),
fact(claim.entity('maren', 'umwelt:member-of', 'elders'), { id: 'maren-elder' }),
fact(claim.entity('wil', 'umwelt:member-of', 'elders'), { id: 'wil-elder' }),
],
});// What happened: the reeve moved the seed grain to the dry loft before the rain.
var took = Claim.Entity("oswin", "took", "seed-grain");
var stole = Claim.Entity("oswin", "stole", "seed-grain");
var sold = Claim.Entity("oswin", "sold", "seed-grain", qualifiers: new Dictionary<string, object?> { ["to"] = "the river traders" });
var inLoft = Claim.Entity("seed-grain", "kept-in", "loft");
var ladder = new[] { took, stole, sold };
var night = Position.At(1);
var witnessed = Acquired.Witnessed("night-1");
engine.Commit(new CommitInput
{
ProjectId = projectId,
AttemptId = "night-1",
Event = new EventInput { Id = "night-1", Type = "night", Title = "Sacks in the dark", Position = night, Participants = new() { "oswin", "tam" }, Provenance = provenance },
Records = new()
{
Records.Fact(took, id: "canon-took"),
Records.Fact(inLoft, id: "canon-loft"),
Records.Fact(stole.Negated(), id: "canon-not-stolen"),
Records.Fact(sold.Negated(), id: "canon-not-sold"),
Records.Knows("oswin", inLoft, witnessed, id: "oswin-loft"),
Records.Knows("tam", took, witnessed, id: "tam-saw", text: "I saw the reeve carry the seed grain out of the granary at midnight."),
Records.Fact(Claim.Entity("maren", "umwelt:member-of", "elders"), id: "maren-elder"),
Records.Fact(Claim.Entity("wil", "umwelt:member-of", "elders"), id: "wil-elder"),
},
});A Fact is nobody’s knowledge. The canon records make no villager know anything. Only Tam’s Holding does.
Trust
How much each villager believes the others. The numbers are your game’s; stored as Dispositions, your rule can read them back on any day, and you can ask later why someone believed what they did.
// How much each villager trusts the others. Your game owns these numbers; Umwelt keeps them.
const trust: [string, string, number][] = [
['bess', 'tam', 0.9], ['wil', 'tam', 0.6], ['ida', 'bess', 0.9], ['maren', 'bess', 0.5],
['ida', 'wil', 0.8], ['rook', 'ida', 0.95], ['maren', 'ida', 0.7], ['maren', 'rook', 0.4],
['oswin', 'wil', 0.9], ['oswin', 'maren', 0.9], ['bess', 'ida', 0.8], ['wil', 'ida', 0.7],
];
await client.dispositionSetBatch({
projectId, attemptId: 'trust', position: at(1),
dispositions: trust.map(([holder, target, value]) => ({ holder, target, dimension: 'trust', value })),
});// How much each villager trusts the others. Your game owns these numbers; Umwelt keeps them.
var trust = new (string Holder, string Target, double Value)[]
{
("bess", "tam", 0.9), ("wil", "tam", 0.6), ("ida", "bess", 0.9), ("maren", "bess", 0.5),
("ida", "wil", 0.8), ("rook", "ida", 0.95), ("maren", "ida", 0.7), ("maren", "rook", 0.4),
("oswin", "wil", 0.9), ("oswin", "maren", 0.9), ("bess", "ida", 0.8), ("wil", "ida", 0.7),
};
engine.DispositionSetBatch(new SetDispositionsInput
{
ProjectId = projectId,
AttemptId = "trust",
Position = night,
Dispositions = trust.Select(t => new DispositionSetting
{
Holder = t.Holder, Target = t.Target, Dimension = "trust", Value = JsonSerializer.SerializeToElement(t.Value),
}).ToList(),
});The gossip rule
Each day, everyone sure enough of the story tells the people they happen to meet. With some luck the story gets worse: took becomes stole, stole becomes sold. Confidence is the teller’s, times the listener’s trust. All of it is ordinary code, with a seeded random so the village gossips the same way every run.
// The gossip rule lives in your code. Umwelt never decides who talks or what they believe.
const meets: Record<string, string[]> = {
tam: ['bess', 'wil'], bess: ['tam', 'ida', 'maren'], wil: ['tam', 'ida', 'oswin'],
ida: ['bess', 'wil', 'rook', 'maren'], rook: ['ida', 'maren'], maren: ['bess', 'ida', 'rook', 'oswin'],
oswin: ['wil', 'maren'],
};
let seed = 12;
const random = () => {
seed = (seed + 0x6d2b79f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
// Who holds which version of the story, read back from Umwelt.
async function versionsAt(tick: number) {
const held = new Map<string, { rung: number; recordId: string; confidence: number }>();
for (const [rung, claim] of ladder.entries()) {
for (const item of await client.whoHolds({ projectId, claim, at: { tick }, filter: { polarity: true } })) {
held.set(item.holder, { rung, recordId: item.recordId, confidence: item.confidence });
}
}
return held;
}
async function trustOf(listener: string, teller: string, tick: number) {
const [trust] = await client.dispositions({ projectId, holder: listener, target: teller, dimension: 'trust', at: { tick } });
return trust?.kind === 'disposition' ? Number(trust.value) : 0.5;
}
for (const day of [2, 3, 4]) {
const held = await versionsAt(day - 1);
const told = new Set<string>();
const transmits: TransmitBatchItem[] = [];
for (const [teller, story] of [...held].sort(([a], [b]) => a.localeCompare(b))) {
if (story.confidence < 0.5) continue; // too unsure to repeat it
for (const listener of meets[teller]) {
if (listener === 'oswin' || held.has(listener) || told.has(listener)) continue;
if (random() > 0.7) continue; // they did not meet today
const rung = Math.min(2, story.rung + (random() < 0.4 ? 1 : 0));
const confidence = Math.round(story.confidence * (await trustOf(listener, teller, day)) * 100) / 100;
told.add(listener);
transmits.push({
attemptId: `day-${day}-${teller}-${listener}`,
from: teller, to: listener, sourceRecordId: story.recordId,
distortedClaim: rung !== story.rung ? ladder[rung] : null,
attitude: confidence >= 0.6 ? 'believes' : 'suspects',
confidence, recordId: `${listener}-heard`,
});
}
}
if (!transmits.length) continue; // a quiet day
// What every telling of the day has in common is said once. They believe what they say.
const outcomes = await client.transmitBatch({ shared: { projectId, intent: 'honest', position: at(day) }, transmits });
if (day === 2) print('day-2', outcomes);
}// The gossip rule lives in your code. Umwelt never decides who talks or what they believe.
var meets = new Dictionary<string, string[]>
{
["tam"] = new[] { "bess", "wil" }, ["bess"] = new[] { "tam", "ida", "maren" }, ["wil"] = new[] { "tam", "ida", "oswin" },
["ida"] = new[] { "bess", "wil", "rook", "maren" }, ["rook"] = new[] { "ida", "maren" },
["maren"] = new[] { "bess", "ida", "rook", "oswin" }, ["oswin"] = new[] { "wil", "maren" },
};
var seed = 12;
double Random()
{
unchecked
{
seed += 0x6d2b79f5;
var t = (seed ^ (int)((uint)seed >> 15)) * (1 | seed);
t = (t + (t ^ (int)((uint)t >> 7)) * (61 | t)) ^ t;
return (uint)(t ^ (int)((uint)t >> 14)) / 4294967296.0;
}
}
// Who holds which version of the story, read back from Umwelt.
Dictionary<string, (int Rung, string RecordId, double Confidence)> VersionsAt(long tick)
{
var held = new Dictionary<string, (int, string, double)>();
for (var rung = 0; rung < ladder.Length; rung++)
{
var filter = new WhoHoldsFilter { Polarity = true };
foreach (var item in engine.WhoHolds(new WhoHoldsPayload { ProjectId = projectId, Claim = ladder[rung], At = new At { Tick = tick }, Filter = filter }))
{
held[item.Holder] = (rung, item.RecordId, item.Confidence);
}
}
return held;
}
double TrustOf(string listener, string teller, long tick)
{
var trust = engine.Dispositions(new DispositionsPayload { ProjectId = projectId, Holder = listener, Target = teller, Dimension = "trust", At = new At { Tick = tick } });
return trust.OfType<Record.Disposition>().FirstOrDefault()?.Value.GetDouble() ?? 0.5;
}
foreach (var day in new long[] { 2, 3, 4 })
{
var held = VersionsAt(day - 1);
var told = new HashSet<string>();
var transmits = new List<TransmitBatchItem>();
foreach (var (teller, story) in held.OrderBy(pair => pair.Key, StringComparer.Ordinal))
{
if (story.Confidence < 0.5) continue; // too unsure to repeat it
foreach (var listener in meets[teller])
{
if (listener == "oswin" || held.ContainsKey(listener) || told.Contains(listener)) continue;
if (Random() > 0.7) continue; // they did not meet today
var rung = Math.Min(2, story.Rung + (Random() < 0.4 ? 1 : 0));
var confidence = Math.Round(story.Confidence * TrustOf(listener, teller, day) * 100, MidpointRounding.AwayFromZero) / 100;
told.Add(listener);
transmits.Add(new TransmitBatchItem
{
AttemptId = $"day-{day}-{teller}-{listener}",
From = teller, To = listener, SourceRecordId = story.RecordId,
DistortedClaim = rung != story.Rung ? ladder[rung] : null,
Attitude = confidence >= 0.6 ? Attitude.Believes : Attitude.Suspects,
Confidence = confidence, RecordId = $"{listener}-heard",
});
}
}
if (transmits.Count == 0) continue; // a quiet day
// What every telling of the day has in common is said once. They believe what they say.
var outcomes = engine.TransmitBatch(new TransmitBatchPayload
{
Shared = new TransmitShared { ProjectId = projectId, Intent = Intent.Honest, Position = Position.At(day) },
Transmits = transmits,
});
if (day == 2) Print("day-2", outcomes);
}The first day of gossip sends two tellings in one batch. Tam tells Bess, and the story bends; Tam tells Wil, and it does not.
[
{
"eventId": "01a1cbb7-cc12-7003-b39a-925053460fd2",
"recordIds": ["bess-heard"],
"transitionIds": [],
"version": 3,
"warnings": []
},
{
"eventId": "01a1cbb7-cc12-7003-b39a-92699797d4d3",
"recordIds": ["wil-heard"],
"transitionIds": [],
"version": 4,
"warnings": []
}
]Intent is honest. Nobody in Thornmere lies: they repeat what they believe. A distorted claim with intent: "honest" is a story that bent on its own. A liar would be deceptive.
The truth comes out
On day five Oswin opens the loft. Wil, Maren and Bess are there and see the grain. Your rule says that whoever sees it drops the theft story, so their Holdings move to abandoned. Wil only ever held that Oswin took the grain, which is true, so he keeps it. Nobody else changes their mind.
// Day 5: the reeve opens the loft. Whoever sees the grain drops the theft story; nobody else does.
const present = ['wil', 'maren', 'bess'];
await client.witness({
projectId, attemptId: 'day-5',
event: { id: 'loft-opened', type: 'scene', title: 'The loft is opened', position: at(5), participants: ['oswin', ...present], provenance },
claims: [{ claim: inLoft }],
witnesses: present.map((holder) => ({ holder })),
});
const before = await versionsAt(5);
const disproved = present.filter((who) => (before.get(who)?.rung ?? 0) > 0).map((who) => before.get(who)!.recordId);
if (disproved.length) {
await client.transitionBatch({ projectId, attemptId: 'abandon', position: at(5), recordIds: disproved, status: 'abandoned' });
}// Day 5: the reeve opens the loft. Whoever sees the grain drops the theft story; nobody else does.
var present = new List<string> { "wil", "maren", "bess" };
engine.Witness(new WitnessInput
{
ProjectId = projectId,
AttemptId = "day-5",
Event = new EventInput
{
Id = "loft-opened", Type = "scene", Title = "The loft is opened", Position = Position.At(5),
Participants = present.Prepend("oswin").ToList(), Provenance = provenance,
},
Claims = new() { new WitnessedClaim { Claim = inLoft } },
Witnesses = present.Select(holder => new Witness { Holder = holder }).ToList(),
});
var before = VersionsAt(5);
var disproved = present.Where(who => before.TryGetValue(who, out var story) && story.Rung > 0).Select(who => before[who].RecordId).ToList();
if (disproved.Count > 0)
{
engine.TransitionBatch(new TransitionBatchInput
{
ProjectId = projectId, AttemptId = "abandon", Position = Position.At(5), RecordIds = disproved, Status = TransitionStatus.Abandoned,
});
}Nothing is deleted. Read day four and Bess still believes Oswin stole the grain. The transition only ends the belief from day five on.
Ask the village
const spread: Record<string, (string | number)[][]> = {};
for (const day of [1, 2, 3, 4, 5]) {
const held = await versionsAt(day);
spread[day] = Object.keys(villagers).map((who) => {
const story = held.get(who);
return story ? [who, ladder[story.rung].predicate, story.confidence] : [who, '', 0];
});
}
print('spread', spread);
print('whoHolds-stole', await client.whoHolds({ projectId, claim: stole, at: { tick: 4 } }));
const farthest = [...(await versionsAt(4))].find(([, story]) => story.rung === 2);
if (farthest) {
const trace = await client.trace({ projectId, recordId: farthest[1].recordId });
print('trace', trace.chain.map(({ record, acquisition }) => ({
holder: record.kind === 'holding' ? record.holder : null,
says: record.kind === 'holding' ? record.claim.predicate : null,
confidence: record.kind === 'holding' ? record.confidence : null,
how: acquisition?.kind,
from: acquisition?.kind === 'told-by' ? acquisition.sourceHolderId : null,
})));
print('contradictions', await client.contradictions({ projectId, holder: farthest[0], at: { tick: 5 } }));
}
print('council-day-4', await client.groupCoverage({ projectId, group: 'elders', claim: stole, at: { tick: 4 } }));
print('council-day-5', await client.groupCoverage({ projectId, group: 'elders', claim: stole, at: { tick: 5 } }));
print('tam-day-5', await client.holds({ projectId, holder: 'tam', claim: took, at: { tick: 5 } }));
await client.close();var spread = new Dictionary<string, object[][]>();
foreach (var day in new long[] { 1, 2, 3, 4, 5 })
{
var held = VersionsAt(day);
spread[day.ToString()] = villagers.Keys.Select(who => held.TryGetValue(who, out var story)
? new object[] { who, ladder[story.Rung].Predicate, story.Confidence }
: new object[] { who, "", 0 }).ToArray();
}
Print("spread", spread);
Print("whoHolds-stole", engine.WhoHolds(new WhoHoldsPayload { ProjectId = projectId, Claim = stole, At = new At { Tick = 4 } }));
var farthest = VersionsAt(4).FirstOrDefault(pair => pair.Value.Rung == 2);
if (farthest.Key is not null)
{
var trace = engine.Trace(new RecordId { ProjectId = projectId, RecordIdValue = farthest.Value.RecordId });
Print("trace", trace.Chain.Select(link => new
{
holder = (link.Record as Record.Holding)?.Holder,
says = (link.Record as Record.Holding)?.Claim.Predicate,
confidence = (link.Record as Record.Holding)?.Confidence,
how = link.Acquisition?.Kind,
from = (link.Acquisition as Acquisition.ToldBy)?.SourceHolderId,
}));
Print("contradictions", engine.Contradictions(new HolderAt { ProjectId = projectId, Holder = farthest.Key, At = new At { Tick = 5 } }));
}
Print("council-day-4", engine.GroupCoverage(new GroupCoveragePayload { ProjectId = projectId, Group = "elders", Claim = stole, At = new At { Tick = 4 } }));
Print("council-day-5", engine.GroupCoverage(new GroupCoveragePayload { ProjectId = projectId, Group = "elders", Claim = stole, At = new At { Tick = 5 } }));
Print("tam-day-5", engine.Holds(new HoldsPayload { ProjectId = projectId, Holder = "tam", Claim = took, At = new At { Tick = 5 } }));Who believes he stole it
On day four, two villagers hold the middle version. Maren only suspects it: she heard it from Bess, whom she trusts by half.
[
{
"attitude": "believes",
"confidence": 0.9,
"holder": "bess",
"polarity": true,
"recordId": "bess-heard"
},
{
"attitude": "suspects",
"confidence": 0.45,
"holder": "maren",
"polarity": true,
"recordId": "maren-heard"
}
]Where Ida’s story came from
trace follows each told-by back to its source. Ida believes the grain was sold; Bess told her it was stolen; Tam only saw it taken.
What canon says about it
Ida’s belief contradicts the Fact that the grain was not sold. Umwelt reports it; it never corrects her.
[
{
"holdingId": "ida-heard",
"kind": "vs-canon",
"otherRecordId": "canon-not-sold",
"reason": "opposite-polarity"
}
]What the elders know
On day four one elder suspects the theft. On day five, after the loft, none does.
Try next
transmitBatch, and see where it arrives too late.beliefsAbout what he thinks the village believes.travel axis, and compare what a traveller hears with what the village knew.context.admit so a model playing Rook can only repeat what Rook holds. See The AI innkeeper.The whole program
One file that builds the world, runs it and prints every answer on this page. The TypeScript program and the .NET one print the same answers: the script that runs them for these pages checks it.
import { at, by, claim, createClient, fact, knows, negate, witnessed } from '@kallamo/umwelt';
import type { TransmitBatchItem } from '@kallamo/umwelt';
const client = createClient({ file: 'thornmere.umw', create: true });
const projectId = 'thornmere';
const provenance = by.human();
const print = (label: string, value: unknown) => console.log(`@@${label}\n${JSON.stringify(value)}`);
const villagers = {
tam: 'Tam, the miller’s apprentice', bess: 'Bess, the baker', wil: 'Wil, the smith',
ida: 'Ida, the innkeeper', rook: 'Rook, the ferryman', maren: 'Maren, the herbalist',
oswin: 'Oswin, the reeve',
};
// The world, declared once: sending the same manifest again changes nothing.
await client.worldEnsure({
project: { id: projectId, name: 'Thornmere', timescale: 'day' },
entityTypes: [
{ id: 'villager', name: 'Villager', kind: 'agent' },
{ id: 'council', name: 'Council', kind: 'group' },
{ id: 'thing', name: 'Thing', kind: 'other' },
],
entities: [
...Object.entries(villagers).map(([id, name]) => ({ id, typeId: 'villager', name })),
{ id: 'elders', typeId: 'council', name: 'The council of elders' },
{ id: 'seed-grain', typeId: 'thing', name: 'The seed grain' },
{ id: 'loft', typeId: 'thing', name: 'The dry loft' },
],
predicates: [
{ id: 'took', label: 'took', object: 'entity', cardinality: 'many' },
{ id: 'stole', label: 'stole', object: 'entity', cardinality: 'many' },
{ id: 'sold', label: 'sold', object: 'entity', cardinality: 'many' },
{ id: 'kept-in', label: 'is kept in', object: 'entity', cardinality: 'one' },
],
});
// What happened: the reeve moved the seed grain to the dry loft before the rain.
const took = claim.entity('oswin', 'took', 'seed-grain');
const stole = claim.entity('oswin', 'stole', 'seed-grain');
const sold = claim.entity('oswin', 'sold', 'seed-grain', { qualifiers: { to: 'the river traders' } });
const inLoft = claim.entity('seed-grain', 'kept-in', 'loft');
const ladder = [took, stole, sold];
await client.commit({
projectId, attemptId: 'night-1',
event: { id: 'night-1', type: 'night', title: 'Sacks in the dark', position: at(1), participants: ['oswin', 'tam'], provenance },
records: [
fact(took, { id: 'canon-took' }),
fact(inLoft, { id: 'canon-loft' }),
fact(negate(stole), { id: 'canon-not-stolen' }),
fact(negate(sold), { id: 'canon-not-sold' }),
knows('oswin', inLoft, witnessed('night-1'), { id: 'oswin-loft' }),
knows('tam', took, witnessed('night-1'), { id: 'tam-saw', text: 'I saw the reeve carry the seed grain out of the granary at midnight.' }),
fact(claim.entity('maren', 'umwelt:member-of', 'elders'), { id: 'maren-elder' }),
fact(claim.entity('wil', 'umwelt:member-of', 'elders'), { id: 'wil-elder' }),
],
});
// How much each villager trusts the others. Your game owns these numbers; Umwelt keeps them.
const trust: [string, string, number][] = [
['bess', 'tam', 0.9], ['wil', 'tam', 0.6], ['ida', 'bess', 0.9], ['maren', 'bess', 0.5],
['ida', 'wil', 0.8], ['rook', 'ida', 0.95], ['maren', 'ida', 0.7], ['maren', 'rook', 0.4],
['oswin', 'wil', 0.9], ['oswin', 'maren', 0.9], ['bess', 'ida', 0.8], ['wil', 'ida', 0.7],
];
await client.dispositionSetBatch({
projectId, attemptId: 'trust', position: at(1),
dispositions: trust.map(([holder, target, value]) => ({ holder, target, dimension: 'trust', value })),
});
// The gossip rule lives in your code. Umwelt never decides who talks or what they believe.
const meets: Record<string, string[]> = {
tam: ['bess', 'wil'], bess: ['tam', 'ida', 'maren'], wil: ['tam', 'ida', 'oswin'],
ida: ['bess', 'wil', 'rook', 'maren'], rook: ['ida', 'maren'], maren: ['bess', 'ida', 'rook', 'oswin'],
oswin: ['wil', 'maren'],
};
let seed = 12;
const random = () => {
seed = (seed + 0x6d2b79f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
// Who holds which version of the story, read back from Umwelt.
async function versionsAt(tick: number) {
const held = new Map<string, { rung: number; recordId: string; confidence: number }>();
for (const [rung, claim] of ladder.entries()) {
for (const item of await client.whoHolds({ projectId, claim, at: { tick }, filter: { polarity: true } })) {
held.set(item.holder, { rung, recordId: item.recordId, confidence: item.confidence });
}
}
return held;
}
async function trustOf(listener: string, teller: string, tick: number) {
const [trust] = await client.dispositions({ projectId, holder: listener, target: teller, dimension: 'trust', at: { tick } });
return trust?.kind === 'disposition' ? Number(trust.value) : 0.5;
}
for (const day of [2, 3, 4]) {
const held = await versionsAt(day - 1);
const told = new Set<string>();
const transmits: TransmitBatchItem[] = [];
for (const [teller, story] of [...held].sort(([a], [b]) => a.localeCompare(b))) {
if (story.confidence < 0.5) continue; // too unsure to repeat it
for (const listener of meets[teller]) {
if (listener === 'oswin' || held.has(listener) || told.has(listener)) continue;
if (random() > 0.7) continue; // they did not meet today
const rung = Math.min(2, story.rung + (random() < 0.4 ? 1 : 0));
const confidence = Math.round(story.confidence * (await trustOf(listener, teller, day)) * 100) / 100;
told.add(listener);
transmits.push({
attemptId: `day-${day}-${teller}-${listener}`,
from: teller, to: listener, sourceRecordId: story.recordId,
distortedClaim: rung !== story.rung ? ladder[rung] : null,
attitude: confidence >= 0.6 ? 'believes' : 'suspects',
confidence, recordId: `${listener}-heard`,
});
}
}
if (!transmits.length) continue; // a quiet day
// What every telling of the day has in common is said once. They believe what they say.
const outcomes = await client.transmitBatch({ shared: { projectId, intent: 'honest', position: at(day) }, transmits });
if (day === 2) print('day-2', outcomes);
}
// Day 5: the reeve opens the loft. Whoever sees the grain drops the theft story; nobody else does.
const present = ['wil', 'maren', 'bess'];
await client.witness({
projectId, attemptId: 'day-5',
event: { id: 'loft-opened', type: 'scene', title: 'The loft is opened', position: at(5), participants: ['oswin', ...present], provenance },
claims: [{ claim: inLoft }],
witnesses: present.map((holder) => ({ holder })),
});
const before = await versionsAt(5);
const disproved = present.filter((who) => (before.get(who)?.rung ?? 0) > 0).map((who) => before.get(who)!.recordId);
if (disproved.length) {
await client.transitionBatch({ projectId, attemptId: 'abandon', position: at(5), recordIds: disproved, status: 'abandoned' });
}
const spread: Record<string, (string | number)[][]> = {};
for (const day of [1, 2, 3, 4, 5]) {
const held = await versionsAt(day);
spread[day] = Object.keys(villagers).map((who) => {
const story = held.get(who);
return story ? [who, ladder[story.rung].predicate, story.confidence] : [who, '', 0];
});
}
print('spread', spread);
print('whoHolds-stole', await client.whoHolds({ projectId, claim: stole, at: { tick: 4 } }));
const farthest = [...(await versionsAt(4))].find(([, story]) => story.rung === 2);
if (farthest) {
const trace = await client.trace({ projectId, recordId: farthest[1].recordId });
print('trace', trace.chain.map(({ record, acquisition }) => ({
holder: record.kind === 'holding' ? record.holder : null,
says: record.kind === 'holding' ? record.claim.predicate : null,
confidence: record.kind === 'holding' ? record.confidence : null,
how: acquisition?.kind,
from: acquisition?.kind === 'told-by' ? acquisition.sourceHolderId : null,
})));
print('contradictions', await client.contradictions({ projectId, holder: farthest[0], at: { tick: 5 } }));
}
print('council-day-4', await client.groupCoverage({ projectId, group: 'elders', claim: stole, at: { tick: 4 } }));
print('council-day-5', await client.groupCoverage({ projectId, group: 'elders', claim: stole, at: { tick: 5 } }));
print('tam-day-5', await client.holds({ projectId, holder: 'tam', claim: took, at: { tick: 5 } }));
await client.close();using System.Text.Json;
using System.Text.Json.Serialization;
using Kallamo.Umwelt;
using var engine = UmweltEngine.Create("thornmere.umw");
const string projectId = "thornmere";
var provenance = Provenance.Human();
var json = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.KebabCaseLower) },
};
void Print(string label, object? value) => Console.WriteLine($"@@{label}\n{JsonSerializer.Serialize(value, json)}");
var villagers = new Dictionary<string, string>
{
["tam"] = "Tam, the miller’s apprentice", ["bess"] = "Bess, the baker", ["wil"] = "Wil, the smith",
["ida"] = "Ida, the innkeeper", ["rook"] = "Rook, the ferryman", ["maren"] = "Maren, the herbalist",
["oswin"] = "Oswin, the reeve",
};
// The world, declared once: sending the same manifest again changes nothing.
engine.WorldEnsure(new EnsureWorldInput
{
Project = new WorldProject { Id = projectId, Name = "Thornmere", Timescale = "day" },
EntityTypes = new()
{
new WorldEntityType { Id = "villager", Name = "Villager", Kind = EntityKind.Agent },
new WorldEntityType { Id = "council", Name = "Council", Kind = EntityKind.Group },
new WorldEntityType { Id = "thing", Name = "Thing", Kind = EntityKind.Other },
},
Entities = villagers.Select(villager => new WorldEntity { Id = villager.Key, TypeId = "villager", Name = villager.Value })
.Append(new WorldEntity { Id = "elders", TypeId = "council", Name = "The council of elders" })
.Append(new WorldEntity { Id = "seed-grain", TypeId = "thing", Name = "The seed grain" })
.Append(new WorldEntity { Id = "loft", TypeId = "thing", Name = "The dry loft" })
.ToList(),
Predicates = new()
{
new PredicateDef { Id = "took", Label = "took", Object = ObjectKind.Entity, Cardinality = Cardinality.Many },
new PredicateDef { Id = "stole", Label = "stole", Object = ObjectKind.Entity, Cardinality = Cardinality.Many },
new PredicateDef { Id = "sold", Label = "sold", Object = ObjectKind.Entity, Cardinality = Cardinality.Many },
new PredicateDef { Id = "kept-in", Label = "is kept in", Object = ObjectKind.Entity, Cardinality = Cardinality.One },
},
});
// What happened: the reeve moved the seed grain to the dry loft before the rain.
var took = Claim.Entity("oswin", "took", "seed-grain");
var stole = Claim.Entity("oswin", "stole", "seed-grain");
var sold = Claim.Entity("oswin", "sold", "seed-grain", qualifiers: new Dictionary<string, object?> { ["to"] = "the river traders" });
var inLoft = Claim.Entity("seed-grain", "kept-in", "loft");
var ladder = new[] { took, stole, sold };
var night = Position.At(1);
var witnessed = Acquired.Witnessed("night-1");
engine.Commit(new CommitInput
{
ProjectId = projectId,
AttemptId = "night-1",
Event = new EventInput { Id = "night-1", Type = "night", Title = "Sacks in the dark", Position = night, Participants = new() { "oswin", "tam" }, Provenance = provenance },
Records = new()
{
Records.Fact(took, id: "canon-took"),
Records.Fact(inLoft, id: "canon-loft"),
Records.Fact(stole.Negated(), id: "canon-not-stolen"),
Records.Fact(sold.Negated(), id: "canon-not-sold"),
Records.Knows("oswin", inLoft, witnessed, id: "oswin-loft"),
Records.Knows("tam", took, witnessed, id: "tam-saw", text: "I saw the reeve carry the seed grain out of the granary at midnight."),
Records.Fact(Claim.Entity("maren", "umwelt:member-of", "elders"), id: "maren-elder"),
Records.Fact(Claim.Entity("wil", "umwelt:member-of", "elders"), id: "wil-elder"),
},
});
// How much each villager trusts the others. Your game owns these numbers; Umwelt keeps them.
var trust = new (string Holder, string Target, double Value)[]
{
("bess", "tam", 0.9), ("wil", "tam", 0.6), ("ida", "bess", 0.9), ("maren", "bess", 0.5),
("ida", "wil", 0.8), ("rook", "ida", 0.95), ("maren", "ida", 0.7), ("maren", "rook", 0.4),
("oswin", "wil", 0.9), ("oswin", "maren", 0.9), ("bess", "ida", 0.8), ("wil", "ida", 0.7),
};
engine.DispositionSetBatch(new SetDispositionsInput
{
ProjectId = projectId,
AttemptId = "trust",
Position = night,
Dispositions = trust.Select(t => new DispositionSetting
{
Holder = t.Holder, Target = t.Target, Dimension = "trust", Value = JsonSerializer.SerializeToElement(t.Value),
}).ToList(),
});
// The gossip rule lives in your code. Umwelt never decides who talks or what they believe.
var meets = new Dictionary<string, string[]>
{
["tam"] = new[] { "bess", "wil" }, ["bess"] = new[] { "tam", "ida", "maren" }, ["wil"] = new[] { "tam", "ida", "oswin" },
["ida"] = new[] { "bess", "wil", "rook", "maren" }, ["rook"] = new[] { "ida", "maren" },
["maren"] = new[] { "bess", "ida", "rook", "oswin" }, ["oswin"] = new[] { "wil", "maren" },
};
var seed = 12;
double Random()
{
unchecked
{
seed += 0x6d2b79f5;
var t = (seed ^ (int)((uint)seed >> 15)) * (1 | seed);
t = (t + (t ^ (int)((uint)t >> 7)) * (61 | t)) ^ t;
return (uint)(t ^ (int)((uint)t >> 14)) / 4294967296.0;
}
}
// Who holds which version of the story, read back from Umwelt.
Dictionary<string, (int Rung, string RecordId, double Confidence)> VersionsAt(long tick)
{
var held = new Dictionary<string, (int, string, double)>();
for (var rung = 0; rung < ladder.Length; rung++)
{
var filter = new WhoHoldsFilter { Polarity = true };
foreach (var item in engine.WhoHolds(new WhoHoldsPayload { ProjectId = projectId, Claim = ladder[rung], At = new At { Tick = tick }, Filter = filter }))
{
held[item.Holder] = (rung, item.RecordId, item.Confidence);
}
}
return held;
}
double TrustOf(string listener, string teller, long tick)
{
var trust = engine.Dispositions(new DispositionsPayload { ProjectId = projectId, Holder = listener, Target = teller, Dimension = "trust", At = new At { Tick = tick } });
return trust.OfType<Record.Disposition>().FirstOrDefault()?.Value.GetDouble() ?? 0.5;
}
foreach (var day in new long[] { 2, 3, 4 })
{
var held = VersionsAt(day - 1);
var told = new HashSet<string>();
var transmits = new List<TransmitBatchItem>();
foreach (var (teller, story) in held.OrderBy(pair => pair.Key, StringComparer.Ordinal))
{
if (story.Confidence < 0.5) continue; // too unsure to repeat it
foreach (var listener in meets[teller])
{
if (listener == "oswin" || held.ContainsKey(listener) || told.Contains(listener)) continue;
if (Random() > 0.7) continue; // they did not meet today
var rung = Math.Min(2, story.Rung + (Random() < 0.4 ? 1 : 0));
var confidence = Math.Round(story.Confidence * TrustOf(listener, teller, day) * 100, MidpointRounding.AwayFromZero) / 100;
told.Add(listener);
transmits.Add(new TransmitBatchItem
{
AttemptId = $"day-{day}-{teller}-{listener}",
From = teller, To = listener, SourceRecordId = story.RecordId,
DistortedClaim = rung != story.Rung ? ladder[rung] : null,
Attitude = confidence >= 0.6 ? Attitude.Believes : Attitude.Suspects,
Confidence = confidence, RecordId = $"{listener}-heard",
});
}
}
if (transmits.Count == 0) continue; // a quiet day
// What every telling of the day has in common is said once. They believe what they say.
var outcomes = engine.TransmitBatch(new TransmitBatchPayload
{
Shared = new TransmitShared { ProjectId = projectId, Intent = Intent.Honest, Position = Position.At(day) },
Transmits = transmits,
});
if (day == 2) Print("day-2", outcomes);
}
// Day 5: the reeve opens the loft. Whoever sees the grain drops the theft story; nobody else does.
var present = new List<string> { "wil", "maren", "bess" };
engine.Witness(new WitnessInput
{
ProjectId = projectId,
AttemptId = "day-5",
Event = new EventInput
{
Id = "loft-opened", Type = "scene", Title = "The loft is opened", Position = Position.At(5),
Participants = present.Prepend("oswin").ToList(), Provenance = provenance,
},
Claims = new() { new WitnessedClaim { Claim = inLoft } },
Witnesses = present.Select(holder => new Witness { Holder = holder }).ToList(),
});
var before = VersionsAt(5);
var disproved = present.Where(who => before.TryGetValue(who, out var story) && story.Rung > 0).Select(who => before[who].RecordId).ToList();
if (disproved.Count > 0)
{
engine.TransitionBatch(new TransitionBatchInput
{
ProjectId = projectId, AttemptId = "abandon", Position = Position.At(5), RecordIds = disproved, Status = TransitionStatus.Abandoned,
});
}
var spread = new Dictionary<string, object[][]>();
foreach (var day in new long[] { 1, 2, 3, 4, 5 })
{
var held = VersionsAt(day);
spread[day.ToString()] = villagers.Keys.Select(who => held.TryGetValue(who, out var story)
? new object[] { who, ladder[story.Rung].Predicate, story.Confidence }
: new object[] { who, "", 0 }).ToArray();
}
Print("spread", spread);
Print("whoHolds-stole", engine.WhoHolds(new WhoHoldsPayload { ProjectId = projectId, Claim = stole, At = new At { Tick = 4 } }));
var farthest = VersionsAt(4).FirstOrDefault(pair => pair.Value.Rung == 2);
if (farthest.Key is not null)
{
var trace = engine.Trace(new RecordId { ProjectId = projectId, RecordIdValue = farthest.Value.RecordId });
Print("trace", trace.Chain.Select(link => new
{
holder = (link.Record as Record.Holding)?.Holder,
says = (link.Record as Record.Holding)?.Claim.Predicate,
confidence = (link.Record as Record.Holding)?.Confidence,
how = link.Acquisition?.Kind,
from = (link.Acquisition as Acquisition.ToldBy)?.SourceHolderId,
}));
Print("contradictions", engine.Contradictions(new HolderAt { ProjectId = projectId, Holder = farthest.Key, At = new At { Tick = 5 } }));
}
Print("council-day-4", engine.GroupCoverage(new GroupCoveragePayload { ProjectId = projectId, Group = "elders", Claim = stole, At = new At { Tick = 4 } }));
Print("council-day-5", engine.GroupCoverage(new GroupCoveragePayload { ProjectId = projectId, Group = "elders", Claim = stole, At = new At { Tick = 5 } }));
Print("tam-day-5", engine.Holds(new HoldsPayload { ProjectId = projectId, Holder = "tam", Claim = took, At = new At { Tick = 5 } }));