diff --git a/packages/ludic.jobs/README.md b/packages/ludic.jobs/README.md index 7561670a..809b3028 100644 --- a/packages/ludic.jobs/README.md +++ b/packages/ludic.jobs/README.md @@ -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` | `{ 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` | `{ 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 diff --git a/packages/ludic.jobs/defs.ludic b/packages/ludic.jobs/defs.ludic index 4da15821..657fc49a 100644 --- a/packages/ludic.jobs/defs.ludic +++ b/packages/ludic.jobs/defs.ludic @@ -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 diff --git a/packages/ludic.jobs/giveup.ludic b/packages/ludic.jobs/giveup.ludic new file mode 100644 index 00000000..95b9c972 --- /dev/null +++ b/packages/ludic.jobs/giveup.ludic @@ -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 +} diff --git a/packages/ludic.jobs/index.ludic b/packages/ludic.jobs/index.ludic index b29ea912..6ff8a86a 100644 --- a/packages/ludic.jobs/index.ludic +++ b/packages/ludic.jobs/index.ludic @@ -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" diff --git a/packages/ludic.jobs/jobs.ludic b/packages/ludic.jobs/jobs.ludic index 2ec1c933..2be089f8 100644 --- a/packages/ludic.jobs/jobs.ludic +++ b/packages/ludic.jobs/jobs.ludic @@ -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) diff --git a/packages/ludic.jobs/save.ludic b/packages/ludic.jobs/save.ludic index 46a15a0e..fedc10a3 100644 --- a/packages/ludic.jobs/save.ludic +++ b/packages/ludic.jobs/save.ludic @@ -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 } diff --git a/packages/ludic.jobs/state.ludic b/packages/ludic.jobs/state.ludic index c3c2b7d8..370701f2 100644 --- a/packages/ludic.jobs/state.ludic +++ b/packages/ludic.jobs/state.ludic @@ -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() diff --git a/packages/ludic.jobs/tests/giveup_test.ludic b/packages/ludic.jobs/tests/giveup_test.ludic new file mode 100644 index 00000000..96b83c66 --- /dev/null +++ b/packages/ludic.jobs/tests/giveup_test.ludic @@ -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)) + } +}