From 79ed7a335b4b8519c34e853bb3d5512188d9f014 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Fri, 25 Sep 2026 07:45:26 +0300 Subject: [PATCH] feat(packages): ludic.session - a party's roster (slot 0 a port, the others as reported), the roles and who owns the world, the shared night, votes and a world-changing act asking the party; joins, leaves, roles, votes and the night as facts Co-Authored-By: Claude Opus 5.5 --- packages/ludic.session/README.md | 60 ++++++ packages/ludic.session/index.ludic | 13 ++ packages/ludic.session/near.ludic | 30 +++ packages/ludic.session/night.ludic | 43 +++++ packages/ludic.session/package.ludic | 5 + packages/ludic.session/roles.ludic | 46 +++++ packages/ludic.session/roster.ludic | 76 ++++++++ packages/ludic.session/state.ludic | 89 +++++++++ .../ludic.session/tests/session_test.ludic | 177 ++++++++++++++++++ packages/ludic.session/vote_state.ludic | 10 + packages/ludic.session/votes.ludic | 111 +++++++++++ packages/ludic.session/where.ludic | 47 +++++ 12 files changed, 707 insertions(+) create mode 100644 packages/ludic.session/README.md create mode 100644 packages/ludic.session/index.ludic create mode 100644 packages/ludic.session/near.ludic create mode 100644 packages/ludic.session/night.ludic create mode 100644 packages/ludic.session/package.ludic create mode 100644 packages/ludic.session/roles.ludic create mode 100644 packages/ludic.session/roster.ludic create mode 100644 packages/ludic.session/state.ludic create mode 100644 packages/ludic.session/tests/session_test.ludic create mode 100644 packages/ludic.session/vote_state.ludic create mode 100644 packages/ludic.session/votes.ludic create mode 100644 packages/ludic.session/where.ludic diff --git a/packages/ludic.session/README.md b/packages/ludic.session/README.md new file mode 100644 index 00000000..6dc31662 --- /dev/null +++ b/packages/ludic.session/README.md @@ -0,0 +1,60 @@ +# ludic.session + +Who is in a party and who runs the world, whatever carries its messages: the roster, the roles, +the shared night, votes, and "a world-changing act asks the party". It sends nothing - what the +other machines have to hear comes out as facts, and the game puts them on its wire. Uses +[`ludic.base`](../ludic.base/README.md) and nothing else. + +```ludic +import "ludic.session" +``` + +## The roster + +Slot 0 is this machine's player, read live through the `SessionLocal` port; slots 1 to +`PLY_MAX - 1` are remote players, holding what their own machines reported (where they stand, +their heading and speed, how much their rest speeds the clock, whether they have turned in, their +name). World code asks the roster, never "the player": `for p in 0 .. PLY_MAX { if ply_on(p) ... +ply_x(p) ... }`, or `ply_near(x, z)` and `ply_near_i()`. + +```ludic +port SessionLocal { + x, y, z: fn() -> float # required + yaw, speed: fn() -> float (0), rest: fn() -> float (1), name: fn() -> string ("") +} +``` + +## Roles + +`SES_SOLO` owns the world and has no company; `SES_HOST` owns it and has company; `SES_GUEST` +owns only its own player. `ses_owns_world()`, `ses_live()`, `ses_night_passes()` (alone, lying +down ends the night). The transport says what this machine became (`ses_set_role`). + +## API + +| | | +| --- | --- | +| `ply_init()`, `ply_join(pid) -> slot`, `ply_leave(p)`, `ply_leave_all()` | the roster (-1: full, or pid 0) | +| `ply_report(p, x, y, z, yaw, speed)`, `ply_report_rest(p, rest, sleeping)`, `ply_report_name(p, name)` | what a remote machine said | +| `ply_on`, `ply_is_local`, `ply_slot_of(pid)`, `ply_count`, `ply_pid`, `ply_name`, `ply_x/y/z/yaw/speed/rest(p)`, `ply_sleeping(p)` | who and where | +| `ply_near(x, z)`, `ply_near_d2`, `ply_near_i()`, `ply_dist(p, x, z)` | the nearest player | +| `ses_role()`, `ses_set_role(r)`, `ses_owns_world()`, `ses_live()`, `ses_hosting()`, `ses_guest()`, `ses_night_passes()` | roles | +| `ses_set_party(max, open)`, `ses_set_open(b)`, `ses_max()`, `ses_open()`, `ses_room_for_one()` | the party a host allows | +| `ses_sleep(on) -> bool`, `ses_sleeping()`, `ses_sleep_ready(p, on)`, `ses_night_check()`, `ses_night_passed()`, `ses_sleep_waiting()` | the shared night: false alone; the host counts | +| `ses_may(kind, param, text) -> SES_MAY_GO / _ASKED / _BUSY / _HOSTS` | a world-changing act | +| `ses_vote_open`, `ses_vote_cast`, `ses_vote_heard(id, yes)`, `ses_vote_told(id, kind, text)`, `ses_vote_ended(id, passed)`, `ses_vote_tick(dt)` | a majority of the players, the host included, within thirty seconds | +| `ses_vote_live/id/kind/text/left/yes/no/mine/passed()` | the vote as it stands | +| `ses_reset()` | everyone else gone, alone, nothing asked, nobody asleep | +| `ses_facts() -> Queue` | `SES_JOINED` / `SES_LEFT` (slot, pid), `SES_ROLE` (role), `SES_VOTE_OPENED`, `SES_VOTE_CAST` (a guest's answer to send), `SES_VOTE_RESOLVED` (yes: passed), `SES_NIGHT` (the host: everyone is in), `SES_SLEEP` (this player turned in or got up) | + +It is not a `System`: a party exists before any world does (a join from the title), so the game +calls `ses_vote_tick` and drains `ses_facts()` in its own frame. + +## Tests + +```bash +ludic test packages/ludic.session +``` + +A fake local player: the roster filling and emptying, the nearest player, roles, the party's size, +the night, votes passing, failing and timing out, and a guest's side of each. diff --git a/packages/ludic.session/index.ludic b/packages/ludic.session/index.ludic new file mode 100644 index 00000000..690bbc3c --- /dev/null +++ b/packages/ludic.session/index.ludic @@ -0,0 +1,13 @@ +# ludic.session - a party's roster and rules, whatever carries its messages: slot 0 is this +# machine's player (a port), the others are what their own machines reported +module ludic_session uses ludic_base +numbers float +import "ludic.base" +import "state.ludic" +import "roster.ludic" +import "where.ludic" +import "near.ludic" +import "roles.ludic" +import "night.ludic" +import "votes.ludic" +import "vote_state.ludic" diff --git a/packages/ludic.session/near.ludic b/packages/ludic.session/near.ludic new file mode 100644 index 00000000..056cd203 --- /dev/null +++ b/packages/ludic.session/near.ludic @@ -0,0 +1,30 @@ +# near.ludic - the world asks who is nearest, never "the player": an animal notices whoever is +# closest, a camper keeps clear of everyone +export function ply_near_d2(x: float, z: float) -> float { + session__near_i = 0 + var best = 0.0 + var first = true + for p in 0 .. PLY_MAX { + if not ply_on(p) { continue } + let dx = x - ply_x(p) + let dz = z - ply_z(p) + let d2 = dx * dx + dz * dz + if first or d2 < best { + best = d2 + session__near_i = p + first = false + } + } + return best +} + +# the distance to the nearest player, in metres; ply_near_i() says which +export function ply_near(x: float, z: float) -> float { return Math.sqrt(ply_near_d2(x, z)) } + +export function ply_near_i() -> int { return session__near_i } + +export function ply_dist(p: int, x: float, z: float) -> float { + let dx = x - ply_x(p) + let dz = z - ply_z(p) + return Math.sqrt(dx * dx + dz * dz) +} diff --git a/packages/ludic.session/night.ludic b/packages/ludic.session/night.ludic new file mode 100644 index 00000000..d13862c8 --- /dev/null +++ b/packages/ludic.session/night.ludic @@ -0,0 +1,43 @@ +# night.ludic - with company the night passes when everyone has turned in. Lying down marks this +# player ready and getting up takes it back; the host counts, and when the last one is in it hears +# SES_NIGHT and passes the night for everyone. +var session__me_sleeping: bool = false + +export function ses_sleeping() -> bool { return session__me_sleeping } + +# this player turns in (or gets up): false alone, where a bed simply sleeps +export function ses_sleep(on: bool) -> bool { + if not ses_live() { return false } + if on == session__me_sleeping { return on } + session__me_sleeping = on + let f = session__fact(SES_SLEEP, 0, 0) + f.yes = on + if on { ses_night_check() } + return true +} + +# the host: a guest said it turned in, or got up +export function ses_sleep_ready(p: int, on: bool) -> void { + if p <= 0 or not ply_on(p) { return } + session__slot[p].ready = on + ses_night_check() +} + +# the host: is everyone in? Then the night is due, and everyone's readiness starts again +export function ses_night_check() -> void { + if session__role != SES_HOST or not session__me_sleeping { return } + for p in 1 .. PLY_MAX { if ply_on(p) and not session__slot[p].ready { return } } + for p in 1 .. PLY_MAX { session__slot[p].ready = false } + session__fact(SES_NIGHT, -1, 0) +} + +# the night went by on this machine: awake again +export function ses_night_passed() -> void { session__me_sleeping = false } + +# how many are still up, this player included +export function ses_sleep_waiting() -> int { + var n = 0 + for p in 1 .. PLY_MAX { if ply_on(p) and not session__slot[p].sleeping { n += 1 } } + if not session__me_sleeping { n += 1 } + return n +} diff --git a/packages/ludic.session/package.ludic b/packages/ludic.session/package.ludic new file mode 100644 index 00000000..6c1d704a --- /dev/null +++ b/packages/ludic.session/package.ludic @@ -0,0 +1,5 @@ +# ludic.session - who is in a party and who runs the world: the roster, the roles, the shared +# night, votes and "a world-changing act asks the party". Transport-free. See README.md. +package "ludic.session" +version "0.1.0" +kind source diff --git a/packages/ludic.session/roles.ludic b/packages/ludic.session/roles.ludic new file mode 100644 index 00000000..d5b42f19 --- /dev/null +++ b/packages/ludic.session/roles.ludic @@ -0,0 +1,46 @@ +# roles.ludic - who owns the world. Alone, this machine does; a host does and has company; a +# guest owns only its own player. The transport says what this machine became. +export function ses_role() -> int { return session__role } + +export function ses_set_role(r: int) -> void { + if r == session__role { return } + session__role = r + session__fact(SES_ROLE, -1, 0) +} + +# this machine runs the world: the clock, the weather, the animals, the people +export function ses_owns_world() -> bool { return session__role != SES_GUEST } + +# somebody else is in this world, so nothing this player opens may stop it +export function ses_live() -> bool { return session__role != SES_SOLO } + +export function ses_hosting() -> bool { return session__role == SES_HOST } + +export function ses_guest() -> bool { return session__role == SES_GUEST } + +# whether this player lying down ends the night; alone, it always does +export function ses_night_passes() -> bool { return ses_owns_world() and not ses_live() } + +# the party a host allows (2..PLY_MAX) and whether it takes new players +export function ses_set_party(max: int, open: bool) -> void { + session__max = max + if session__max < 2 { session__max = 2 } + if session__max > PLY_MAX { session__max = PLY_MAX } + session__open = open +} + +export function ses_set_open(open: bool) -> void { session__open = open } +export function ses_max() -> int { return session__max } +export function ses_open() -> bool { return session__open } + +# may one more join: open, and not full +export function ses_room_for_one() -> bool { return session__open and ply_count() < session__max } + +# the session is over: everyone else gone, alone again, nothing asked and nobody asleep +export function ses_reset() -> void { + ply_leave_all() + ses_set_role(SES_SOLO) + session__me_sleeping = false + session__vote_t = 0.0 + session__vote_mine = -1 +} diff --git a/packages/ludic.session/roster.ludic b/packages/ludic.session/roster.ludic new file mode 100644 index 00000000..f0865d11 --- /dev/null +++ b/packages/ludic.session/roster.ludic @@ -0,0 +1,76 @@ +# roster.ludic - who is here. Slot 0 is this machine's player and reads the SessionLocal port +# live; slots 1.. are remote players, filled by what their machines report. +export function ply_init() -> void { session__init() } + +export function ply_on(p: int) -> bool { + if p == 0 { return true } + if session__slot == null or p < 0 or p >= PLY_MAX { return false } + return session__slot[p].on +} + +export function ply_is_local(p: int) -> bool { return p == 0 } + +# the roster slot holding network player `pid`, or -1 +export function ply_slot_of(pid: int) -> int { + if session__slot == null or pid <= 0 { return -1 } + for p in 1 .. PLY_MAX { if session__slot[p].on and session__slot[p].pid == pid { return p } } + return -1 +} + +export function ply_count() -> int { + var n = 1 + for p in 1 .. PLY_MAX { if ply_on(p) { n += 1 } } + return n +} + +# a remote player comes in, standing where this one stands until they say: the first free +# slot, or -1 when the party is full +export function ply_join(pid: int) -> int { + session__init() + if pid <= 0 { return -1 } # 0 is this machine's player + for p in 1 .. PLY_MAX { + if session__slot[p].on { continue } + let r = new PartyMember + r.on = true + r.pid = pid + r.x = SessionLocal.x() + r.y = SessionLocal.y() + r.z = SessionLocal.z() + session__slot[p] = r + session__fact(SES_JOINED, p, pid) + return p + } + return -1 +} + +export function ply_leave(p: int) -> void { + if p <= 0 or not ply_on(p) { return } + let r = session__slot[p] + r.on = false + r.ready = false + session__fact(SES_LEFT, p, r.pid) +} + +export function ply_leave_all() -> void { for p in 1 .. PLY_MAX { ply_leave(p) } } + +# what a remote player's machine reported this tick +export function ply_report(p: int, x: float, y: float, z: float, yaw: float, speed: float) -> void { + if p <= 0 or not ply_on(p) { return } + let r = session__slot[p] + r.x = x + r.y = y + r.z = z + r.yaw = yaw + r.speed = speed +} + +export function ply_report_rest(p: int, rest: float, sleeping: bool) -> void { + if p <= 0 or not ply_on(p) { return } + session__slot[p].rest = rest + session__slot[p].sleeping = sleeping +} + +export function ply_report_name(p: int, name: string) -> void { + if p <= 0 or not ply_on(p) { return } + session__slot[p].name = name +} diff --git a/packages/ludic.session/state.ludic b/packages/ludic.session/state.ludic new file mode 100644 index 00000000..35445521 --- /dev/null +++ b/packages/ludic.session/state.ludic @@ -0,0 +1,89 @@ +# state.ludic - a roster slot, the local player's port, the roles, and the facts the game drains +export const PLY_MAX: int = 12 # a party: this player and eleven more + +export const SES_SOLO: int = 0 +export const SES_HOST: int = 1 +export const SES_GUEST: int = 2 + +# what happened: a player in or out (slot, pid), the role changed (role), a vote opened, this +# guest's own vote to send, a vote decided (id, kind, param, passed, text), everyone in for the +# night (the host), this player turned in or got up (yes) +export const SES_JOINED: int = 0 +export const SES_LEFT: int = 1 +export const SES_ROLE: int = 2 +export const SES_VOTE_OPENED: int = 3 +export const SES_VOTE_CAST: int = 4 +export const SES_VOTE_RESOLVED: int = 5 +export const SES_NIGHT: int = 6 +export const SES_SLEEP: int = 7 + +export property SessionFact { + what: int = 0, + slot: int = -1, + pid: int = 0, + role: int = 0, + id: int = 0, + kind: int = 0, + param: int = 0, + yes: bool = false, + text: string = "" +} + +# a remote player, as its own machine reported it +property PartyMember { + on: bool = false, + pid: int = 0, # the session's id for them (0 is never a remote player) + name: string = "", + x: float = 0.0, + y: float = 0.0, + z: float = 0.0, + yaw: float = 0.0, + speed: float = 0.0, + rest: float = 1.0, # how much faster their rest runs the clock (1 on their feet) + sleeping: bool = false, # turned in, as their body says + ready: bool = false # turned in, as they told the host (the host's count for the night) +} + +# this machine's player, read live +export port SessionLocal { + x: fn() -> float + y: fn() -> float + z: fn() -> float + yaw: fn() -> float = fn session__zero + speed: fn() -> float = fn session__zero + rest: fn() -> float = fn session__one + name: fn() -> string = fn session__nameless +} + +function session__zero() -> float { return 0.0 } +function session__one() -> float { return 1.0 } +function session__nameless() -> string { return "" } + +var session__slot: []PartyMember = null +var session__role: int = 0 +var session__max: int = 4 +var session__open: bool = true +var session__near_i: int = 0 +var session__facts: Queue = null + +export function ses_facts() -> Queue { + if session__facts == null { session__facts = queue_new("session.facts") } + return session__facts +} + +function session__fact(what: int, slot: int, pid: int) -> SessionFact { + let f = new SessionFact + f.what = what + f.slot = slot + f.pid = pid + f.role = session__role + q_push(ses_facts(), f) + return f +} + +function session__init() -> void { + if session__slot != null { return } + session__slot = new []PartyMember + for i in 0 .. PLY_MAX { push(session__slot, new PartyMember) } + session__slot[0].on = true +} diff --git a/packages/ludic.session/tests/session_test.ludic b/packages/ludic.session/tests/session_test.ludic new file mode 100644 index 00000000..954efe43 --- /dev/null +++ b/packages/ludic.session/tests/session_test.ludic @@ -0,0 +1,177 @@ +# session_test.ludic - the roster, the roles, the nearest player, the shared night and votes, +# with a fake local player +import "ludic.session" +import "ludic.base" +program SessionTest { + numbers float + var me_x: float = 0.0 + var me_z: float = 0.0 + function fx() -> float { return me_x } + function fy() -> float { return 0.0 } + function fz() -> float { return me_z } + function fname() -> string { return "me" } + bind SessionLocal { x: fn fx, y: fn fy, z: fn fz, name: fn fname } + + function whats() -> []int { + let fs = q_drain(ses_facts()) + let out = new []int + for i in 0 .. len(fs) { push(out, fs[i].what) } + return out + } + + function last_of(what: int) -> SessionFact { + let fs = q_drain(ses_facts()) + var f: SessionFact = null + for i in 0 .. len(fs) { if fs[i].what == what { f = fs[i] } } + return f + } + + # a host with n guests, pids 2.., facts drained + function party(n: int) -> void { + ply_init() + ses_set_role(SES_HOST) + for i in 0 .. n { ply_join(2 + i) } + q_clear(ses_facts()) + } + + test "slot 0 is the local player, read live; others join at its feet and leave" { + ply_init() + me_x = 5.0 + expect(ply_on(0)) + expect_near(ply_x(0), 5.0, 0.001) + expect(ply_name(0) == "me") + let p = ply_join(7) + expect_eq(p, 1) + expect_near(ply_x(p), 5.0, 0.001) + expect_eq(ply_slot_of(7), 1) + expect_eq(ply_count(), 2) + expect_eq(ply_join(0), -1) + ply_leave(p) + expect_eq(ply_slot_of(7), -1) + expect_eq(ply_count(), 1) + let w = whats() + expect_eq(len(w), 2) + expect_eq(w[0], SES_JOINED) + expect_eq(w[1], SES_LEFT) + } + + test "the roster fills at twelve" { + ply_init() + for i in 0 .. PLY_MAX - 1 { expect(ply_join(10 + i) > 0) } + expect_eq(ply_join(99), -1) + expect_eq(ply_count(), PLY_MAX) + } + + test "the nearest player, whoever it is" { + ply_init() + me_x = 100.0 + let p = ply_join(2) + ply_report(p, 3.0, 0.0, 4.0, 0.0, 1.0) + expect_near(ply_near(0.0, 0.0), 5.0, 0.001) + expect_eq(ply_near_i(), p) + expect_near(ply_dist(0, 100.0, 0.0), 0.0, 0.001) + } + + test "roles: alone owns the world, a guest does not, and a change is a fact" { + ply_init() + expect(ses_owns_world()) + expect(not ses_live()) + expect(ses_night_passes()) + ses_set_role(SES_GUEST) + expect(not ses_owns_world()) + expect(ses_live()) + expect(not ses_night_passes()) + let f = last_of(SES_ROLE) + expect_eq(f.role, SES_GUEST) + ses_set_role(SES_HOST) + expect(ses_owns_world()) + expect(not ses_night_passes()) + } + + test "the party's size and door" { + ses_set_party(20, true) + expect_eq(ses_max(), PLY_MAX) + ses_set_party(3, true) + party(1) + expect(ses_room_for_one()) + ply_join(9) + expect(not ses_room_for_one()) + ses_set_party(12, false) + expect(not ses_room_for_one()) + } + + test "the night passes when the host and every guest have turned in" { + party(2) + expect(ses_sleep(true)) + expect_eq(ses_sleep_waiting(), 2) + ses_sleep_ready(1, true) + expect(last_of(SES_NIGHT) == null) + ses_sleep_ready(2, true) + expect(last_of(SES_NIGHT) != null) + ses_night_passed() + expect(not ses_sleeping()) + } + + test "a guest's turning in is a fact for the host to hear; alone a bed just sleeps" { + ply_init() + expect(not ses_sleep(true)) + ses_set_role(SES_GUEST) + q_clear(ses_facts()) + expect(ses_sleep(true)) + let f = last_of(SES_SLEEP) + expect(f.yes) + ses_sleep(false) + expect(not ses_sleeping()) + } + + test "a vote passes on a majority, the host's yes counted" { + party(2) + expect_eq(ses_may(4, 9, "set out"), SES_MAY_ASKED) + expect(ses_vote_live()) + expect_eq(ses_may(4, 9, "set out"), SES_MAY_BUSY) + ses_vote_heard(ses_vote_id(), true) + let f = last_of(SES_VOTE_RESOLVED) + expect(f.yes) + expect_eq(f.param, 9) + expect(f.text == "set out") + expect(not ses_vote_live()) + } + + test "a vote fails on half saying no, or when the time runs out" { + party(1) + ses_vote_open(1, 0, "a") + ses_vote_heard(ses_vote_id(), false) + expect(not last_of(SES_VOTE_RESOLVED).yes) + party(3) + ses_vote_open(1, 0, "b") + ses_vote_tick(31.0) + let f = last_of(SES_VOTE_RESOLVED) + expect(f != null) + expect(not f.yes) + } + + test "alone it may go; a guest may not, and its vote goes to the host" { + ply_init() + expect_eq(ses_may(1, 0, "x"), SES_MAY_GO) + ses_set_role(SES_GUEST) + expect_eq(ses_may(1, 0, "x"), SES_MAY_HOSTS) + ses_vote_told(5, 1, "x") + expect(ses_vote_live()) + q_clear(ses_facts()) + ses_vote_cast(true) + let f = last_of(SES_VOTE_CAST) + expect_eq(f.id, 5) + expect(f.yes) + ses_vote_ended(5, true) + expect_eq(ses_vote_passed(), 1) + } + + test "a reset leaves everyone and stands alone" { + party(2) + ses_sleep(true) + ses_reset() + expect_eq(ply_count(), 1) + expect_eq(ses_role(), SES_SOLO) + expect(not ses_sleeping()) + } +} diff --git a/packages/ludic.session/vote_state.ludic b/packages/ludic.session/vote_state.ludic new file mode 100644 index 00000000..2c9c6b64 --- /dev/null +++ b/packages/ludic.session/vote_state.ludic @@ -0,0 +1,10 @@ +# vote_state.ludic - the vote as it stands +export function ses_vote_live() -> bool { return session__vote_t > 0.0 } +export function ses_vote_id() -> int { return session__vote_id } +export function ses_vote_kind() -> int { return session__vote_kind } +export function ses_vote_text() -> string { return session__vote_text } +export function ses_vote_left() -> float { return session__vote_t } +export function ses_vote_yes() -> int { return session__vote_yes } +export function ses_vote_no() -> int { return session__vote_no } +export function ses_vote_mine() -> int { return session__vote_mine } +export function ses_vote_passed() -> int { return session__vote_passed } diff --git a/packages/ludic.session/votes.ludic b/packages/ludic.session/votes.ludic new file mode 100644 index 00000000..85c67170 --- /dev/null +++ b/packages/ludic.session/votes.ludic @@ -0,0 +1,111 @@ +# votes.ludic - what changes the world for everyone is the host's to put to the party and the +# party's to agree: a majority of the players, the host included, within thirty seconds. A guest +# only hears of a vote (ses_vote_told), casts its own (SES_VOTE_CAST) and hears how it ended. +export const SES_MAY_GO: int = 0 # alone, or nobody else here: go ahead +export const SES_MAY_ASKED: int = 1 # the party has been asked; the act waits for SES_VOTE_RESOLVED +export const SES_MAY_BUSY: int = 2 # a vote is already open +export const SES_MAY_HOSTS: int = 3 # a guest: it is for the host to ask + +const SESSION_VOTE_SECS: float = 30.0 + +var session__vote_id: int = 0 +var session__vote_kind: int = 0 +var session__vote_param: int = 0 +var session__vote_text: string = "" +var session__vote_t: float = 0.0 +var session__vote_yes: int = 0 +var session__vote_no: int = 0 +var session__vote_mine: int = -1 # -1 not voted, 0 no, 1 yes +var session__vote_passed: int = -1 # the last vote: -1 none, 0 failed, 1 passed + +# a world-changing act: may it go ahead now, or does the party have to be asked first? +export function ses_may(kind: int, param: int, text: string) -> int { + if not ses_live() { return SES_MAY_GO } + if session__role == SES_GUEST { return SES_MAY_HOSTS } + if ply_count() <= 1 { return SES_MAY_GO } + if not ses_vote_open(kind, param, text) { return SES_MAY_BUSY } + return SES_MAY_ASKED +} + +# the host puts it to the party, voting yes itself +export function ses_vote_open(kind: int, param: int, text: string) -> bool { + if session__role != SES_HOST or ses_vote_live() { return false } + session__vote_id += 1 + session__vote_kind = kind + session__vote_param = param + session__vote_text = text + session__vote_t = SESSION_VOTE_SECS + session__vote_yes = 1 + session__vote_no = 0 + session__vote_mine = 1 + session__vote_passed = -1 + let f = session__vote_fact(SES_VOTE_OPENED) + f.yes = true + session__vote_check() + return true +} + +# this player's answer; a guest's goes to the host as SES_VOTE_CAST +export function ses_vote_cast(yes: bool) -> void { + if not ses_vote_live() or session__vote_mine >= 0 { return } + if yes { session__vote_mine = 1 } else { session__vote_mine = 0 } + if session__role == SES_GUEST { + let f = session__vote_fact(SES_VOTE_CAST) + f.yes = yes + return + } + ses_vote_heard(session__vote_id, yes) +} + +# the host: a player's answer to vote `id` +export function ses_vote_heard(id: int, yes: bool) -> void { + if session__role != SES_HOST or id != session__vote_id or not ses_vote_live() { return } + if yes { session__vote_yes += 1 } else { session__vote_no += 1 } + session__vote_check() +} + +# a guest: the host opened vote `id`, or ended it +export function ses_vote_told(id: int, kind: int, text: string) -> void { + session__vote_id = id + session__vote_kind = kind + session__vote_text = text + session__vote_t = SESSION_VOTE_SECS + session__vote_mine = -1 +} + +export function ses_vote_ended(id: int, passed: bool) -> void { + if id != session__vote_id { return } + session__vote_t = 0.0 + if passed { session__vote_passed = 1 } else { session__vote_passed = 0 } +} + +# the vote's thirty seconds; on the host, running out decides it +export function ses_vote_tick(dt: float) -> void { + if not ses_vote_live() { return } + session__vote_t = session__vote_t - dt + if session__role == SES_HOST and not ses_vote_live() { session__vote_finish() } +} + +function session__vote_check() -> void { + let n = ply_count() + let yes = session__vote_yes + let no = session__vote_no + if yes * 2 > n or no * 2 >= n or yes + no >= n { session__vote_finish() } +} + +function session__vote_finish() -> void { + let passed = session__vote_yes * 2 > ply_count() + session__vote_t = 0.0 + if passed { session__vote_passed = 1 } else { session__vote_passed = 0 } + let f = session__vote_fact(SES_VOTE_RESOLVED) + f.yes = passed +} + +function session__vote_fact(what: int) -> SessionFact { + let f = session__fact(what, -1, 0) + f.id = session__vote_id + f.kind = session__vote_kind + f.param = session__vote_param + f.text = session__vote_text + return f +} diff --git a/packages/ludic.session/where.ludic b/packages/ludic.session/where.ludic new file mode 100644 index 00000000..142782ee --- /dev/null +++ b/packages/ludic.session/where.ludic @@ -0,0 +1,47 @@ +# where.ludic - who and where each player is; slot 0 answers through SessionLocal, live +export function ply_pid(p: int) -> int { + if p <= 0 or not ply_on(p) { return 0 } + return session__slot[p].pid +} + +export function ply_name(p: int) -> string { + if p == 0 { return SessionLocal.name() } + if not ply_on(p) { return "" } + return session__slot[p].name +} + +export function ply_x(p: int) -> float { + if p == 0 { return SessionLocal.x() } + return session__slot[p].x +} + +export function ply_y(p: int) -> float { + if p == 0 { return SessionLocal.y() } + return session__slot[p].y +} + +export function ply_z(p: int) -> float { + if p == 0 { return SessionLocal.z() } + return session__slot[p].z +} + +export function ply_yaw(p: int) -> float { + if p == 0 { return SessionLocal.yaw() } + return session__slot[p].yaw +} + +export function ply_speed(p: int) -> float { + if p == 0 { return SessionLocal.speed() } + return session__slot[p].speed +} + +# how much a player's rest speeds the clock: their seat's or the ground's, 1 on their feet +export function ply_rest(p: int) -> float { + if p == 0 { return SessionLocal.rest() } + return session__slot[p].rest +} + +export function ply_sleeping(p: int) -> bool { + if p == 0 { return session__me_sleeping } + return ply_on(p) and session__slot[p].sleeping +}