Feelings that change
Trust, fear and affection at court, one arrow at a time. A rescue changes one heart, a betrayal another, and the past keeps the old values.
| From | Toward | Feeling | Act 1 | Act 2, the hunt | Act 3, the letters |
|---|---|---|---|---|---|
| Aurel | Sabine | affection | 0.70 | 0.70 | 0.70 |
| Aurel | Sabine | fear | · | · | 0.40 |
| Aurel | Sabine | trust | 0.90 | 0.90 | 0.20 |
| Caspar | Aurel | trust | 0.95 | 0.95 | 0.95 |
| Caspar | Sabine | affection | · | 0.50 | 0.50 |
| Caspar | Sabine | fear | 0.20 | 0.00 | 0.00 |
| Caspar | Sabine | trust | 0.30 | 0.70 | 0.70 |
| Sabine | Aurel | affection | 0.60 | 0.60 | 0.60 |
| Sabine | Aurel | trust | 0.40 | 0.40 | 0.40 |
| Sabine | Caspar | affection | 0.50 | 0.50 | 0.50 |
Every Disposition at court, act by act, read back with dispositions. Red numbers changed in that act. Each row is one direction.
The idea
Relationships are not symmetric. A rescue changes how the rescued feels about the rescuer, not the other way round; a betrayal changes the betrayed. Umwelt stores feelings as Dispositions: one holder, one target, one dimension, one value, and the newest one is current. Your rules decide what events do to them. The history keeps every value they ever had.
Your game decides
- the dimensions: trust, affection, fear
- what a rescue or a betrayal does to them
- who feels it: only the one it happened to
- what a feeling changes in the story
Umwelt keeps
- one arrow per holder, target and dimension
- the newest value as the current one
- every earlier value, at its act
- nothing in the other direction unless you write it
The court of Ilvar
Prince Aurel trusts Sabine, the envoy from across the sea, more than she trusts him. Sir Caspar, his knight, trusts her very little.
In the second act Sabine saves Caspar from a boar at the royal hunt. In the third, Aurel finds she has been reading his letters.
Act 1
One batch of Dispositions sets the court as the story opens.
// Act 1: how each of them stands toward the others. One arrow at a time:
// what Aurel feels for Sabine says nothing about what she feels for him.
await client.dispositionSetBatch({
projectId, attemptId: 'act-1', position: at(1),
dispositions: [
{ holder: 'aurel', target: 'sabine', dimension: 'trust', value: 0.9 },
{ holder: 'aurel', target: 'sabine', dimension: 'affection', value: 0.7 },
{ holder: 'sabine', target: 'aurel', dimension: 'trust', value: 0.4 },
{ holder: 'sabine', target: 'aurel', dimension: 'affection', value: 0.6 },
{ holder: 'caspar', target: 'sabine', dimension: 'trust', value: 0.3 },
{ holder: 'caspar', target: 'sabine', dimension: 'fear', value: 0.2 },
{ holder: 'sabine', target: 'caspar', dimension: 'affection', value: 0.5 },
{ holder: 'caspar', target: 'aurel', dimension: 'trust', value: 0.95 },
],
});// Act 1: how each of them stands toward the others. One arrow at a time:
// what Aurel feels for Sabine says nothing about what she feels for him.
DispositionSetting Feels(string holder, string target, string dimension, double value) =>
new() { Holder = holder, Target = target, Dimension = dimension, Value = JsonSerializer.SerializeToElement(value) };
engine.DispositionSetBatch(new SetDispositionsInput
{
ProjectId = projectId, AttemptId = "act-1", Position = Position.At(1),
Dispositions = new()
{
Feels("aurel", "sabine", "trust", 0.9), Feels("aurel", "sabine", "affection", 0.7),
Feels("sabine", "aurel", "trust", 0.4), Feels("sabine", "aurel", "affection", 0.6),
Feels("caspar", "sabine", "trust", 0.3), Feels("caspar", "sabine", "fear", 0.2),
Feels("sabine", "caspar", "affection", 0.5), Feels("caspar", "aurel", "trust", 0.95),
},
});Directional. Aurel’s trust in Sabine is 0.9; hers in him is 0.4. They are two records, and neither says anything about the other.
What events do
Your rules read the current values, compute new ones and record them. The hunt raises Caspar’s trust and affection for Sabine and takes his fear away. The letters cut Aurel’s trust in her and give him reason to fear her.
// Your rules for what events do to feelings. Umwelt keeps each new value as the current one.
async function current(holder: string, target: string, tick: number) {
const now: Record<string, number> = {};
for (const record of await client.dispositions({ projectId, holder, target, at: { tick } })) {
if (record.kind === 'disposition') now[record.dimension] = Number(record.value);
}
return now;
}
const clamp = (x: number) => Math.round(Math.min(1, Math.max(0, x)) * 100) / 100;
// Act 2, the hunt: Sabine saves Caspar. Only the one who was saved changes.
const saved = await current('caspar', 'sabine', 2);
const hunt: DispositionSetting[] = [
{ holder: 'caspar', target: 'sabine', dimension: 'trust', value: clamp((saved.trust ?? 0.5) + 0.4) },
{ holder: 'caspar', target: 'sabine', dimension: 'affection', value: clamp((saved.affection ?? 0) + 0.5) },
{ holder: 'caspar', target: 'sabine', dimension: 'fear', value: 0 },
];
await client.dispositionSetBatch({ projectId, attemptId: 'act-2', position: at(2), dispositions: hunt });
// Act 3, the letters: Aurel finds Sabine has been reading his correspondence.
const betrayed = await current('aurel', 'sabine', 3);
await client.dispositionSetBatch({
projectId, attemptId: 'act-3', position: at(3),
dispositions: [
{ holder: 'aurel', target: 'sabine', dimension: 'trust', value: clamp((betrayed.trust ?? 0.5) - 0.7) },
{ holder: 'aurel', target: 'sabine', dimension: 'fear', value: clamp((betrayed.fear ?? 0) + 0.4) },
],
});// Your rules for what events do to feelings. Umwelt keeps each new value as the current one.
Dictionary<string, double> Current(string holder, string target, long tick)
{
var felt = engine.Dispositions(new DispositionsPayload { ProjectId = projectId, Holder = holder, Target = target, At = new At { Tick = tick } });
var now = new Dictionary<string, double>();
foreach (var record in felt.OfType<Record.Disposition>())
{
now[record.Dimension] = record.Value.GetDouble();
}
return now;
}
double Clamp(double x) => Math.Round(Math.Min(1, Math.Max(0, x)) * 100, MidpointRounding.AwayFromZero) / 100;
double Or(Dictionary<string, double> values, string key, double fallback) => values.TryGetValue(key, out var v) ? v : fallback;
// Act 2, the hunt: Sabine saves Caspar. Only the one who was saved changes.
var saved = Current("caspar", "sabine", 2);
engine.DispositionSetBatch(new SetDispositionsInput
{
ProjectId = projectId, AttemptId = "act-2", Position = Position.At(2),
Dispositions = new()
{
Feels("caspar", "sabine", "trust", Clamp(Or(saved, "trust", 0.5) + 0.4)),
Feels("caspar", "sabine", "affection", Clamp(Or(saved, "affection", 0) + 0.5)),
Feels("caspar", "sabine", "fear", 0),
},
});
// Act 3, the letters: Aurel finds Sabine has been reading his correspondence.
var betrayed = Current("aurel", "sabine", 3);
engine.DispositionSetBatch(new SetDispositionsInput
{
ProjectId = projectId, AttemptId = "act-3", Position = Position.At(3),
Dispositions = new()
{
Feels("aurel", "sabine", "trust", Clamp(Or(betrayed, "trust", 0.5) - 0.7)),
Feels("aurel", "sabine", "fear", Clamp(Or(betrayed, "fear", 0) + 0.4)),
},
});Every arrow, every act
// Every arrow in every act, read back from Umwelt.
const acts: Record<string, [string, string, string, number][]> = {};
for (const act of [1, 2, 3]) {
acts[act] = [];
for (const holder of Object.keys(court)) {
for (const target of Object.keys(court)) {
if (holder === target) continue;
for (const [dimension, value] of Object.entries(await current(holder, target, act))) {
acts[act].push([holder, target, dimension, value]);
}
}
}
}
print('acts', acts);
// The same pair, both ways, after the letters.
print('both-ways', {
aurelToSabine: await current('aurel', 'sabine', 3),
sabineToAurel: await current('sabine', 'aurel', 3),
});
await client.close();// Every arrow in every act, read back from Umwelt.
var acts = new Dictionary<string, List<object[]>>();
foreach (var act in new long[] { 1, 2, 3 })
{
acts[act.ToString()] = new();
foreach (var holder in court.Keys)
{
foreach (var target in court.Keys.Where(t => t != holder))
{
foreach (var (dimension, value) in Current(holder, target, act))
{
acts[act.ToString()].Add(new object[] { holder, target, dimension, value });
}
}
}
}
Print("acts", acts);
// The same pair, both ways, after the letters.
Print("both-ways", new { aurelToSabine = Current("aurel", "sabine", 3), sabineToAurel = Current("sabine", "aurel", 3) });{
"aurelToSabine": { "affection": 0.7, "fear": 0.4, "trust": 0.2 },
"sabineToAurel": { "affection": 0.6, "trust": 0.4 }
}After the letters, Aurel trusts Sabine at 0.2 and now fears her. Sabine’s feelings for him have not moved: nothing happened to her.
The past keeps its values. Read act 1 and Aurel still trusts Sabine at 0.9. Each act’s Dispositions are records at that act; the newest one is only the current one from its own position on.
Try next
beliefsAbout knows better.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, createClient } from '@kallamo/umwelt';
import type { DispositionSetting } from '@kallamo/umwelt';
const client = createClient({ file: 'ilvar.umw', create: true });
const projectId = 'ilvar';
const print = (label: string, value: unknown) => console.log(`@@${label}\n${JSON.stringify(value)}`);
const court = { aurel: 'Prince Aurel', sabine: 'Sabine, the envoy', caspar: 'Sir Caspar' };
// The world, declared once: sending the same manifest again changes nothing.
await client.worldEnsure({
project: { id: projectId, name: 'The court of Ilvar', timescale: 'act' },
entityTypes: [
{ id: 'courtier', name: 'Courtier', kind: 'agent' },
],
entities: [
...Object.entries(court).map(([id, name]) => ({ id, typeId: 'courtier', name })),
],
});
// Act 1: how each of them stands toward the others. One arrow at a time:
// what Aurel feels for Sabine says nothing about what she feels for him.
await client.dispositionSetBatch({
projectId, attemptId: 'act-1', position: at(1),
dispositions: [
{ holder: 'aurel', target: 'sabine', dimension: 'trust', value: 0.9 },
{ holder: 'aurel', target: 'sabine', dimension: 'affection', value: 0.7 },
{ holder: 'sabine', target: 'aurel', dimension: 'trust', value: 0.4 },
{ holder: 'sabine', target: 'aurel', dimension: 'affection', value: 0.6 },
{ holder: 'caspar', target: 'sabine', dimension: 'trust', value: 0.3 },
{ holder: 'caspar', target: 'sabine', dimension: 'fear', value: 0.2 },
{ holder: 'sabine', target: 'caspar', dimension: 'affection', value: 0.5 },
{ holder: 'caspar', target: 'aurel', dimension: 'trust', value: 0.95 },
],
});
// Your rules for what events do to feelings. Umwelt keeps each new value as the current one.
async function current(holder: string, target: string, tick: number) {
const now: Record<string, number> = {};
for (const record of await client.dispositions({ projectId, holder, target, at: { tick } })) {
if (record.kind === 'disposition') now[record.dimension] = Number(record.value);
}
return now;
}
const clamp = (x: number) => Math.round(Math.min(1, Math.max(0, x)) * 100) / 100;
// Act 2, the hunt: Sabine saves Caspar. Only the one who was saved changes.
const saved = await current('caspar', 'sabine', 2);
const hunt: DispositionSetting[] = [
{ holder: 'caspar', target: 'sabine', dimension: 'trust', value: clamp((saved.trust ?? 0.5) + 0.4) },
{ holder: 'caspar', target: 'sabine', dimension: 'affection', value: clamp((saved.affection ?? 0) + 0.5) },
{ holder: 'caspar', target: 'sabine', dimension: 'fear', value: 0 },
];
await client.dispositionSetBatch({ projectId, attemptId: 'act-2', position: at(2), dispositions: hunt });
// Act 3, the letters: Aurel finds Sabine has been reading his correspondence.
const betrayed = await current('aurel', 'sabine', 3);
await client.dispositionSetBatch({
projectId, attemptId: 'act-3', position: at(3),
dispositions: [
{ holder: 'aurel', target: 'sabine', dimension: 'trust', value: clamp((betrayed.trust ?? 0.5) - 0.7) },
{ holder: 'aurel', target: 'sabine', dimension: 'fear', value: clamp((betrayed.fear ?? 0) + 0.4) },
],
});
// Every arrow in every act, read back from Umwelt.
const acts: Record<string, [string, string, string, number][]> = {};
for (const act of [1, 2, 3]) {
acts[act] = [];
for (const holder of Object.keys(court)) {
for (const target of Object.keys(court)) {
if (holder === target) continue;
for (const [dimension, value] of Object.entries(await current(holder, target, act))) {
acts[act].push([holder, target, dimension, value]);
}
}
}
}
print('acts', acts);
// The same pair, both ways, after the letters.
print('both-ways', {
aurelToSabine: await current('aurel', 'sabine', 3),
sabineToAurel: await current('sabine', 'aurel', 3),
});
await client.close();using System.Text.Json;
using System.Text.Json.Serialization;
using Kallamo.Umwelt;
using var engine = UmweltEngine.Create("ilvar.umw");
const string projectId = "ilvar";
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 court = new Dictionary<string, string> { ["aurel"] = "Prince Aurel", ["sabine"] = "Sabine, the envoy", ["caspar"] = "Sir Caspar" };
// The world, declared once: sending the same manifest again changes nothing.
engine.WorldEnsure(new EnsureWorldInput
{
Project = new WorldProject { Id = projectId, Name = "The court of Ilvar", Timescale = "act" },
EntityTypes = new()
{
new WorldEntityType { Id = "courtier", Name = "Courtier", Kind = EntityKind.Agent },
},
Entities = court.Select(courtier => new WorldEntity { Id = courtier.Key, TypeId = "courtier", Name = courtier.Value }).ToList(),
});
// Act 1: how each of them stands toward the others. One arrow at a time:
// what Aurel feels for Sabine says nothing about what she feels for him.
DispositionSetting Feels(string holder, string target, string dimension, double value) =>
new() { Holder = holder, Target = target, Dimension = dimension, Value = JsonSerializer.SerializeToElement(value) };
engine.DispositionSetBatch(new SetDispositionsInput
{
ProjectId = projectId, AttemptId = "act-1", Position = Position.At(1),
Dispositions = new()
{
Feels("aurel", "sabine", "trust", 0.9), Feels("aurel", "sabine", "affection", 0.7),
Feels("sabine", "aurel", "trust", 0.4), Feels("sabine", "aurel", "affection", 0.6),
Feels("caspar", "sabine", "trust", 0.3), Feels("caspar", "sabine", "fear", 0.2),
Feels("sabine", "caspar", "affection", 0.5), Feels("caspar", "aurel", "trust", 0.95),
},
});
// Your rules for what events do to feelings. Umwelt keeps each new value as the current one.
Dictionary<string, double> Current(string holder, string target, long tick)
{
var felt = engine.Dispositions(new DispositionsPayload { ProjectId = projectId, Holder = holder, Target = target, At = new At { Tick = tick } });
var now = new Dictionary<string, double>();
foreach (var record in felt.OfType<Record.Disposition>())
{
now[record.Dimension] = record.Value.GetDouble();
}
return now;
}
double Clamp(double x) => Math.Round(Math.Min(1, Math.Max(0, x)) * 100, MidpointRounding.AwayFromZero) / 100;
double Or(Dictionary<string, double> values, string key, double fallback) => values.TryGetValue(key, out var v) ? v : fallback;
// Act 2, the hunt: Sabine saves Caspar. Only the one who was saved changes.
var saved = Current("caspar", "sabine", 2);
engine.DispositionSetBatch(new SetDispositionsInput
{
ProjectId = projectId, AttemptId = "act-2", Position = Position.At(2),
Dispositions = new()
{
Feels("caspar", "sabine", "trust", Clamp(Or(saved, "trust", 0.5) + 0.4)),
Feels("caspar", "sabine", "affection", Clamp(Or(saved, "affection", 0) + 0.5)),
Feels("caspar", "sabine", "fear", 0),
},
});
// Act 3, the letters: Aurel finds Sabine has been reading his correspondence.
var betrayed = Current("aurel", "sabine", 3);
engine.DispositionSetBatch(new SetDispositionsInput
{
ProjectId = projectId, AttemptId = "act-3", Position = Position.At(3),
Dispositions = new()
{
Feels("aurel", "sabine", "trust", Clamp(Or(betrayed, "trust", 0.5) - 0.7)),
Feels("aurel", "sabine", "fear", Clamp(Or(betrayed, "fear", 0) + 0.4)),
},
});
// Every arrow in every act, read back from Umwelt.
var acts = new Dictionary<string, List<object[]>>();
foreach (var act in new long[] { 1, 2, 3 })
{
acts[act.ToString()] = new();
foreach (var holder in court.Keys)
{
foreach (var target in court.Keys.Where(t => t != holder))
{
foreach (var (dimension, value) in Current(holder, target, act))
{
acts[act.ToString()].Add(new object[] { holder, target, dimension, value });
}
}
}
}
Print("acts", acts);
// The same pair, both ways, after the letters.
Print("both-ways", new { aurelToSabine = Current("aurel", "sabine", 3), sabineToAurel = Current("sabine", "aurel", 3) });