Developer quick start

The LivingSimSdk public API, one minimal example at a time. The first block below is run as written by QuickstartDocTests, which fails the build if this page and the code ever differ; the shorter blocks after it use only names the same test suite pins (SdkByAreaTests).

Create a world

using LivingSim.Sdk;

var sdk = new LivingSimSdk();   // seed 0
var sdk7 = new LivingSimSdk(7); // seed 7
  • seed: optional, default 0. Use 0 to int.MaxValue; a negative seed acts like its absolute value, so -7 gives the world of 7.
  • The seed is the only option. Every world runs the same tuning; there is nothing else to choose.
  • A new world is empty: no places, no characters.

The SDK by area

sdk.Characters    // add, move, personality, autonomy, feelings, habits, memory, intent, reactions, decisions
sdk.Places        // add places; how a character feels about a place
sdk.Items         // things people own: where they are, who has them, what the owner knows
sdk.Actions       // tell it what happened
sdk.Relationships // how one character regards another, and the remembered events behind it
sdk.Time          // Step and Advance
sdk.Events        // the world log
sdk.Save(); sdk.Load(json);

Sixty seconds, end to end

using LivingSim.Core;
using LivingSim.Sdk;

var sdk = new LivingSimSdk(seed: 7);
// Places first: nothing creates a place for you. Then the people in them.
sdk.Places.Add("kitchen", name: "Kitchen");
sdk.Places.Add("garden");
sdk.Characters.Add(new() { Id = "player", Place = "kitchen" });
string nora = sdk.Characters.Add(new() { Name = "Nora", Place = "kitchen" }); // no Id given: one is made and returned
sdk.Characters.Add(new() { Id = "ada", Place = "garden" });
// The player is yours: the engine never acts in their name.
sdk.Characters.SetAutonomy("player", AutonomyLevel.Controlled);
// Who Nora is: nine traits, 0 to 100, 50 when not set. An anxious person feels a threat more.
sdk.Characters.SetPersonality(nora, new PersonalitySettings { Neuroticism = 75, Agreeableness = 60 });
// Something happens: the player helps Nora in the kitchen. Impact is the calibrated severity.
sdk.Actions.By("player").Toward(nora).At("kitchen").Kind(SocialActionKind.Help)
    .Impact(ActionSeverity.Get(EventType.Help)!.Value).TryRecord();
// Read what she holds, how she feels, how she regards the player, and why.
var memory = sdk.Characters.GetMemory(nora);
var feelings = sdk.Characters.GetEmotions(nora);
var bond = sdk.Relationships.Get(nora, "player");
var why = sdk.Relationships.Explain(nora, "player");
var advice = sdk.Characters.GetReactions(nora); // ranked options for YOUR systems; nothing is executed for you
// Let the world turn: one step is one turn for everyone in it.
sdk.Time.Step();
var decision = sdk.Characters.ExplainDecision(nora); // her committed goal, the typed why, the plan
// Time passing with nobody acting: feelings fade, memories age.
sdk.Time.Advance(10);
// Save and load are byte-identical.
string save = sdk.Save();
sdk.Load(save);

Places and characters

sdk.Places.Add("kitchen_1", name: "Kitchen");
string nora = sdk.Characters.Add(new() { Name = "Nora", Place = "kitchen_1" });
sdk.Characters.Add(new() { Id = "npc_042", Name = "Nora" }); // same name, different person; no place yet
sdk.Characters.Move("npc_042", "kitchen_1");                  // where your game has put them
sdk.Characters.AddKnownPlace(nora, "garden");                 // a room she can plan to walk to
  • Ids are unique; names can repeat. Give no Id and one is made and returned.
  • The three setup calls, Places.Add, Characters.Add and Items.Add, throw on a setup mistake: a reused or blank id, a place the world does not have, an owner or holder who is not a character. Every other call is Try-first for an unknown id: a write returns false, a read returns a view with Found == false, null, or an empty list, and an action result says why. Bad arguments still throw as in any .NET API: a negative Time.Advance, an enum value that does not exist, a Load of text that is not a save.
  • A character with no place is off stage: they remember, feel and hear what is relayed to them, but they are not cast in anything and do nothing until you place them.
  • Nothing is recorded when you Move someone. The people in the new room are the ones who will see what happens to them next.

Personality

sdk.Characters.SetPersonality(nora, new PersonalitySettings { Agreeableness = 20, Honesty = 30 });
var who = sdk.Characters.GetPersonality(nora);
  • Nine traits, each 0 to 100, 50 when not set: Openness, Conscientiousness, Extraversion, Agreeableness, Neuroticism, Honesty, Temperance, Intellect, Focus.
  • They change how a character answers what happens: low Agreeableness confronts, high Neuroticism keeps away sooner, low Honesty bends a retold story.
  • SetPersonality returns false for an unknown id.

Autonomy

sdk.Characters.SetAutonomy(nora, AutonomyLevel.Autonomous);     // the default: plans, wanders, tells
sdk.Characters.SetAutonomy("barman", AutonomyLevel.Reactive);   // feels and answers, but plans nothing
sdk.Characters.SetAutonomy("player", AutonomyLevel.Controlled); // the engine never acts in their name
  • Autonomous: goals, plans and their execution, acts in passing, and its own answer to news.
  • Reactive: remembers and feels, may be cast in an act in passing, answers news; no goals, no plans.
  • Controlled: the player, or your own AI. Nothing is authored in their name.
  • Change a level at any time. Taking autonomy away abandons the plan under way.

Feelings and habits

var feelings = sdk.Characters.GetEmotions(nora);           // eleven feelings, each 0 to 100
var habits = sdk.Characters.GetHabits(nora);
sdk.Characters.SetHabit(nora, HabitKind.Gossip, 60);        // a gossip
sdk.Characters.SetHabit(nora, HabitKind.GetEven, 40, "tom"); // one who gets even with tom in particular
  • Feelings fade with time; habits form from what a character does and tilt what they choose next.
  • HabitKind: Gossip, KeepAwayFromPeople, KeepAwayFromPlaces, Befriend, GetEven, Comfort, Wander. Habits matter for Autonomous characters.

Actions

var result = sdk.Actions.TryRecord(new TryRecordSocialActionRequest(
    "tom", nora, "kitchen", SocialActionKind.Insult, ActionSeverity.Get(EventType.Insult)!.Value));
if (!result.Success) { Console.WriteLine(result.ReasonCode); } // for example target_not_present

sdk.Actions.TryRecordObservation(new TryRecordObservationRequest(nora, ObservationMeaning.Suffering, Intensity: 60, PlaceId: "kitchen")); // she saw someone suffering; ObservationMeaning is in LivingSim.Core
sdk.Actions.TrySteal("tom", "envelope");                        // a theft takes a thing; see Items

// your game's own verbs: register once, record by your word, read it back on every view of the act
sdk.Actions.RegisterVerb(new HostVerbOptions { Id = "spill-drink", Kind = SocialActionKind.Insult, Severity = SeverityTier.Light, Label = "spilled a drink on" });
sdk.Actions.By("tom").Toward(nora).Verb("spill-drink").TryRecord();   // an Insult at the Light weight; memory.HostVerbLabel == "spilled a drink on"
  • Record only what your game decided happened. The engine works out who saw it (the people in that place), what they remember and feel, and what follows.
  • A refused action says why instead of throwing, with a stable reason code: an unknown actor or target (actor_not_found, target_not_found), an impact of zero (impact_zero), a place that cannot be resolved (place_unresolved), a target who is not in that place (target_not_present), or one of your own verbs that was never registered (unknown_host_verb).
  • The actor is not checked for presence: an action names where it happened, and recording it puts the actor there. The target must already be in that place, because what happens to someone happens where they stand. The people in that place witness it.
  • ActionSeverity.Get(EventType, SeverityTier) gives the calibrated impact for a verb at Light, Standard or Severe, and the builder uses the standard one when you set none. Prefer it: the engine's thresholds are tuned to that scale. Any non-zero int is accepted and recorded as given.

Items

sdk.Items.Add(new ItemSetup("envelope", OwnerId: nora, ItemLocation.AtPlace("kitchen")));
sdk.Actions.TrySteal("tom", "envelope");              // the theft, with its witnesses
sdk.Items.Move("envelope", ItemLocation.HeldBy("tom")); // your game moves the object; tell the engine before Time.Step
var owned = sdk.Items.OwnedBy(nora);
var known = sdk.Items.GetKnowledge(nora, "envelope"); // where she believes it is, whether she has missed it, who she thinks took it
  • The engine keeps ownership, where each thing is believed to be, the theft and who saw it, a noticed loss, a search, and learning who took it. Your game keeps the physical object.

