diff --git a/LANEHANDOVER.md b/LANEHANDOVER.md index db1c9ea..ba3fe70 100644 --- a/LANEHANDOVER.md +++ b/LANEHANDOVER.md @@ -2394,3 +2394,41 @@ Worth remembering: **a health check that cannot distinguish "refused" from Reset is **00:00 UTC = 10:00 AEST**. An evening session in Australia will always find it spent; a morning one gets ~150 free images first. Today's poster re-rolls were the first CF-generated sprites in the project. +## SESSION — FABLE-SOLO-26 · 2026-07-22 + +**Branch:** main (solo, Fable) · **Gate:** lint ✓ build ✓ test ✓ (793 tests, 54 files) +**Deployed** + verified. + +### The back office — the haul finally means something +The sweep was collecting an untouched kebab, a single vintage sneaker and +seventeen bobby pins, then throwing the lot away as one integer. Now there is a +screen between the swept floor and the paperwork: you tip the night onto a desk +and feed it to a COIN-U-SORT 400, one thing at a time. + +- **`rules/haul.ts`** (pure, 10 tests): `tallyHaul(haul, pocketedBaggie)` gives + every item a FATE — cash `counted`, phone `rejected` (into a lost-property + tray that is a shoebox), curios `jammed`, the bag `refused`. Quips are picked + by POSITION, not chance, so a seed's night always tells the same joke. + `sorterVerdict()` signs off based on how badly you treated a machine worth $80. +- **`scenes/shared/HaulDeskScene`**: items fall into the hopper, the tally pops, + the machine physically shakes when it jams, and a ledger builds down the left + (`+ $20 / · JAM / + $6 / · not a coin / · lost property / + $50`) — the night + as a receipt. Booth tips are added at the end as cash the sorter "never needs + to know about". +- The bag gets a different line depending on the call you made hours earlier: + pocketed, "the sorter has no slot for this. neither, legally, do you."; binned, + the desk notes your restraint in pencil. +- `Sweep` now returns `SweepResult { cash, haul, pocketedBaggie }` instead of a + bare number, so the desk can replay what you actually picked up. + +### Driven from update(), NOT tween callbacks — deliberately +First cut chained the ceremony off `tween.onComplete` + `delayedCall` and it hung +at item 1 in the pane. That is the kebab-run lesson from SOLO-10 (tweens stall +under the stepped clock) but the REAL reason to change it is softlock class: a +dropped callback would strand the player on this screen with no route to the +paperwork. Tweens here decorate; `update()` owns the state machine. + +### And a bug the verification caught +The desk handed off to NightSummary while **staying active underneath it** — +nobody stopped it. Same class as the dev-route bug that left a live Night +driving a dead Door. `go()` now stops itself before calling `next()`. diff --git a/src/data/strings/floor.ts b/src/data/strings/floor.ts index 5122f6f..1ef94ea 100644 --- a/src/data/strings/floor.ts +++ b/src/data/strings/floor.ts @@ -164,6 +164,18 @@ export const NO_AUTHORITY = { moot: 'it resolved itself, the way these things do when nobody is coming.', } as const; +/** The back office at 3:40 AM, and a machine being asked to do too much. */ +export const HAUL_UI = { + title: 'THE BACK OFFICE', + subtitle: '3:40 AM · tip it out · the sorter will handle it · the sorter will not handle it', + machineName: 'COIN-U-SORT 400', + tallyLabel: 'TONIGHT', + doneItem: 'pockets empty. desk covered. machine humbled.', + /** {n} = cash somebody actually handed you during the shift. */ + plusPocket: 'plus ${n} from the shoe, which the sorter never needs to know about.', + prompt: 'click for the paperwork · ESC to skip the ceremony', +} as const; + /** Radio command (design §6): the staff on the other end of the walkie. */ export const STAFF_RADIO = { hint: 'RADIO — 1 bump (mess) · 2 shan (bar) · 3 kayden (rope)', diff --git a/src/main.ts b/src/main.ts index bf2843b..e5c179e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,7 @@ import { NightSummaryScene } from './scenes/door/NightSummaryScene'; import { IncidentReportScene } from './scenes/door/IncidentReportScene'; import { FloorDemoScene } from './scenes/floor/FloorDemoScene'; import { RosterScene } from './scenes/shared/RosterScene'; +import { HaulDeskScene } from './scenes/shared/HaulDeskScene'; import { JuiceDemoScene } from './ui/JuiceDemoScene'; import { MixDeskScene } from './ui/MixDeskScene'; @@ -33,6 +34,7 @@ const game = new Phaser.Game({ NightScene, DoorScene, NightSummaryScene, + HaulDeskScene, IncidentReportScene, new FloorDemoScene('Floor'), BootScene, diff --git a/src/rules/haul.ts b/src/rules/haul.ts new file mode 100644 index 0000000..7531544 --- /dev/null +++ b/src/rules/haul.ts @@ -0,0 +1,95 @@ +// The back office, 3:40 AM. You tip the night out of your pockets onto the +// desk and feed it to the coin sorter — a machine built for one job, being +// asked to do several. +// +// Pure sorting here; scenes/shared/HaulDeskScene owns the desk, the falling and +// the noise. The comedy is entirely in what the machine will and will not +// accept: cash sails through, everything else is a small institutional crisis. + +import type { Drop, DropKind } from './floorScore'; + +/** What the sorter does with a thing. */ +export type SorterFate = 'counted' | 'rejected' | 'jammed' | 'refused'; + +export interface HaulLine { + label: string; + value: number; + fate: SorterFate; + /** The machine's opinion, printed under the tally. */ + quip: string; +} + +export interface HaulTally { + lines: HaulLine[]; + /** Cash the sorter actually counted. */ + counted: number; + jams: number; +} + +/** Sorter behaviour per kind. Money is the only thing it was designed for. */ +const FATE: Record = { + note: 'counted', + coins: 'counted', + phone: 'rejected', + curio: 'jammed', + baggie: 'refused', +}; + +const JAM_QUIPS: readonly string[] = [ + 'the sorter thinks about it for a long moment.', + 'the sorter makes a noise it has never made before.', + 'something inside the sorter gives up.', + 'the sorter rejects this on principle.', + 'the sorter would like to speak to someone about this.', + 'the machine stops. the machine considers its career.', +]; + +const COUNT_QUIPS: readonly string[] = [ + 'in it goes.', + 'the sorter purrs.', + 'clunk.', + 'the satisfying one.', +]; + +export const REJECT_QUIP = 'slides out the side into the LOST PROPERTY tray. the tray is a shoebox.'; +export const REFUSE_QUIP = 'the sorter has no slot for this. neither, legally, do you.'; +export const REFUSE_BINNED_QUIP = 'binned hours ago. the desk notes your restraint, in pencil, somewhere.'; + +/** + * Feed the night's haul through the machine, in the order you picked it up. + * Deterministic: quips are chosen by position, not by chance, so the same + * night always tells the same joke — the seed owns the humour too. + */ +export function tallyHaul(haul: readonly Drop[], pocketedBaggie: boolean): HaulTally { + const lines: HaulLine[] = []; + let counted = 0; + let jams = 0; + + haul.forEach((drop, i) => { + const fate = FATE[drop.kind]; + let quip: string; + if (fate === 'counted') { + counted += drop.value; + quip = COUNT_QUIPS[i % COUNT_QUIPS.length]!; + } else if (fate === 'rejected') { + quip = REJECT_QUIP; + } else if (fate === 'refused') { + // A binned bag never reaches the desk; a pocketed one very much does. + quip = pocketedBaggie ? REFUSE_QUIP : REFUSE_BINNED_QUIP; + } else { + jams++; + quip = JAM_QUIPS[jams % JAM_QUIPS.length]!; + } + lines.push({ label: drop.label, value: drop.value, fate, quip }); + }); + + return { lines, counted, jams }; +} + +/** The sign-off, earned by how badly you jammed a machine worth $80. */ +export function sorterVerdict(t: HaulTally): string { + if (t.lines.length === 0) return 'nothing in the pockets. a clean night, or a boring one.'; + if (t.jams === 0) return 'the sorter had a lovely evening. so did you, financially.'; + if (t.jams >= 4) return 'the sorter will need to be looked at. by someone. eventually.'; + return 'the sorter survives to count another night.'; +} diff --git a/src/scenes/door/NightScene.ts b/src/scenes/door/NightScene.ts index f0132c8..f0898dc 100644 --- a/src/scenes/door/NightScene.ts +++ b/src/scenes/door/NightScene.ts @@ -133,7 +133,7 @@ export interface NightContext { kaydenHold?: () => 'sent' | 'cooldown'; /** The floor score: lights on at close, sweep what the night dropped. * Registered by the floor; the night calls it before the summary. */ - beginSweep?: (done: (score: { cash: number }) => void) => void; + beginSweep?: (done: (score: import('../floor/sweep').SweepResult) => void) => void; /** * Cash taken during the shift, banked as it is taken. Pushed rather than read * at close so it survives a night that ends early — you keep what you were @@ -615,7 +615,16 @@ export class NightScene extends Phaser.Scene { this.applyLocation('floor'); this.ctx.beginSweep((score) => { this.log.floorCash = score.cash; - proceed(); + // The back office: you tip the night out and feed it to the sorter + // before anyone lets you near the paperwork. + this.scene.stop('Door'); + this.scene.stop('Floor'); + this.scene.start('HaulDesk', { + haul: score.haul, + pocketedBaggie: score.pocketedBaggie, + pocketCash: this.log.pocketCash, + next: proceed, + }); }); } else { proceed(); diff --git a/src/scenes/floor/sweep.ts b/src/scenes/floor/sweep.ts index 71cdcc9..cbbe6f1 100644 --- a/src/scenes/floor/sweep.ts +++ b/src/scenes/floor/sweep.ts @@ -32,6 +32,13 @@ export interface SweepHost { playCoin(): void; } +/** What the night hands the office desk (scenes/shared/HaulDeskScene). */ +export interface SweepResult { + cash: number; + haul: Drop[]; + pocketedBaggie: boolean; +} + interface Lying { drop: Drop; x: number; @@ -42,7 +49,11 @@ interface Lying { export class Sweep { private active = false; private cash = 0; - private done: ((score: { cash: number }) => void) | null = null; + private done: ((score: SweepResult) => void) | null = null; + /** Everything actually picked up, in the order it left the floor. */ + private readonly haul: Drop[] = []; + /** Did the bag go in the pocket rather than the bin? The desk asks. */ + private pocketedBaggie = false; private lying: Lying[] = []; /** The bag freezes your feet until you make the call. */ private baggiePending: Drop | null = null; @@ -62,10 +73,12 @@ export class Sweep { } /** The night is over; the lights come on; the floor pays out. */ - begin(rng: RngStream, crowdSize: number, done: (score: { cash: number }) => void): void { + begin(rng: RngStream, crowdSize: number, done: (score: SweepResult) => void): void { this.active = true; this.done = done; this.cash = 0; + this.haul.length = 0; + this.pocketedBaggie = false; this.baggiePending = null; // Everyone out. The crowd sim keeps the bodies; the lights do not. @@ -159,6 +172,7 @@ export class Sweep { d.gfx.destroy(); this.lying = this.lying.filter((x) => x !== d); const { drop } = d; + this.haul.push(drop); if (drop.kind === 'baggie') { this.baggiePending = drop; this.host.toast(drop.label); @@ -177,6 +191,7 @@ export class Sweep { private resolveBaggie(pocket: boolean): void { this.baggiePending = null; + this.pocketedBaggie = pocket; if (pocket) { this.host.logIncident('baggiePocketed', undefined, 'Contraband off the floor. Retained by security.'); this.host.toast(SWEEP_UI.baggiePocketed); @@ -193,7 +208,7 @@ export class Sweep { this.host.toast(SWEEP_UI.done.replace('{cash}', String(this.cash))); const cb = this.done; this.done = null; - const cash = this.cash; - this.scene.time.delayedCall(900, () => cb?.({ cash })); + const result: SweepResult = { cash: this.cash, haul: [...this.haul], pocketedBaggie: this.pocketedBaggie }; + this.scene.time.delayedCall(900, () => cb?.(result)); } } diff --git a/src/scenes/shared/HaulDeskScene.ts b/src/scenes/shared/HaulDeskScene.ts new file mode 100644 index 0000000..87cc067 --- /dev/null +++ b/src/scenes/shared/HaulDeskScene.ts @@ -0,0 +1,237 @@ +import Phaser from 'phaser'; +import { tallyHaul, sorterVerdict, type HaulLine } from '../../rules/haul'; +import type { Drop } from '../../rules/floorScore'; +import { HAUL_UI } from '../../data/strings/floor'; + +// The back office, 3:40 AM. Between the swept floor and the paperwork, you tip +// the night out onto the desk and feed it to the coin sorter one thing at a +// time. Cash goes through. Everything else is a small institutional crisis. +// +// Deliberately NOT interactive beyond "next": the joke is watching a machine +// fail at its job, and rushing it is the only verb anyone wants. +// +// The SEQUENCE is driven from update(), not from tween.onComplete chains. Two +// reasons, one of them learned the hard way here already (the kebab run, +// SOLO-10): tweens stall under the preview pane's stepped clock so a callback +// chain is unverifiable, and — the real one — a dropped callback would strand +// the player on this screen with no way to the paperwork. Tweens here decorate; +// they never advance the state machine. + +const W = 640; +const H = 360; +const MONO = 'monospace'; + +/** Real ms an item takes to fall from the hand into the mouth of the sorter. */ +const FALL_MS = 520; +/** Beat after it lands, before the next one goes in. */ +const BEAT_MS = 620; +/** A jam costs the machine a longer, more embarrassing pause. */ +const JAM_MS = 1150; + +export interface HaulDeskData { + haul: Drop[]; + pocketedBaggie: boolean; + /** Cash already in your pocket from the shift (booth tips). Not sorted. */ + pocketCash: number; + next: () => void; +} + +export class HaulDeskScene extends Phaser.Scene { + private args!: HaulDeskData; + private lines: HaulLine[] = []; + private idx = 0; + private counted = 0; + private tallyText!: Phaser.GameObjects.Text; + private quipText!: Phaser.GameObjects.Text; + private itemText!: Phaser.GameObjects.Text; + private sorter!: Phaser.GameObjects.Container; + private ledger!: Phaser.GameObjects.Text; + private finished = false; + /** Sequence state, advanced by update() alone. */ + private phase: 'wait' | 'falling' | 'settling' = 'wait'; + private phaseMs = 0; + private phaseFor = 700; + private falling: Phaser.GameObjects.Arc | null = null; + private tally!: ReturnType; + + constructor() { + super('HaulDesk'); + } + + init(args: HaulDeskData): void { + this.args = args; + } + + create(): void { + const t = tallyHaul(this.args.haul, this.args.pocketedBaggie); + this.lines = t.lines; + + this.add.rectangle(W / 2, H / 2, W, H, 0x14110c); + // the desk: a slab of laminate under a single tired bulb + this.add.ellipse(W / 2, 96, 460, 220, 0x2a2318, 0.55); + this.add.rectangle(W / 2, 250, W, 220, 0x3a2f1e); + this.add.rectangle(W / 2, 142, W, 3, 0x241c12); + + this.add + .text(W / 2, 20, HAUL_UI.title, { fontFamily: MONO, fontSize: '15px', color: '#f0e4d0' }) + .setOrigin(0.5); + this.add + .text(W / 2, 38, HAUL_UI.subtitle, { fontFamily: MONO, fontSize: '8px', color: '#8a7c60' }) + .setOrigin(0.5); + + this.buildSorter(); + + this.itemText = this.add + .text(W / 2, 60, '', { fontFamily: MONO, fontSize: '9px', color: '#e8dcc4', align: 'center', wordWrap: { width: 420 } }) + .setOrigin(0.5, 0); + this.quipText = this.add + .text(W / 2, 300, '', { fontFamily: MONO, fontSize: '8px', color: '#a09070', align: 'center', wordWrap: { width: 460 } }) + .setOrigin(0.5, 0); + this.tallyText = this.add + .text(W - 18, 250, '$0', { fontFamily: MONO, fontSize: '26px', color: '#f0d060' }) + .setOrigin(1, 0.5); + this.add + .text(W - 18, 232, HAUL_UI.tallyLabel, { fontFamily: MONO, fontSize: '7px', color: '#8a7c60' }) + .setOrigin(1, 0.5); + this.ledger = this.add + .text(18, 176, '', { fontFamily: MONO, fontSize: '7px', color: '#6a5c44', lineSpacing: 2 }); + + this.tally = t; + const skip = (): void => this.finish(t); + this.input.keyboard?.once('keydown-ESC', skip); + } + + /** The only thing that moves the ceremony along. */ + override update(_time: number, deltaMs: number): void { + if (this.finished) return; + this.phaseMs += deltaMs; + + if (this.phase === 'falling' && this.falling) { + // Hand-interpolated so the fall is visible in a stepped clock too. + const k = Math.min(1, this.phaseMs / FALL_MS); + this.falling.y = 84 + (168 - 84) * k * k; + } + if (this.phaseMs < this.phaseFor) return; + + this.phaseMs = 0; + if (this.phase === 'falling') { + this.falling?.destroy(); + this.falling = null; + const line = this.lines[this.idx - 1]; + if (line) this.land(line); + return; + } + // 'wait' or 'settling': the next thing goes in. + this.feedNext(); + } + + /** The machine: a hopper, a slot, and a tray that has seen things. */ + private buildSorter(): void { + const x = W / 2; + const y = 210; + const c = this.add.container(x, y); + this.sorter = c; + c.add(this.add.rectangle(0, 0, 150, 92, 0x5a5348).setStrokeStyle(2, 0x3a352c)); + c.add(this.add.rectangle(0, -38, 116, 20, 0x241f18)); // hopper mouth + c.add(this.add.rectangle(0, -46, 132, 8, 0x6a6355)); + for (let i = -2; i <= 2; i++) { + c.add(this.add.rectangle(i * 26, 22, 18, 26, 0x2a251c)); // coin channels + } + c.add(this.add.rectangle(0, 50, 150, 10, 0x46403a)); // the tray + c.add( + this.add + .text(0, -8, HAUL_UI.machineName, { fontFamily: MONO, fontSize: '7px', color: '#9a9080' }) + .setOrigin(0.5), + ); + } + + private feedNext(): void { + if (this.finished) return; + if (this.idx >= this.lines.length) return this.finish(this.tally); + const line = this.lines[this.idx]!; + this.idx++; + + this.itemText.setText(line.label); + this.quipText.setText(''); + + // The thing falls from the hand into the hopper. + const colour = + line.fate === 'counted' ? 0xf0d060 : line.fate === 'refused' ? 0xa0e0a0 : 0xc8bca0; + this.falling = this.add.circle(W / 2, 84, 6, colour).setDepth(5); + this.phase = 'falling'; + this.phaseFor = FALL_MS; + } + + private land(line: HaulLine): void { + this.quipText.setText(line.quip); + const push = (s: string): void => { + const prev = this.ledger.text ? this.ledger.text.split('\n') : []; + prev.push(s); + this.ledger.setText(prev.slice(-9).join('\n')); + }; + + if (line.fate === 'counted') { + this.counted += line.value; + this.tallyText.setText(`$${this.counted}`); + this.tweens.add({ targets: this.tallyText, scale: { from: 1.3, to: 1 }, duration: 180 }); + this.tweens.add({ targets: this.sorter, y: { from: 214, to: 210 }, duration: 90, yoyo: true }); + push(`+ $${line.value}`); + this.phase = 'settling'; + this.phaseFor = BEAT_MS; + return; + } + + if (line.fate === 'rejected') { + push('· lost property'); + this.phase = 'settling'; + this.phaseFor = BEAT_MS; + return; + } + + // A jam or a refusal: the machine shakes, thinks, and moves on with less + // dignity than it had. This is the whole reason the screen exists. + push(line.fate === 'refused' ? '· not a coin' : '· JAM'); + this.tweens.add({ + targets: this.sorter, + x: { from: W / 2 - 3, to: W / 2 + 3 }, + duration: 55, + yoyo: true, + repeat: line.fate === 'refused' ? 2 : 6, + onComplete: () => this.sorter.setX(W / 2), + }); + this.phase = 'settling'; + this.phaseFor = JAM_MS; + } + + private finish(t: ReturnType): void { + if (this.finished) return; + this.finished = true; + this.tweens.killAll(); + this.sorter.setPosition(W / 2, 210); + + // Everything the sorter never saw: what somebody handed you during the shift. + const total = t.counted + this.args.pocketCash; + this.counted = t.counted; + this.tallyText.setText(`$${total}`); + this.itemText.setText( + this.args.pocketCash > 0 + ? HAUL_UI.plusPocket.replace('{n}', String(this.args.pocketCash)) + : HAUL_UI.doneItem, + ); + this.quipText.setText(sorterVerdict(t)); + this.add + .text(W / 2, H - 14, HAUL_UI.prompt, { fontFamily: MONO, fontSize: '8px', color: '#8a7c60' }) + .setOrigin(0.5); + + // Stop OURSELVES on the way out. next() starts the summary but has no idea + // this scene exists, so without this the desk keeps running underneath the + // paperwork — same class as the dev-route bug that left a live Night + // driving a dead Door. + const go = (): void => { + this.scene.stop(); + this.args.next(); + }; + this.input.once('pointerdown', go); + this.input.keyboard?.once('keydown-SPACE', go); + } +} diff --git a/tests/haul.test.ts b/tests/haul.test.ts new file mode 100644 index 0000000..c5faa43 --- /dev/null +++ b/tests/haul.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from 'vitest'; +import { REFUSE_QUIP, REFUSE_BINNED_QUIP, tallyHaul, sorterVerdict } from '../src/rules/haul'; +import { rollDrops } from '../src/rules/floorScore'; +import { SeededRNG } from '../src/core/SeededRNG'; +import type { Drop } from '../src/rules/floorScore'; + +const d = (kind: Drop['kind'], value = 0, label = kind): Drop => ({ kind, value, label }); + +describe('the coin sorter', () => { + it('counts money and nothing else', () => { + const t = tallyHaul([d('note', 20), d('coins', 7), d('curio'), d('phone'), d('baggie')], true); + expect(t.counted).toBe(27); + }); + + it('jams on the objects, and that is the whole joke', () => { + const t = tallyHaul([d('curio'), d('curio'), d('note', 5)], false); + expect(t.jams).toBe(2); + expect(t.lines.filter((l) => l.fate === 'jammed')).toHaveLength(2); + expect(t.lines.find((l) => l.fate === 'counted')?.value).toBe(5); + }); + + it('the phone goes sideways into lost property, never into the total', () => { + const t = tallyHaul([d('phone')], false); + expect(t.lines[0]!.fate).toBe('rejected'); + expect(t.counted).toBe(0); + }); + + it('the bag gets a different line depending on what you did with it', () => { + expect(tallyHaul([d('baggie')], true).lines[0]!.quip).toBe(REFUSE_QUIP); + expect(tallyHaul([d('baggie')], false).lines[0]!.quip).toBe(REFUSE_BINNED_QUIP); + }); + + it('keeps the order you picked things up in — the desk replays your night', () => { + const haul: Drop[] = [ + { kind: 'note', value: 10, label: 'a' }, + { kind: 'curio', value: 0, label: 'b' }, + { kind: 'coins', value: 3, label: 'c' }, + ]; + expect(tallyHaul(haul, false).lines.map((l) => l.label)).toEqual(['a', 'b', 'c']); + }); + + it('is deterministic — the same night tells the same joke', () => { + const haul = rollDrops(new SeededRNG(4207).stream('sweep'), 40); + expect(tallyHaul(haul, true)).toEqual(tallyHaul(haul, true)); + }); + + it('every line has words, whatever the machine made of it', () => { + const haul = rollDrops(new SeededRNG(9).stream('sweep'), 60); + for (const line of tallyHaul(haul, true).lines) { + expect(line.quip.length).toBeGreaterThan(5); + expect(line.label.length).toBeGreaterThan(0); + } + }); + + it('an empty haul is handled, and says so', () => { + const t = tallyHaul([], false); + expect(t).toMatchObject({ counted: 0, jams: 0 }); + expect(t.lines).toEqual([]); + expect(sorterVerdict(t)).toContain('nothing in the pockets'); + }); + + it('the sign-off escalates with how badly you treated the machine', () => { + const clean = sorterVerdict(tallyHaul([d('note', 5)], false)); + const rough = sorterVerdict(tallyHaul(Array(5).fill(d('curio')), false)); + expect(clean).not.toBe(rough); + expect(rough).toContain('looked at'); + }); + + it('a real swept night always gives the desk something to do', () => { + for (const seed of [4207, 4208, 99]) { + const t = tallyHaul(rollDrops(new SeededRNG(seed).stream('sweep'), 40), true); + expect(t.lines.length).toBeGreaterThan(0); + expect(t.counted).toBeGreaterThan(0); + } + }); +});