atris 3.74.5 → 3.74.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/gm.js +1 -1
- package/commands/task.js +4 -2
- package/commands/xp.js +68 -15
- package/commands/zen.js +10 -3
- package/lib/career-xp-lock.js +61 -0
- package/lib/career-xp-root.js +61 -0
- package/lib/gm-bet.js +405 -17
- package/lib/gm-card.js +431 -0
- package/lib/gm-phone-page.js +481 -1004
- package/lib/gm-phone.js +33 -38
- package/lib/task-db.js +8 -4
- package/lib/yes-lock.js +22 -3
- package/lib/zen-apply.js +1 -0
- package/lib/zen.js +15 -0
- package/package.json +1 -1
package/lib/gm-bet.js
CHANGED
|
@@ -2544,8 +2544,11 @@ function createBetDesk(deps) {
|
|
|
2544
2544
|
jobs: { working: [], yes: [], at: 0 },
|
|
2545
2545
|
// yes cards passed over this session, so a re-tap moves on
|
|
2546
2546
|
seenYes: new Set(),
|
|
2547
|
-
// yeses said this session, by key; a yes only passed over is still owed
|
|
2547
|
+
// yeses said this session, by key, and nos; a yes only passed over is still owed
|
|
2548
2548
|
answeredYes: new Set(),
|
|
2549
|
+
// yeses skipped with not now, in the order skipped: they wait at the back
|
|
2550
|
+
skippedYes: new Map(),
|
|
2551
|
+
skips: 0,
|
|
2549
2552
|
// reviews running now, by bet id
|
|
2550
2553
|
reviews: new Map(),
|
|
2551
2554
|
// failed jobs being sent back to the team now, by bet id
|
|
@@ -2785,10 +2788,19 @@ function createBetDesk(deps) {
|
|
|
2785
2788
|
.map(row => ({ kind: 'done', bet_id: row.id, mission_id: row.mission_id, title: row.bet.title, owner: row.owner || 'the team', bet: row.bet }));
|
|
2786
2789
|
}
|
|
2787
2790
|
|
|
2791
|
+
// The yeses still owed, one per key: not answered yes or no this session.
|
|
2792
|
+
function owedYes() {
|
|
2793
|
+
const keys = new Set();
|
|
2794
|
+
return state.jobs.yes.filter(j => !state.answeredYes.has(j.key) && !keys.has(j.key) && keys.add(j.key));
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2788
2797
|
// The next card that is not a bet: a yes the owner owes comes first, then
|
|
2789
2798
|
// a finished job. Nothing pops on its own; these only come on the next tap.
|
|
2790
|
-
|
|
2791
|
-
|
|
2799
|
+
// Yes cards never take over the screen: right after a yes, the next card is
|
|
2800
|
+
// a normal one, and only a yes not seen yet comes up on its own. The rest
|
|
2801
|
+
// wait behind the small need-you line, in the job deck.
|
|
2802
|
+
function nextJobCard({ afterYes = false } = {}) {
|
|
2803
|
+
const ask = afterYes ? null : owedYes().find(j => !state.seenYes.has(j.key));
|
|
2792
2804
|
if (ask) return { kind: 'yes', id: `yes:${ask.key}`, job: ask };
|
|
2793
2805
|
const done = doneJobs()[0];
|
|
2794
2806
|
if (done) return { kind: 'done', id: `done:${done.bet_id}`, job: done };
|
|
@@ -2994,8 +3006,9 @@ function createBetDesk(deps) {
|
|
|
2994
3006
|
log({ card: 'bet', choice: 'show', command: null, ran: 'in process', exit_code: 0, kind: 'question', question: item.question.key });
|
|
2995
3007
|
}
|
|
2996
3008
|
|
|
2997
|
-
// a re-tap on a job card moves on: a yes waits for a later session, a done is seen
|
|
2998
|
-
|
|
3009
|
+
// a re-tap on a job card moves on: a yes waits for a later session, a done is seen.
|
|
3010
|
+
// later: not now on a yes, which also sends it to the back of the job deck
|
|
3011
|
+
function passJob(item, { later = false } = {}) {
|
|
2999
3012
|
if (item.kind === 'wait') return;
|
|
3000
3013
|
if (item.kind === 'report') { state.passedQuestions.add(item.id); return; }
|
|
3001
3014
|
if (item.kind === 'choice') {
|
|
@@ -3009,9 +3022,11 @@ function createBetDesk(deps) {
|
|
|
3009
3022
|
log({ card: 'bet', choice: 'pass', command: null, ran: 'in process', exit_code: 0, kind: 'question', question: item.question.key });
|
|
3010
3023
|
return;
|
|
3011
3024
|
}
|
|
3025
|
+
const skipped = later && item.kind === 'yes';
|
|
3012
3026
|
if (item.kind === 'yes') state.seenYes.add(item.job.key);
|
|
3027
|
+
if (skipped) { state.skips += 1; state.skippedYes.set(item.job.key, state.skips); }
|
|
3013
3028
|
if (item.kind === 'done') record(item.job.bet_id, { done_seen_at: iso() });
|
|
3014
|
-
log({ card: 'bet', choice: 'pass', command: null, ran: 'in process', exit_code: 0, kind: item.kind, bet_id: item.job.bet_id });
|
|
3029
|
+
log({ card: 'bet', choice: skipped ? 'later' : 'pass', command: null, ran: 'in process', exit_code: 0, kind: item.kind, bet_id: item.job.bet_id });
|
|
3015
3030
|
}
|
|
3016
3031
|
|
|
3017
3032
|
pollJobs();
|
|
@@ -3106,11 +3121,14 @@ function createBetDesk(deps) {
|
|
|
3106
3121
|
// shows at once: a yes, a finished job, a queued bet, a question, or a
|
|
3107
3122
|
// plain card that says the next idea is being written. A tap never waits.
|
|
3108
3123
|
// sharper passes the bet over and asks the background for a sharper one.
|
|
3109
|
-
|
|
3124
|
+
// later: not now on a yes card.
|
|
3125
|
+
function next({ sharper = false, later = false } = {}) {
|
|
3110
3126
|
if (state.status === 'starting') return { ok: true, busy: true, ...view() };
|
|
3111
3127
|
const onScreen = state.status === 'ready' && state.current ? state.current : null;
|
|
3128
|
+
// passed, skipped, or answered: the card after a yes is never another yes
|
|
3129
|
+
const afterYes = Boolean(state.current && state.current.kind === 'yes');
|
|
3112
3130
|
if (onScreen && onScreen.kind && onScreen.kind !== 'bet') {
|
|
3113
|
-
passJob(onScreen);
|
|
3131
|
+
passJob(onScreen, { later });
|
|
3114
3132
|
} else if (onScreen) {
|
|
3115
3133
|
record(onScreen.id, { status: 'passed', ...(sharper ? { note: 'asked for a sharper idea' } : {}) });
|
|
3116
3134
|
log({ card: 'bet', choice: sharper ? 'sharper' : 'pass', command: null, ran: 'in process', exit_code: 0, bet_id: onScreen.id });
|
|
@@ -3120,7 +3138,7 @@ function createBetDesk(deps) {
|
|
|
3120
3138
|
}
|
|
3121
3139
|
dropStale();
|
|
3122
3140
|
if (now() - state.jobs.at > 5000) pollJobs();
|
|
3123
|
-
const job = nextJobCard();
|
|
3141
|
+
const job = nextJobCard({ afterYes });
|
|
3124
3142
|
const ask = job ? null : askCard();
|
|
3125
3143
|
if (job) showJob(job);
|
|
3126
3144
|
else if (ask) showAsk(ask);
|
|
@@ -3268,7 +3286,10 @@ function createBetDesk(deps) {
|
|
|
3268
3286
|
// A yes from the job deck names its key, and answers that job's yes
|
|
3269
3287
|
// wherever it is; the card on the main screen clears only when it is the
|
|
3270
3288
|
// same yes.
|
|
3271
|
-
|
|
3289
|
+
// The yes or no a key names: the main screen's yes card when it is the
|
|
3290
|
+
// same request, else that job's card in the job deck. Without a key, the
|
|
3291
|
+
// yes on the main screen.
|
|
3292
|
+
function findYes({ key, job: jobId } = {}) {
|
|
3272
3293
|
const shown = onJob('yes');
|
|
3273
3294
|
let item = shown;
|
|
3274
3295
|
if (key) {
|
|
@@ -3280,6 +3301,11 @@ function createBetDesk(deps) {
|
|
|
3280
3301
|
item = found ? { kind: 'yes', job: found } : null;
|
|
3281
3302
|
}
|
|
3282
3303
|
}
|
|
3304
|
+
return { shown, item };
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
async function answerYes({ key, job: jobId } = {}) {
|
|
3308
|
+
const { shown, item } = findYes({ key, job: jobId });
|
|
3283
3309
|
if (!item) return { ok: false, reason: 'gone', message: key ? 'that question was already answered' : 'there is no question on screen' };
|
|
3284
3310
|
const { job } = item;
|
|
3285
3311
|
const clear = () => { if (shown && shown.job.key === job.key) state.status = 'cleared'; };
|
|
@@ -3324,6 +3350,44 @@ function createBetDesk(deps) {
|
|
|
3324
3350
|
return { ok: true, ack: `${job.owner}: got your yes` };
|
|
3325
3351
|
}
|
|
3326
3352
|
|
|
3353
|
+
// no: the one request on the card is told no, and the job goes on without
|
|
3354
|
+
// it. A held command is denied for good (asking again only hears the no)
|
|
3355
|
+
// and the job is pinged; a plain ask is answered no through atris decide,
|
|
3356
|
+
// matched by mission and ask, the way a yes is.
|
|
3357
|
+
async function answerNo({ key, job: jobId } = {}) {
|
|
3358
|
+
const { shown, item } = findYes({ key, job: jobId });
|
|
3359
|
+
if (!item) return { ok: false, reason: 'gone', message: key ? 'that question was already answered' : 'there is no question on screen' };
|
|
3360
|
+
const { job } = item;
|
|
3361
|
+
const display = args => require('./gm-phone').displayCommand(args);
|
|
3362
|
+
const told = `told ${job.owner} no. it goes on without it.`;
|
|
3363
|
+
const done = (ack) => {
|
|
3364
|
+
state.seenYes.add(job.key);
|
|
3365
|
+
state.answeredYes.add(job.key);
|
|
3366
|
+
if (shown && shown.job.key === job.key) state.status = 'cleared';
|
|
3367
|
+
pollJobs();
|
|
3368
|
+
return { ok: true, ack };
|
|
3369
|
+
};
|
|
3370
|
+
if (job.lock_step) {
|
|
3371
|
+
const denied = require('./yes-lock').denyYesLock(lockDir, job.mission_id, job.lock_step, { hash: job.lock_hash || null, at: iso() });
|
|
3372
|
+
if (!denied) return done('that request was already answered.');
|
|
3373
|
+
const args = ['mission', 'ping', job.mission_id, `the owner said no to step ${job.lock_step}: ${job.ask}. do not run it; leave that step undone and go on with the rest.`, '--from', 'gm-phone'];
|
|
3374
|
+
const res = await deps.runCli(args);
|
|
3375
|
+
log({ card: 'bet', choice: 'no', command: display(args), exit_code: res.code, bet_id: job.bet_id, mission_id: job.mission_id, step: job.lock_step });
|
|
3376
|
+
if (job.bet_id) record(job.bet_id, { no_answered: { mission_id: job.mission_id, step: job.lock_step, at: iso() } });
|
|
3377
|
+
return done(told);
|
|
3378
|
+
}
|
|
3379
|
+
const listed = await deps.runCli(['decide', '--json']);
|
|
3380
|
+
const open = (parseJsonOut(listed.stdout) || {}).decisions || [];
|
|
3381
|
+
const match = open.find(d => d && d.mission_id === job.mission_id && d.ask_index === job.ask_index);
|
|
3382
|
+
if (!match) return done('that question was already answered.');
|
|
3383
|
+
const args = ['decide', String(match.number), 'no', '--json'];
|
|
3384
|
+
const res = await deps.runCli(args);
|
|
3385
|
+
log({ card: 'bet', choice: 'no', command: display(args), exit_code: res.code, bet_id: job.bet_id, mission_id: job.mission_id });
|
|
3386
|
+
if (res.code !== 0) return { ok: false, reason: 'answer_failed', message: 'your no did not go through; try again' };
|
|
3387
|
+
if (job.bet_id) record(job.bet_id, { no_answered: { mission_id: job.mission_id, ask_index: job.ask_index, at: iso() } });
|
|
3388
|
+
return done(told);
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3327
3391
|
// Two sentences for the owner: what is going on, and why it matters to
|
|
3328
3392
|
// the business. Nothing about how the work was checked.
|
|
3329
3393
|
function publicJob(item) {
|
|
@@ -3396,6 +3460,8 @@ function createBetDesk(deps) {
|
|
|
3396
3460
|
thread: cur && state.thread.id === cur.id ? state.thread.turns.slice() : [],
|
|
3397
3461
|
// the footer count: only members really working
|
|
3398
3462
|
team_working: statusTable().rows.filter(r => r.status === 'working').length,
|
|
3463
|
+
// the yeses still owed past the one on screen: the small need-you line
|
|
3464
|
+
needs_you: owedYes().filter(j => !(kind === 'yes' && cur.job && cur.job.key === j.key)).length,
|
|
3399
3465
|
events: events.map(e => ({ i: e.i, kind: e.kind, line: e.line, ...(e.key ? { key: e.key } : {}), ...(e.n != null ? { n: e.n } : {}) })),
|
|
3400
3466
|
error: state.error,
|
|
3401
3467
|
queue: state.queue.length,
|
|
@@ -3722,7 +3788,8 @@ function createBetDesk(deps) {
|
|
|
3722
3788
|
const name = String(member || '');
|
|
3723
3789
|
if (!onTeam(name)) return NOT_ON_TEAM;
|
|
3724
3790
|
try {
|
|
3725
|
-
|
|
3791
|
+
const read = deps.engineChoices || ((root, who, o) => require('./roster-swap').memberEngineChoices(root, who, o));
|
|
3792
|
+
return { ok: true, ...read(deps.workspaceRoot, name, { now: new Date(now()) }) };
|
|
3726
3793
|
} catch (err) {
|
|
3727
3794
|
return { ok: false, reason: 'unreadable', message: String((err && err.message) || err) };
|
|
3728
3795
|
}
|
|
@@ -3734,7 +3801,8 @@ function createBetDesk(deps) {
|
|
|
3734
3801
|
const where = scope === 'job' ? 'job' : 'member';
|
|
3735
3802
|
let out;
|
|
3736
3803
|
try {
|
|
3737
|
-
|
|
3804
|
+
const swapFn = deps.swapFromCard || ((...a) => require('./roster-swap').swapFromCard(...a));
|
|
3805
|
+
out = swapFn(deps.workspaceRoot, name, String(engine || ''), String(model || ''), where, { now: new Date(now()) });
|
|
3738
3806
|
} catch (err) {
|
|
3739
3807
|
const message = String((err && err.message) || err);
|
|
3740
3808
|
log({ card: 'bet', choice: 'engine_swap', command: null, ran: 'in process', exit_code: 2, member: name, scope: where, reason: message.slice(0, 200) });
|
|
@@ -3761,7 +3829,8 @@ function createBetDesk(deps) {
|
|
|
3761
3829
|
// passed over on the main screen is still owed, so it stays on its card.
|
|
3762
3830
|
function yesFor(mission) {
|
|
3763
3831
|
const said = state.answeredYes;
|
|
3764
|
-
const
|
|
3832
|
+
const owed = (state.jobs.yes || []).filter(j => j.mission_id === mission.id && !said.has(j.key));
|
|
3833
|
+
const own = owed.find(j => !state.skippedYes.has(j.key)) || owed[0];
|
|
3765
3834
|
if (own) return own;
|
|
3766
3835
|
const owner = mission.owner || 'the team';
|
|
3767
3836
|
for (const lock of require('./yes-lock').askedYesLocks(lockDir, mission.id)) {
|
|
@@ -3795,11 +3864,15 @@ function createBetDesk(deps) {
|
|
|
3795
3864
|
jobs.push({ id: `handoff:${hnd.id || at}`, mission: null, member: hnd.to, row: { on, status: 'waiting to start', time: `waiting ${spanWords(now() - at)}` }, moved: at });
|
|
3796
3865
|
}
|
|
3797
3866
|
jobs.sort((x, y) => STATUS_ORDER.indexOf(x.row.status) - STATUS_ORDER.indexOf(y.row.status) || y.moved - x.moved || x.member.localeCompare(y.member));
|
|
3867
|
+
// a yes skipped with not now waits at the back of the ones that need the owner
|
|
3868
|
+
const skipRank = x => (x.yes && state.skippedYes.get(x.yes.key)) || 0;
|
|
3869
|
+
const needsFirst = x => (x.card.label === 'needs you' ? 0 : 1);
|
|
3798
3870
|
return jobs.map((j) => {
|
|
3799
3871
|
const needs = j.row.status === 'waiting for your yes';
|
|
3800
3872
|
const yes = needs && j.mission ? yesFor(j.mission) : null;
|
|
3801
3873
|
const f = facts.get(j.member) || { role: NO_RATING, ovr: NO_RATING, engine: NO_RATING };
|
|
3802
|
-
|
|
3874
|
+
// a job whose yes was said here is back with its member, not waiting on the owner
|
|
3875
|
+
const label = needs ? (yes ? 'needs you' : 'you answered') : j.row.status;
|
|
3803
3876
|
const card = {
|
|
3804
3877
|
job_id: j.id,
|
|
3805
3878
|
mission_id: j.mission ? j.mission.id : null,
|
|
@@ -3808,9 +3881,11 @@ function createBetDesk(deps) {
|
|
|
3808
3881
|
time: j.row.time,
|
|
3809
3882
|
rows: [{ done: 'not yet', work: plainWork(j.row.on, j.member), member: j.member, role: f.role, ovr: f.ovr, engine: f.engine, status: label }],
|
|
3810
3883
|
};
|
|
3811
|
-
if (yes) card.yes = { key: yes.key, ...publicJob({ kind: 'yes', job: yes }) };
|
|
3884
|
+
if (yes) card.yes = { key: yes.key, ...(yes.lock_hash ? { hash: yes.lock_hash } : {}), ...publicJob({ kind: 'yes', job: yes }) };
|
|
3812
3885
|
return { card, yes };
|
|
3813
|
-
})
|
|
3886
|
+
}).map((x, i) => ({ x, i }))
|
|
3887
|
+
.sort((a, b) => needsFirst(a.x) - needsFirst(b.x) || (needsFirst(a.x) ? 0 : skipRank(a.x) - skipRank(b.x)) || a.i - b.i)
|
|
3888
|
+
.map(p => p.x);
|
|
3814
3889
|
}
|
|
3815
3890
|
|
|
3816
3891
|
function jobDeck() {
|
|
@@ -3871,7 +3946,320 @@ function createBetDesk(deps) {
|
|
|
3871
3946
|
}
|
|
3872
3947
|
}
|
|
3873
3948
|
|
|
3874
|
-
|
|
3949
|
+
// ---- the one card on screen (lib/gm-card.js) ----
|
|
3950
|
+
//
|
|
3951
|
+
// The page draws exactly the card served here and nothing else. Over the
|
|
3952
|
+
// main card (the idea, question, yes, and the rest) can sit one layer: the
|
|
3953
|
+
// team, a member's talk card, the job deck, or a swap card. A reply to
|
|
3954
|
+
// words said on a card is its own card over that card. Every action names
|
|
3955
|
+
// the card it acts on and one option on it; anything else is refused.
|
|
3956
|
+
const gmCard = require('./gm-card');
|
|
3957
|
+
// layer: null | { kind: team|member|deck|swap, id, ... }; reply: null | { base, id, text, suggestions, confirm, thread }
|
|
3958
|
+
state.layer = null;
|
|
3959
|
+
state.reply = null;
|
|
3960
|
+
state.served = null;
|
|
3961
|
+
state.serial = 0;
|
|
3962
|
+
// ids made here carry this desk's own tag, so a page left open across a
|
|
3963
|
+
// restart never names a card this desk made
|
|
3964
|
+
const deskTag = crypto.randomBytes(3).toString('hex');
|
|
3965
|
+
const freshId = kind => `${kind}~${deskTag}${(state.serial += 1)}`;
|
|
3966
|
+
const betBody = b => [b.why_now, b.expect].map(sentence).join(' ');
|
|
3967
|
+
|
|
3968
|
+
// the member's facts for a card about their job
|
|
3969
|
+
function jobFacts(name) {
|
|
3970
|
+
if (!onTeam(name)) return null;
|
|
3971
|
+
const f = rosterFacts().get(name) || {};
|
|
3972
|
+
return { member: name, role: f.role || NO_RATING, ovr: f.ovr || NO_RATING, engine: f.engine || NO_RATING };
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
function mainCard() {
|
|
3976
|
+
const cur = state.current;
|
|
3977
|
+
if (!cur) return null;
|
|
3978
|
+
const kind = cur.kind || 'bet';
|
|
3979
|
+
if (kind === 'bet' && (state.status === 'starting' || state.status === 'started')) {
|
|
3980
|
+
return gmCard.started({ id: `${cur.id}:started`, title: cur.bet.title, body: betBody(cur.bet), line: state.status === 'starting' ? 'starting now' : (state.mission && state.mission.line) });
|
|
3981
|
+
}
|
|
3982
|
+
if (state.status !== 'ready') return null;
|
|
3983
|
+
const v = view();
|
|
3984
|
+
if (kind === 'bet') return gmCard.idea({ id: cur.id, title: cur.bet.title, body: betBody(cur.bet) });
|
|
3985
|
+
if (kind === 'question') return gmCard.question({ id: cur.id, title: v.question.ask, body: v.question.line, choices: v.question.choices });
|
|
3986
|
+
if (kind === 'report') return gmCard.report({ id: cur.id, title: v.report.what, body: v.report.why });
|
|
3987
|
+
if (kind === 'choice') return gmCard.choice({ id: cur.id, items: v.choice.options });
|
|
3988
|
+
if (kind === 'wait') return gmCard.wait({ id: cur.id });
|
|
3989
|
+
if (kind === 'yes') {
|
|
3990
|
+
const j = cur.job;
|
|
3991
|
+
return gmCard.yes({ id: cur.id, title: v.job.what, body: v.job.why, job: jobFacts(j.owner), request: { job: j.mission_id, key: j.key, hash: j.lock_hash || null } });
|
|
3992
|
+
}
|
|
3993
|
+
if (kind === 'done') return gmCard.done({ id: cur.id, title: v.job.what, body: v.job.why, job: jobFacts(cur.job.owner) });
|
|
3994
|
+
return null;
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3997
|
+
function layerCard() {
|
|
3998
|
+
const l = state.layer;
|
|
3999
|
+
if (!l) return null;
|
|
4000
|
+
if (l.kind === 'team') {
|
|
4001
|
+
const rows = teamTable().rows;
|
|
4002
|
+
return gmCard.team({ id: l.id, rows, line: l.line || null, deck: rows.some(r => DECK_STATUS.has(r.status)) });
|
|
4003
|
+
}
|
|
4004
|
+
if (l.kind === 'member') {
|
|
4005
|
+
const row = teamTable().rows.find(r => r.member === l.name);
|
|
4006
|
+
return gmCard.member({ id: l.id, name: l.name, sentence: row ? sentence(row.sentence) : 'nothing in the last day.', job: jobFacts(l.name), line: l.line || null });
|
|
4007
|
+
}
|
|
4008
|
+
if (l.kind === 'deck') {
|
|
4009
|
+
const cards = deckCards().map(x => x.card);
|
|
4010
|
+
if (!cards.length) return null;
|
|
4011
|
+
const at = Math.min(l.at || 0, cards.length - 1);
|
|
4012
|
+
const c = { ...cards[at], said: (l.said && l.said[cards[at].job_id]) || null };
|
|
4013
|
+
const r = c.rows[0];
|
|
4014
|
+
// the deck card is about the job: its row without the rating and the model, which live on the team table
|
|
4015
|
+
const columns = DECK_COLUMNS.filter(k => k !== 'ovr' && k !== 'engine');
|
|
4016
|
+
return gmCard.deck({ id: l.id, at, total: cards.length, card: c, columns, job: { member: c.member, role: r.role, ovr: r.ovr, engine: r.engine } });
|
|
4017
|
+
}
|
|
4018
|
+
if (l.kind === 'swap') {
|
|
4019
|
+
if (!l.choices) {
|
|
4020
|
+
const out = engineCard({ member: l.name });
|
|
4021
|
+
if (!out.ok) return gmCard.member({ id: l.id, name: l.name, sentence: out.message || 'could not read the models on this machine.', job: jobFacts(l.name) });
|
|
4022
|
+
l.choices = out;
|
|
4023
|
+
}
|
|
4024
|
+
return gmCard.swap({ id: l.id, name: l.name, choices: l.choices, scope: l.scope, all: l.all, line: l.line || null });
|
|
4025
|
+
}
|
|
4026
|
+
return null;
|
|
4027
|
+
}
|
|
4028
|
+
|
|
4029
|
+
// the card under a reply: the layer when one is open, else the main card
|
|
4030
|
+
function underCard() {
|
|
4031
|
+
return layerCard() || mainCard();
|
|
4032
|
+
}
|
|
4033
|
+
|
|
4034
|
+
function currentCard() {
|
|
4035
|
+
const under = underCard();
|
|
4036
|
+
if (!under) return null;
|
|
4037
|
+
const r = state.reply;
|
|
4038
|
+
if (r && r.base === under.id) {
|
|
4039
|
+
return gmCard.reply({ id: r.id, base: under, text: r.text, thread: r.thread, suggestions: r.suggestions, confirm: r.confirm });
|
|
4040
|
+
}
|
|
4041
|
+
if (r) state.reply = null;
|
|
4042
|
+
return under;
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
function serve() {
|
|
4046
|
+
const card = currentCard();
|
|
4047
|
+
// served under a stamped id: options that change come with a new id, so a tap on the old one does nothing
|
|
4048
|
+
state.served = card ? gmCard.stamp(gmCard.enforce(card, problems => log({ card: 'bet', choice: 'card_problem', command: null, ran: 'in process', exit_code: 1, kind: card.kind, problems: problems.slice(0, 5) }))) : null;
|
|
4049
|
+
return state.served;
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
// what every answer carries: the card on screen and the small lines around it
|
|
4053
|
+
function envelope(extra = {}) {
|
|
4054
|
+
const card = serve();
|
|
4055
|
+
const v = view();
|
|
4056
|
+
return {
|
|
4057
|
+
ok: true,
|
|
4058
|
+
...extra,
|
|
4059
|
+
card: gmCard.wire(card),
|
|
4060
|
+
status: v.status,
|
|
4061
|
+
queue: v.queue,
|
|
4062
|
+
team_working: v.team_working,
|
|
4063
|
+
needs_you: v.needs_you,
|
|
4064
|
+
};
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
function refuse(reason, message, status = 409) {
|
|
4068
|
+
return { status, body: { ...envelope(), ok: false, reason, message } };
|
|
4069
|
+
}
|
|
4070
|
+
const done200 = extra => ({ status: 200, body: envelope(extra) });
|
|
4071
|
+
|
|
4072
|
+
// the page opened, or looked again: the card on screen now. A read starts
|
|
4073
|
+
// no work: the queue fills when the desk is made, on its timer, and on an act.
|
|
4074
|
+
function card() {
|
|
4075
|
+
return envelope();
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
// a word that came back as a reply: the reply is the next card
|
|
4079
|
+
function replied(out, baseId, text) {
|
|
4080
|
+
if (out.stay) {
|
|
4081
|
+
state.reply = { base: baseId, id: freshId(`${baseId}~r`), text: out.reply, suggestions: out.suggestions || [], confirm: null, thread: (out.thread || []).slice(0, -1) };
|
|
4082
|
+
return done200();
|
|
4083
|
+
}
|
|
4084
|
+
if (out.reason === 'needs_confirm') {
|
|
4085
|
+
state.reply = { base: baseId, id: freshId(`${baseId}~r`), text: out.message, suggestions: [], confirm: { text }, thread: [] };
|
|
4086
|
+
return done200();
|
|
4087
|
+
}
|
|
4088
|
+
return null;
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
// words said on the main card, a reply on it, or its something else
|
|
4092
|
+
async function sayOnMain(text, more = {}) {
|
|
4093
|
+
const under = underCard();
|
|
4094
|
+
const out = await say({ text, ...more });
|
|
4095
|
+
const back = replied(out, under.id, text);
|
|
4096
|
+
if (back) return back;
|
|
4097
|
+
if (!out.ok) return refuse(out.reason || 'gone', out.message || 'that did not send. try again');
|
|
4098
|
+
state.reply = null;
|
|
4099
|
+
next();
|
|
4100
|
+
return done200({ ack: out.ack });
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
// words to the member on a talk card or a deck card
|
|
4104
|
+
async function talkOnLayer(text, more = {}) {
|
|
4105
|
+
const l = state.layer;
|
|
4106
|
+
const under = underCard();
|
|
4107
|
+
const name = l.kind === 'deck' ? under.member : l.name;
|
|
4108
|
+
const out = await talk({ member: name, text, ...more });
|
|
4109
|
+
const back = replied(out, under.id, text);
|
|
4110
|
+
if (back) return back;
|
|
4111
|
+
if (!out.ok) return refuse(out.reason || 'gone', out.message || 'that did not send. try again');
|
|
4112
|
+
state.reply = null;
|
|
4113
|
+
if (l.kind === 'deck') { l.said = { ...(l.said || {}), [under.deck_job]: out.ack }; l.id = freshId('deck'); } else { l.line = out.ack; l.id = freshId('member'); }
|
|
4114
|
+
return done200();
|
|
4115
|
+
}
|
|
4116
|
+
|
|
4117
|
+
function open(kind, extra = {}) {
|
|
4118
|
+
state.reply = null;
|
|
4119
|
+
state.layer = { kind, id: freshId(kind), ...extra };
|
|
4120
|
+
if (kind === 'deck' && !layerCard()) state.layer = { kind: 'team', id: freshId('team') };
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
// One action from the page: { card_id, option_id } or { card_id, text } for
|
|
4124
|
+
// the card's box, or { orb: true } for the next card, or { open: team|deck }
|
|
4125
|
+
// from the footer. confirm: the second tap on an option that asks for one.
|
|
4126
|
+
async function act(body = {}) {
|
|
4127
|
+
const cardId = body.card_id == null || body.card_id === '' ? null : String(body.card_id);
|
|
4128
|
+
const shown = state.served;
|
|
4129
|
+
const stale = Boolean(cardId && (!shown || cardId !== shown.id));
|
|
4130
|
+
// the big orb: the next card; a page that names an older card gets the one on screen, and nothing is passed
|
|
4131
|
+
if (body.orb === true) {
|
|
4132
|
+
if (stale && shown) return done200();
|
|
4133
|
+
state.layer = null;
|
|
4134
|
+
state.reply = null;
|
|
4135
|
+
const out = next({ sharper: false });
|
|
4136
|
+
return done200(out.busy ? { busy: true } : {});
|
|
4137
|
+
}
|
|
4138
|
+
if (body.open === 'team' || body.open === 'deck') {
|
|
4139
|
+
if (stale) return refuse('gone', 'that card changed. here is the one on screen now');
|
|
4140
|
+
open(body.open);
|
|
4141
|
+
return done200();
|
|
4142
|
+
}
|
|
4143
|
+
// an option or words always name the card they were for
|
|
4144
|
+
if (!shown || !cardId || stale) return refuse('gone', 'that card changed, so nothing was done. here is the one on screen now');
|
|
4145
|
+
const optionId = body.option_id == null ? null : String(body.option_id);
|
|
4146
|
+
const opt = optionId ? shown.options.find(o => o.id === optionId) : null;
|
|
4147
|
+
if (optionId && !opt) return refuse('bad_option', 'that is not on this card');
|
|
4148
|
+
if (!opt && !shown.input) return refuse('bad_option', 'this card has no box to type in');
|
|
4149
|
+
const typed = String(body.text == null ? '' : body.text).trim();
|
|
4150
|
+
if ((!opt || opt.type) && !typed) return refuse('empty', 'say something first');
|
|
4151
|
+
if (opt && opt.confirm && body.confirm !== true) return { status: 409, body: { ...envelope(), ok: false, reason: 'needs_confirm', message: opt.confirm } };
|
|
4152
|
+
const action = opt ? opt.action : shown.input.action;
|
|
4153
|
+
const b = (opt && opt.binds) || {};
|
|
4154
|
+
const onLayer = Boolean(state.layer);
|
|
4155
|
+
log({ card: 'bet', choice: 'act', command: null, ran: 'in process', exit_code: 0, kind: shown.kind, action, option: optionId });
|
|
4156
|
+
switch (action) {
|
|
4157
|
+
case 'do': {
|
|
4158
|
+
const out = await doIt();
|
|
4159
|
+
if (!out.ok) return refuse(out.reason, out.message);
|
|
4160
|
+
next();
|
|
4161
|
+
return done200({ ack: out.ack });
|
|
4162
|
+
}
|
|
4163
|
+
case 'sharper': next({ sharper: true }); return done200();
|
|
4164
|
+
case 'skip': {
|
|
4165
|
+
const out = skip();
|
|
4166
|
+
if (!out.ok) return refuse(out.reason, out.message);
|
|
4167
|
+
state.reply = null;
|
|
4168
|
+
return done200({ ack: out.ack, home: true });
|
|
4169
|
+
}
|
|
4170
|
+
case 'next': next(); return done200();
|
|
4171
|
+
case 'later': {
|
|
4172
|
+
if (onLayer && state.layer.kind === 'deck') {
|
|
4173
|
+
state.seenYes.add(b.key);
|
|
4174
|
+
state.skips += 1;
|
|
4175
|
+
state.skippedYes.set(b.key, state.skips);
|
|
4176
|
+
state.layer.id = freshId('deck');
|
|
4177
|
+
return done200();
|
|
4178
|
+
}
|
|
4179
|
+
state.reply = null;
|
|
4180
|
+
next({ later: true });
|
|
4181
|
+
return done200();
|
|
4182
|
+
}
|
|
4183
|
+
case 'yes':
|
|
4184
|
+
case 'no': {
|
|
4185
|
+
const out = await (action === 'yes' ? answerYes : answerNo)({ key: b.key, job: b.job });
|
|
4186
|
+
if (!out.ok) return refuse(out.reason, out.message);
|
|
4187
|
+
state.reply = null;
|
|
4188
|
+
if (onLayer && state.layer.kind === 'deck') {
|
|
4189
|
+
state.layer.said = { ...(state.layer.said || {}), [shown.deck_job]: out.ack };
|
|
4190
|
+
state.layer.id = freshId('deck');
|
|
4191
|
+
return done200();
|
|
4192
|
+
}
|
|
4193
|
+
next();
|
|
4194
|
+
return done200({ ack: out.ack });
|
|
4195
|
+
}
|
|
4196
|
+
case 'nice': {
|
|
4197
|
+
const out = nice();
|
|
4198
|
+
if (!out.ok) return refuse(out.reason, out.message);
|
|
4199
|
+
next();
|
|
4200
|
+
return done200({ ack: out.ack });
|
|
4201
|
+
}
|
|
4202
|
+
case 'answer': {
|
|
4203
|
+
const out = answer({ choice: b.choice });
|
|
4204
|
+
if (!out.ok) return refuse(out.reason, out.message);
|
|
4205
|
+
state.reply = null;
|
|
4206
|
+
next();
|
|
4207
|
+
return done200();
|
|
4208
|
+
}
|
|
4209
|
+
case 'pick': {
|
|
4210
|
+
const out = pick({ id: b.bet });
|
|
4211
|
+
if (!out.ok) return refuse(out.reason, out.message);
|
|
4212
|
+
return done200();
|
|
4213
|
+
}
|
|
4214
|
+
case 'report': {
|
|
4215
|
+
const raw = typed.replace(/,/g, '');
|
|
4216
|
+
if (/^-?\d+(\.\d+)?$/.test(raw)) {
|
|
4217
|
+
const out = report({ value: raw });
|
|
4218
|
+
if (!out.ok) return refuse(out.reason, out.message);
|
|
4219
|
+
state.reply = null;
|
|
4220
|
+
next();
|
|
4221
|
+
return done200({ ack: out.ack });
|
|
4222
|
+
}
|
|
4223
|
+
return sayOnMain(typed);
|
|
4224
|
+
}
|
|
4225
|
+
case 'say': return onLayer ? talkOnLayer(typed || b.text) : sayOnMain(typed || b.text);
|
|
4226
|
+
case 'talk': return talkOnLayer(typed);
|
|
4227
|
+
case 'start':
|
|
4228
|
+
case 'note': {
|
|
4229
|
+
const more = action === 'start' ? { confirm: true } : { note: true };
|
|
4230
|
+
return onLayer ? talkOnLayer(b.text, more) : sayOnMain(b.text, more);
|
|
4231
|
+
}
|
|
4232
|
+
case 'back': {
|
|
4233
|
+
if (state.reply) { state.reply = null; return done200(); }
|
|
4234
|
+
const l = state.layer;
|
|
4235
|
+
// a talk card, a swap card, and the job deck go back to the whole team; the team goes back to the card under it
|
|
4236
|
+
if (l && l.kind !== 'team') open('team');
|
|
4237
|
+
else state.layer = null;
|
|
4238
|
+
return done200();
|
|
4239
|
+
}
|
|
4240
|
+
case 'member': open('member', { name: b.member }); return done200();
|
|
4241
|
+
case 'swap': open('swap', { name: b.member, scope: 'member', all: false }); return done200();
|
|
4242
|
+
case 'deck': open('deck', { at: 0 }); return done200();
|
|
4243
|
+
case 'flip': {
|
|
4244
|
+
const n = deckCards().length;
|
|
4245
|
+
if (n) state.layer.at = (((state.layer.at || 0) + (Number(b.step) || 1)) % n + n) % n;
|
|
4246
|
+
state.layer.id = freshId('deck');
|
|
4247
|
+
return done200();
|
|
4248
|
+
}
|
|
4249
|
+
case 'scope': state.layer.scope = b.scope === 'job' ? 'job' : 'member'; state.layer.id = freshId('swap'); return done200();
|
|
4250
|
+
// more models: the same card, with every model on it
|
|
4251
|
+
case 'more': state.layer.all = true; return done200();
|
|
4252
|
+
case 'swap_to': {
|
|
4253
|
+
const out = swapEngine({ member: state.layer.name, engine: b.engine, model: b.model, scope: b.scope });
|
|
4254
|
+
if (!out.ok) return refuse(out.reason, out.message);
|
|
4255
|
+
open('team', { line: out.ack });
|
|
4256
|
+
return done200({ ack: out.ack });
|
|
4257
|
+
}
|
|
4258
|
+
default: return refuse('bad_option', 'that does nothing here');
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
4261
|
+
|
|
4262
|
+
return { warm, next, pick, say, talk, report, teamTable, jobDeck, engineCard, swapEngine, steer, skip, doIt, nice, answerYes, answerNo, answer, pollJobs, settle, yesCards, recordYes, view, card, act, state };
|
|
3875
4263
|
}
|
|
3876
4264
|
|
|
3877
4265
|
module.exports = {
|