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 <noreply@anthropic.com>
This commit is contained in:
parent
12172d7b75
commit
79ed7a335b
12 changed files with 707 additions and 0 deletions
60
packages/ludic.session/README.md
Normal file
60
packages/ludic.session/README.md
Normal file
|
|
@ -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<SessionFact>` | `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.
|
||||
13
packages/ludic.session/index.ludic
Normal file
13
packages/ludic.session/index.ludic
Normal file
|
|
@ -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"
|
||||
30
packages/ludic.session/near.ludic
Normal file
30
packages/ludic.session/near.ludic
Normal file
|
|
@ -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)
|
||||
}
|
||||
43
packages/ludic.session/night.ludic
Normal file
43
packages/ludic.session/night.ludic
Normal file
|
|
@ -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
|
||||
}
|
||||
5
packages/ludic.session/package.ludic
Normal file
5
packages/ludic.session/package.ludic
Normal file
|
|
@ -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
|
||||
46
packages/ludic.session/roles.ludic
Normal file
46
packages/ludic.session/roles.ludic
Normal file
|
|
@ -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
|
||||
}
|
||||
76
packages/ludic.session/roster.ludic
Normal file
76
packages/ludic.session/roster.ludic
Normal file
|
|
@ -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
|
||||
}
|
||||
89
packages/ludic.session/state.ludic
Normal file
89
packages/ludic.session/state.ludic
Normal file
|
|
@ -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<SessionFact> = null
|
||||
|
||||
export function ses_facts() -> Queue<SessionFact> {
|
||||
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
|
||||
}
|
||||
177
packages/ludic.session/tests/session_test.ludic
Normal file
177
packages/ludic.session/tests/session_test.ludic
Normal file
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
10
packages/ludic.session/vote_state.ludic
Normal file
10
packages/ludic.session/vote_state.ludic
Normal file
|
|
@ -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 }
|
||||
111
packages/ludic.session/votes.ludic
Normal file
111
packages/ludic.session/votes.ludic
Normal file
|
|
@ -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
|
||||
}
|
||||
47
packages/ludic.session/where.ludic
Normal file
47
packages/ludic.session/where.ludic
Normal file
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue