feat(jobs): phase 3 - a job can be given up: jobs_give_up puts a written job back on offer from the next day (the day back is saved with it, so giving up is never a reroll), jobs_post_give_up takes a post down for the morning to replace; an auto board's posts are not taken and not given up; JOBS_F_GIVEN_UP tells the game to take down what it placed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 15:28:06 +03:00
parent e397e4eee9
commit 2c578c6f1c
8 changed files with 170 additions and 6 deletions

View file

@ -64,7 +64,8 @@ export port JobsPay { reward: fn(int, int, int) -> void } # (scope, money, rep
| `jobs_morning()`, `jobs_roll(lo, hi)`, `jobs_post(b, s, kind, param, need, money, rep)`, `jobs_config_seed(seed)` | posting |
| `jobs_slots(b)`, `jobs_posted(b)`, `jobs_post_kind / _param / _need / _have / _money / _rep / _state / _day(b, s)`, `jobs_posts_done(b)`, `jobs_total(b)` | the boards |
| `jobs_post_hand_in(b, s)`, `jobs_post_set(b, s, ...)`, `jobs_set_total(b, n)` | a post's verbs |
| `jobs_facts() -> Queue<JobsFact>` | `{ what: JOBS_F_OFFERED / _PROGRESSED / _COMPLETED / _HANDED_IN, job, board, slot, kind, param, have, need, money, rep, scope }` |
| `jobs_give_up(i)`, `jobs_back_on(i)`, `jobs_post_give_up(b, s)` | letting one go: a written job is offered again from the next day (the day back is saved with it), a post comes down and the morning replaces it; an auto board's posts are never taken, so never given up |
| `jobs_facts() -> Queue<JobsFact>` | `{ what: JOBS_F_OFFERED / _PROGRESSED / _COMPLETED / _HANDED_IN / _GIVEN_UP, job, board, slot, kind, param, have, need, money, rep, scope }` |
| `jobs_system()`, `jobs_party_system()` | `"jobs"` (yours, the tick) and `"jobs.party"` (the party's), each saving its scope's jobs and boards by key |
## Tests

View file

@ -49,6 +49,7 @@ export const JOBS_F_OFFERED: int = 0
export const JOBS_F_PROGRESSED: int = 1
export const JOBS_F_COMPLETED: int = 2
export const JOBS_F_HANDED_IN: int = 3
export const JOBS_F_GIVEN_UP: int = 4 # taken and let go: what the game placed for it comes down
# `job` a written job, or -1 and `board` / `slot` a post; what it counted rides along, since a
# post handed in is replaced before anyone reads the fact

View file

@ -0,0 +1,25 @@
# giveup.ludic - letting a job go. A written job goes back on offer, but not until tomorrow, so
# giving up is never a reroll; a post comes down and the morning puts up another in its place.
# Either way the fact says so, and the game takes down what it had placed for it.
export function jobs_give_up(jobs_st: mut JobsState, i: int) -> bool {
if not jobs_ok(i) or not jobs_under_way(jobs_st, i) { return false }
jobs__fact(jobs_st, JOBS_F_GIVEN_UP, i, -1, -1, jobs_st.jobs__hv[i], Jobs[i].need)
jobs_st.jobs__st[i] = JOBS_OFFERED
jobs_st.jobs__hv[i] = 0
jobs_st.jobs__back[i] = JobsWorld.day() + 1
return true
}
# the day a written job given up may be taken again, 0 never given up
export function jobs_back_on(jobs_st: JobsState, i: int) -> int { return jobs_st.jobs__back[i] }
# a post taken and not yet paid; an auto board's posts are not taken, so they are not given up
export function jobs_post_give_up(jobs_st: mut JobsState, b: int, s: int) -> bool {
if not jobs_board_ok(b) or s < 0 or s >= JobBoards[b].slots or JobBoards[b].auto { return false }
let k = jobs_st.jobs__off[b] + s
let st = jobs_st.jobs__ps[k]
if jobs_st.jobs__pk[k] < 0 or (st != JOBS_ACTIVE and st != JOBS_READY) { return false }
jobs__fact(jobs_st, JOBS_F_GIVEN_UP, -1, b, s, jobs_st.jobs__ph[k], jobs_st.jobs__pn[k])
jobs__clear(jobs_st, k)
return true
}

View file

@ -7,6 +7,7 @@ import "defs.ludic"
import "ports.ludic"
import "state.ludic"
import "jobs.ludic"
import "giveup.ludic"
import "count.ludic"
import "lists.ludic"
import "boards.ludic"

View file

@ -20,12 +20,12 @@ export function jobs_under_way(jobs_st: JobsState, i: int) -> bool {
# on offer: not taken, the scope's standing and the story far enough, and the game allowing it
export function jobs_offered(jobs_st: JobsState, i: int) -> bool {
let d = Jobs[i]
if jobs_state(jobs_st, i) != JOBS_OFFERED { return false }
if jobs_state(jobs_st, i) != JOBS_OFFERED or JobsWorld.day() < jobs_st.jobs__back[i] { return false }
return JobsWorld.rep(d.scope) >= d.minrep and JobsWorld.stage() >= d.minstage and JobsWorld.open(i)
}
export function jobs_take(jobs_st: mut JobsState, i: int) -> bool {
if not jobs_ok(i) or jobs_state(jobs_st, i) != JOBS_OFFERED { return false }
if not jobs_ok(i) or jobs_state(jobs_st, i) != JOBS_OFFERED or JobsWorld.day() < jobs_st.jobs__back[i] { return false }
jobs_st.jobs__st[i] = JOBS_ACTIVE
jobs_st.jobs__hv[i] = 0
jobs__recheck_one(jobs_st, i)

View file

@ -5,6 +5,7 @@ function jobs__reset_scope(jobs_st: mut JobsState, sc: int) -> void {
if Jobs[i].scope != sc { continue }
jobs_st.jobs__st[i] = JOBS_OFFERED
jobs_st.jobs__hv[i] = 0
jobs_st.jobs__back[i] = 0
}
for b in 0 .. BOARD_COUNT {
if JobBoards[b].scope != sc { continue }
@ -19,8 +20,8 @@ function jobs__save_scope(jobs_st: JobsState, sc: int) -> Val {
let v = Value.object()
let js = Value.object()
for i in 0 .. JOB_COUNT {
if Jobs[i].scope != sc or jobs_st.jobs__st[i] == JOBS_OFFERED { continue }
sv_put_ints(js, Jobs[i].key, [jobs_st.jobs__st[i], jobs_st.jobs__hv[i]])
if Jobs[i].scope != sc or (jobs_st.jobs__st[i] == JOBS_OFFERED and jobs_st.jobs__back[i] == 0) { continue }
sv_put_ints(js, Jobs[i].key, [jobs_st.jobs__st[i], jobs_st.jobs__hv[i], jobs_st.jobs__back[i]])
}
Value.put(v, "jobs", js)
let bs = Value.object()
@ -48,7 +49,9 @@ function jobs__load_scope(jobs_st: mut JobsState, sc: int, v: Val) -> void {
let js = Value.get(v, "jobs")
for i in 0 .. JOB_COUNT {
let p = sv_ints(js, Jobs[i].key)
if Jobs[i].scope == sc and len(p) == 2 { jobs_set(jobs_st, i, p[0], p[1]) }
if Jobs[i].scope != sc or len(p) < 2 { continue }
jobs_set(jobs_st, i, p[0], p[1])
if len(p) > 2 { jobs_st.jobs__back[i] = p[2] }
}
}
if Value.has(v, "boards") == 0 { return }

View file

@ -4,6 +4,7 @@ export state JobsState {
jobs__st: []int = jobs__ints(JOB_COUNT, JOBS_OFFERED)
jobs__hv: []int = jobs__ints(JOB_COUNT, 0)
jobs__seen: []bool = jobs__bools(JOB_COUNT) # an offer already said
jobs__back: []int = jobs__ints(JOB_COUNT, 0) # given up: the first day it may be taken again
jobs__primed: bool = false # the first tick after a reset learns the offers quietly
jobs__done_n: []int = jobs__ints(JOBS_GROUPS, 0) # handed in, per group
jobs__off: []int = jobs__offsets()

View file

@ -0,0 +1,132 @@
# giveup_test.ludic - letting a job go: a written job back on offer tomorrow, not today; a post taken
# down and replaced in the morning; a daily never taken; the day back saved with the job
import "ludic.jobs"
import "ludic.base"
program JobsGiveUpTest {
numbers float
const K_FISH: int = 0
const K_PHOTO: int = 1
const K_WOOD: int = 2
def Jobs catch { group: 0, kind: K_FISH, need: 3, money: 30, rep: 5 }
def Jobs deer { group: 0, kind: K_PHOTO, param: 4, need: 1, money: 20, rep: 2, minrep: 10 }
def Jobs ten { group: 0, kind: K_FISH, need: 10, money: 70, rep: 12, scope: JOBS_PARTY }
def Jobs still { group: 1, need: 0, money: 60, rep: 12, minstage: 2 }
def JobBoards daily { slots: 3, keep: 1, auto: true, refill: false }
def JobBoards ada { slots: 2, keep: 7, scope: JOBS_PARTY }
state JobsTestState {
rep: int = 0
stage: int = 0
day: int = 1
guest: bool = false
fish: int = 0 # the state's count of fish
paid: int = 0
paid_rep: int = 0
paid_scope: int = -1
}
function world_rep(jobs_test_st: JobsTestState, sc: int) -> int { return jobs_test_st.rep }
function world_stage(jobs_test_st: JobsTestState) -> int { return jobs_test_st.stage }
function world_day(jobs_test_st: JobsTestState) -> int { return jobs_test_st.day }
function world_owns(jobs_test_st: JobsTestState, sc: int) -> bool { return sc == JOBS_SELF or not jobs_test_st.guest }
function world_evidence(jobs_test_st: JobsTestState, kind: int, param: int) -> int {
if kind == K_FISH { return jobs_test_st.fish }
return -1
}
function world_roll(jobs_st: mut JobsState, b: int, s: int) -> void {
let need = jobs_roll(jobs_st, 2, 4)
if b == BOARD_DAILY { jobs_post(jobs_st, b, s, K_WOOD, -1, need, 7, 2) } else { jobs_post(jobs_st, b, s, K_FISH, -1, need, 9 * need, 3) }
}
function pay(jobs_test_st: mut JobsTestState, sc: int, money: int, r: int) -> void {
jobs_test_st.paid += money
jobs_test_st.paid_rep += r
jobs_test_st.paid_scope = sc
}
bind JobsWorld { rep: fn world_rep, stage: fn world_stage, day: fn world_day, owns: fn world_owns, evidence: fn world_evidence, roll: fn world_roll }
bind JobsPay { reward: fn pay }
function fresh(jobs_st: mut JobsState, jobs_test_st: mut JobsTestState) -> void {
jobs_test_st.rep = 0
jobs_test_st.stage = 0
jobs_test_st.day = 1
jobs_test_st.guest = false
jobs_test_st.fish = 0
jobs_test_st.paid = 0
jobs_test_st.paid_rep = 0
jobs_reset(jobs_st)
jobs_party_reset(jobs_st)
}
function count_of(jobs_st: JobsState, what: int) -> int {
let fs = q_drain(jobs_facts(jobs_st))
var n = 0
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
return n
}
test "a job given up is offered again tomorrow, not today" (jobs_st: mut JobsState, jobs_test_st: mut JobsTestState) {
fresh(jobs_st, jobs_test_st)
expect(jobs_take(jobs_st, JOB_CATCH))
jobs_count(jobs_st, K_FISH, 0, 2)
count_of(jobs_st, JOBS_F_GIVEN_UP)
expect(jobs_give_up(jobs_st, JOB_CATCH))
expect_eq(count_of(jobs_st, JOBS_F_GIVEN_UP), 1)
expect_eq(jobs_state(jobs_st, JOB_CATCH), JOBS_OFFERED)
expect_eq(jobs_have(jobs_st, JOB_CATCH), 0)
expect(not jobs_offered(jobs_st, JOB_CATCH))
expect(not jobs_take(jobs_st, JOB_CATCH))
expect(not jobs_give_up(jobs_st, JOB_CATCH))
jobs_test_st.day = 2
expect(jobs_offered(jobs_st, JOB_CATCH))
expect(jobs_take(jobs_st, JOB_CATCH))
expect_eq(jobs_test_st.paid, 0)
}
test "a ready job can be let go too, and nothing is paid" (jobs_st: mut JobsState, jobs_test_st: mut JobsTestState) {
fresh(jobs_st, jobs_test_st)
jobs_take(jobs_st, JOB_CATCH)
jobs_count(jobs_st, K_FISH, 0, 3)
expect_eq(jobs_state(jobs_st, JOB_CATCH), JOBS_READY)
expect(jobs_give_up(jobs_st, JOB_CATCH))
expect(not jobs_hand_in(jobs_st, JOB_CATCH))
expect_eq(jobs_test_st.paid, 0)
}
test "a post given up comes down, and the morning puts up another" (jobs_st: mut JobsState, jobs_test_st: mut JobsTestState) {
fresh(jobs_st, jobs_test_st)
jobs_morning(jobs_st)
count_of(jobs_st, JOBS_F_OFFERED)
expect(jobs_post_give_up(jobs_st, BOARD_ADA, 0))
expect_eq(count_of(jobs_st, JOBS_F_GIVEN_UP), 1)
expect_eq(jobs_post_kind(jobs_st, BOARD_ADA, 0), -1)
expect(not jobs_post_give_up(jobs_st, BOARD_ADA, 0))
expect(jobs_post_kind(jobs_st, BOARD_ADA, 1) >= 0)
jobs_test_st.day = 2
jobs_morning(jobs_st)
expect(jobs_post_kind(jobs_st, BOARD_ADA, 0) >= 0)
expect_eq(jobs_post_day(jobs_st, BOARD_ADA, 0), 2)
}
test "a daily is not taken, so it is not given up" (jobs_st: mut JobsState, jobs_test_st: mut JobsTestState) {
fresh(jobs_st, jobs_test_st)
jobs_morning(jobs_st)
expect(not jobs_post_give_up(jobs_st, BOARD_DAILY, 0))
expect(jobs_post_kind(jobs_st, BOARD_DAILY, 0) >= 0)
}
test "the day back is saved with the job" (jobs_st: mut JobsState, jobs_test_st: mut JobsTestState) {
fresh(jobs_st, jobs_test_st)
jobs_take(jobs_st, JOB_CATCH)
jobs_give_up(jobs_st, JOB_CATCH)
let body = jobs_save(jobs_st)
fresh(jobs_st, jobs_test_st)
expect(jobs_offered(jobs_st, JOB_CATCH))
jobs_load(jobs_st, body, 1)
expect_eq(jobs_back_on(jobs_st, JOB_CATCH), 2)
expect(not jobs_offered(jobs_st, JOB_CATCH))
jobs_test_st.day = 2
expect(jobs_offered(jobs_st, JOB_CATCH))
}
}