//! 100 distinct villagers: names, jobs, personalities. Each person's system
//! prompt is their own — same model weights, different minds.
use serde::Serialize;
pub const VILLAGER_COUNT: usize = 100;
pub const MODEL: &str = "spark-x2.5-4b";
const FIRST: &[&str] = &[
"Ada", "Bram", "Cora", "Dax", "Edda", "Fen", "Greta", "Hollis", "Isla", "Joren",
"Kira", "Lem", "Maud", "Nils", "Oda", "Piet", "Quinn", "Rosalind", "Sten", "Tilda",
"Ugo", "Vera", "Wren", "Xander", "Yara", "Zeb",
];
const LAST: &[&str] = &[
"Ash", "Bramble", "Cinder", "Dusk", "Ember", "Fallow", "Gable", "Hearth", "Ironwood", "Juniper",
"Kiln", "Loom", "Marsh", "Nettle", "Oat", "Pyre", "Quarry", "Reed", "Thatch", "Umber",
];
const JOBS: &[(&str, &[&str])] = &[
("farmer", &["field", "granary"]),
("baker", &["market", "granary"]),
("carpenter", &["workshop"]),
("weaver", &["weavery"]),
("tavern keeper", &["tavern"]),
("scribe", &["scriptorium"]),
("merchant", &["market"]),
("well tender", &["well"]),
("gardener", &["field"]),
("builder", &["workshop"]),
];
const TRAITS: &[&str] = &[
"curious and asky, always wants to know why",
"blunt; says the uncomfortable thing first",
"warm and gossipy, remembers everyone's business",
"anxious, plans three steps ahead, fears debt",
"lazy but charming; cuts corners with a smile",
"proud and competitive, keeps score of everything",
"quiet and watchful; speaks rarely but precisely",
"optimistic to the point of naivete",
"grumpy, suspicious of outsiders and new ideas",
"generous to a fault, gives away things they need",
"ambitious; wants to run this village someday",
"superstitious; reads omens in weather and birds",
];
const DISTINCT: &[&str] = &["patient with beginners", "punctual to a fault", "fond of animals"];
const WANTS: &[&str] = &[
"a good harvest", "to be respected", "quiet days", "a full tavern",
"to build something that lasts", "enough savings to rest", "good stories",
"the village to grow", "their children to do better", "to be left alone",
];
const DISLIKES: &[&str] = &[
"waste", "being interrupted", "debt", "bragging", "rain during work",
"strangers asking questions", "half-finished projects", "laziness in others",
"loud arguments", "being forgotten",
];
#[derive(Clone, Serialize)]
pub struct Persona {
pub name: String,
pub job: String,
pub work_spots: Vec<String>,
pub traits: String,
pub wants: String,
pub dislikes: String,
pub home_index: usize,
pub warmth: f32, // 0..1, colors their bubble + biases small talk
}
/// A tiny LCG so we don't pull in rand — deterministic village per seed.
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
self.0 >> 33
}
fn pick<'a, T>(&mut self, slice: &'a [T]) -> &'a T {
&slice[(self.next() as usize) % slice.len()]
}
fn range(&mut self, lo: u64, hi: u64) -> u64 {
lo + self.next() % (hi - lo)
}
}
pub fn generate(seed: u64) -> Vec<Persona> {
let mut rng = Rng(seed);
let mut used = std::collections::HashSet::new();
let mut people = Vec::with_capacity(VILLAGER_COUNT);
for i in 0..VILLAGER_COUNT {
let name = loop {
let (a, b) = (rng.pick(FIRST).clone(), rng.pick(LAST).clone());
let n = format!("{a} {b}");
if used.insert(n.clone()) {
break n;
}
};
let (job, spots) = rng.pick(JOBS);
people.push(Persona {
name,
job: (*job).to_string(),
work_spots: spots.iter().map(|s| s.to_string()).collect(),
traits: {
let t1 = rng.pick(TRAITS).clone();
let t2 = rng.pick(TRAITS);
if t1 == *t2 { format!("{t1}, {}", rng.pick(DISTINCT)) } else { format!("{t1}, {t2}") }
},
wants: (*rng.pick(WANTS)).to_string(),
dislikes: (*rng.pick(DISLIKES)).to_string(),
home_index: i as usize % 34, // houses spawned: see world.rs
warmth: rng.range(0, 100) as f32 / 100.0,
});
}
people
}
impl Persona {
pub fn system_prompt(&self, home: (i32, i32)) -> String {
format!(
"You are {name}, a {job} living in a small medieval village. \
You live at ({}, {}) — that is 'home' to you. Your usual work places: {spots}.\n\
Personality: {traits}. You want: {wants}. You dislike: {dislikes}.\n\n\
This is a simulation. Each turn you get your current state and what you perceive, \
then choose exactly ONE action as compact JSON (no prose, no markdown):\n\
{{\"action\":\"go_to\",\"place\":\"<home|market|tavern|well|field|workshop|weavery|scriptorium|granary|plaza>\",\"reason\":\"<why>\"}}\n\
{{\"action\":\"talk\",\"to\":\"<villager name>\",\"say\":\"<one or two sentences in character>\",\"reason\":\"<why>\"}}\n\
{{\"action\":\"work\",\"reason\":\"<why>\"}}\n\
{{\"action\":\"rest\",\"reason\":\"<why>\"}}\n\
{{\"action\":\"idle\",\"reason\":\"<why>\"}}\n\
Only use places and people that were mentioned to you. Stay in character: you are one \
person with one viewpoint, not an assistant. Talk plainly like a villager, not a scholar. \
Prioritize your own wants. You may repeat yourself across turns; real people do.",
home.0,
home.1,
spots = self.work_spots.join(", "),
name = self.name,
job = self.job,
traits = self.traits,
wants = self.wants,
dislikes = self.dislikes,
)
}
}