Reading a character

var memory = sdk.Characters.GetMemory(nora, max: 10); // what she holds, with source and importance
var intent = sdk.Characters.GetNextIntent(nora);      // her coarse next move
var advice = sdk.Characters.GetReactions(nora);       // ranked ReactionKind options; never executed for you
var decision = sdk.Characters.ExplainDecision(nora);  // after a Step: goal, typed why, plan, prediction
var guess = sdk.Characters.GetLastPrediction(nora);   // what she expected her last plan to bring
var regard = sdk.Relationships.Get(nora, "tom");      // affinity, trust, respect, intimacy, dependency, conflict
var evidence = sdk.Relationships.Explain(nora, "tom"); // the remembered events behind it

Every read returns copied values, never a live reference; Found == false for an unknown id.

Time

sdk.Time.Step();     // one turn for everyone; draws on the seed
sdk.Time.Step(15);   // fifteen turns
sdk.Time.Advance();  // one tick of time with nobody acting: feelings fade, memories age; no randomness
long now = sdk.Time.Now;

Each Step, in order: one character may be cast in an act in passing; owners notice whether their things are where they stand; autonomous characters choose a goal and a plan; everyone with a plan takes one step of it; owners notice again where plans have moved them; each telling under way moves one stage; expectations that came due resolve; the clock moves one tick. A step can record no events or several.

Nothing calls these for you. Call them from your game loop; in Unity, LivingSimWorld can step on a timer (stepEverySeconds), or you call Step yourself.

Events

foreach (var e in sdk.Events.Recent(20))
{
    Console.WriteLine($"{e.RecordedAtTick}: {e.EventType} {e.ActorId} -> {e.TargetId} at {e.PlaceId} ({e.Impact})");
}

Reacting without polling

sdk.Events.OnEvent        += e => { /* a log entry landed: the same WorldEventView Recent returns */ };
sdk.Events.OnGossip       += g => { /* g.TellerId told g.ListenerId a story: g.Memory, g.Distorted */ };
sdk.Events.OnDecision     += d => { /* d.CharacterId committed: d.Decision is ExplainDecision's view at that moment */ };
sdk.Events.OnPlanStep     += p => { /* p.CharacterId took step p.StepIndex (p.Step.Kind: move / exchange / wait) */ };
sdk.Events.OnPlaceChanged += m => { /* m.CharacterId: m.FromPlaceId -> m.ToPlaceId; m.ByHost when you moved them */ };

sdk.Time.Step(3);         // each turn's events are raised at the end of that turn

Events are collected while a call runs and raised just before it returns, in the engine's own order: the act in passing, then each character's new decision (in id order), then each plan step (a move's Travel event and place change first, then the step), then the tellings delivered, then the events that resolving expectations records. Step(n) raises after each turn, so a handler sees the world as that turn left it (Time.Now is one past the event's tick) and Step(n) raises what n calls of Step(1) raise. A write of your own (TryRecord, Characters.Move, Characters.Add) raises what it caused before it returns; Advance raises nothing.

Handlers read; they do not write. Every read is allowed inside a handler, Save included. A write (Step, Advance, Load, any Actions call, adding or moving a character or item, adding a place, setting a personality, autonomy, habit or known place) throws InvalidOperationException: note what to do and do it after the call returns. A handler's exception propagates out of the call with that turn already taken; the remaining handlers and turns of that call do not run, and nothing stays pending. Subscribers change no outcome and are not saved: the same seed gives the same world and the same event sequence with or without them, Load replays nothing, and the subscriptions survive it.

Determinism, save and load

static string Run(int seed)
{
    var sdk = new LivingSimSdk(seed);
    sdk.Places.Add("kitchen");
    sdk.Characters.Add(new() { Id = "player", Place = "kitchen" });
    sdk.Characters.Add(new() { Id = "nora", Place = "kitchen" });
    sdk.Time.Step(15);
    return sdk.Save();
}

Console.WriteLine(Run(7) == Run(7)); // True, on every machine
  • Only Time.Step draws on the seed: who is cast in an act in passing, toward whom, where people wander, how a dishonest teller bends a story. Setup, recorded actions, Advance and reads never do.
  • Save() is a JSON string holding everything, the seed and the random stream's position included. Load(json) on a facade resumes byte for byte: the next Save() equals the one you loaded, and the next Step is the step the saved world would have taken.