Establishes time conversions
This commit is contained in:
220
docs/architecture/simulation-clock.md
Normal file
220
docs/architecture/simulation-clock.md
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
<!-- CLAUDE CODE INSTRUCTIONS
|
||||||
|
Git protocol: git fetch origin main && git merge origin main BEFORE writing.
|
||||||
|
File to write: docs/architecture/simulation-clock.md
|
||||||
|
Action: CREATE new file at the path above with the content below this header block.
|
||||||
|
Commit message: "docs: add simulation clock canonical document"
|
||||||
|
Push: git push origin main
|
||||||
|
Rebuild required: NO
|
||||||
|
PM2 restart required: NO
|
||||||
|
Do not touch any other file.
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Simulation Clock
|
||||||
|
## Integer Time Architecture for OTIVM and the Simulator
|
||||||
|
### TheRON — OTIVM / CIVICVS / TESSERA Stack
|
||||||
|
### Status: Canonical. Do not alter without project owner instruction.
|
||||||
|
### Date: 2026-05-02
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Purpose
|
||||||
|
|
||||||
|
This document defines the governing time model for OTIVM and the Simulator. It
|
||||||
|
is a hard architectural constraint, not a preference. Every timed element in the
|
||||||
|
system — routes, scenarios, otium intervals, epoch calculations — must conform
|
||||||
|
to the integer clock rule defined here. A future assistant or developer who
|
||||||
|
bypasses this constraint will corrupt the behavioral record.
|
||||||
|
|
||||||
|
Read this document before touching any value that involves time, duration, or
|
||||||
|
simulated date.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. The Governing Constant
|
||||||
|
|
||||||
|
```
|
||||||
|
MS_PER_SIM_DAY = 3,000
|
||||||
|
```
|
||||||
|
|
||||||
|
One simulated day equals 3,000 real milliseconds — 3 real seconds.
|
||||||
|
|
||||||
|
This is a game compression constant. It has no historical basis and is not
|
||||||
|
intended to represent any real ratio of Roman time. Its purpose is to make the
|
||||||
|
game playable without making the simulation clock meaningless. It is defined
|
||||||
|
once, in `src/constants.js`, and read from there everywhere it is used. It is
|
||||||
|
never hardcoded in any other file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The Integer Constraint
|
||||||
|
|
||||||
|
**All `duration_ms` values must be exact integer multiples of `MS_PER_SIM_DAY`.**
|
||||||
|
|
||||||
|
This is not a preference. It is a hard requirement.
|
||||||
|
|
||||||
|
A `duration_ms` that is not a multiple of `MS_PER_SIM_DAY` produces a
|
||||||
|
fractional `duration_days` value in `venture_legs`. Fractional days corrupt the
|
||||||
|
behavioral record. The Simulator reads `duration_days` as an integer count. The
|
||||||
|
epoch arithmetic depends on integer day counts. A single fractional value
|
||||||
|
propagates error through every downstream calculation.
|
||||||
|
|
||||||
|
### The enforcement rule
|
||||||
|
|
||||||
|
When adding a route, scenario, or any timed game element:
|
||||||
|
|
||||||
|
1. Decide the duration in **simulated days** first. This is the primary value.
|
||||||
|
2. Derive `duration_ms` by multiplication: `duration_ms = duration_days × MS_PER_SIM_DAY`
|
||||||
|
3. Never set `duration_ms` independently and then divide to get `duration_days`.
|
||||||
|
|
||||||
|
The days are primary. The milliseconds are derived. Always in that order.
|
||||||
|
|
||||||
|
### The adjustment record
|
||||||
|
|
||||||
|
When `MS_PER_SIM_DAY` was set to 3,000, the Grain route (Brundisium →
|
||||||
|
Carthago) had a `duration_ms` of 13,000, which is not divisible by 3,000. It
|
||||||
|
was adjusted to 12,000ms (4 simulated days). The difference is 1 real second —
|
||||||
|
imperceptible in gameplay. This adjustment was made deliberately and is
|
||||||
|
recorded here so it is not treated as an error in future review.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Current Route Durations
|
||||||
|
|
||||||
|
| Route | From | To | mode | duration_ms | duration_days |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| olive | Ostia | Capua | road | 6,000 | 2 |
|
||||||
|
| wine | Capua | Brundisium | road | 9,000 | 3 |
|
||||||
|
| grain | Brundisium | Carthago | sea | 12,000 | 4 |
|
||||||
|
| linen | Carthago | Alexandria | sea | 18,000 | 6 |
|
||||||
|
|
||||||
|
All values are exact integer multiples of `MS_PER_SIM_DAY = 3,000`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Otium Duration
|
||||||
|
|
||||||
|
```
|
||||||
|
OTIUM_DURATION_MS = 9,000 (3 simulated days)
|
||||||
|
```
|
||||||
|
|
||||||
|
Otium takes 3 simulated days. This value was chosen because:
|
||||||
|
|
||||||
|
- Otium is purposeful social activity, not passive rest. It requires time
|
||||||
|
proportional to its function.
|
||||||
|
- 3 days gives otium weight relative to the shortest route (2 days). A merchant
|
||||||
|
who completes the olive run and then takes otium has a 5-day cycle — a
|
||||||
|
working week with meaning.
|
||||||
|
- The otium duration is a social minimum, not a function of route distance. A
|
||||||
|
merchant returning from Alexandria takes the same minimum otium as one
|
||||||
|
returning from Capua. The social obligations otium represents do not scale
|
||||||
|
with distance travelled.
|
||||||
|
|
||||||
|
This produces the following complete cycles:
|
||||||
|
|
||||||
|
| Route | route days | otium days | cycle total |
|
||||||
|
|---|---|---|---|
|
||||||
|
| olive | 2 | 3 | 5 |
|
||||||
|
| wine | 3 | 3 | 6 |
|
||||||
|
| grain | 4 | 3 | 7 |
|
||||||
|
| linen | 6 | 3 | 9 |
|
||||||
|
|
||||||
|
All integers. Pairs of linen runs with one otium: 6 + 6 + 3 = 15 days. Integer
|
||||||
|
throughout.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. The Simulation Epoch Window
|
||||||
|
|
||||||
|
The simulation runs from **1 January 100 BCE to 31 December 100 CE**.
|
||||||
|
|
||||||
|
| Boundary | Calendar date | Julian Day Number |
|
||||||
|
|---|---|---|
|
||||||
|
| Start | 1 January 100 BCE | 1684595 |
|
||||||
|
| End | 31 December 100 CE | 1757641 |
|
||||||
|
| Span | — | 73,047 simulated days |
|
||||||
|
|
||||||
|
The epoch anchor is `SIM_EPOCH_JDN_START = 1684595`. All simulated dates are
|
||||||
|
integer offsets from this anchor. The window closes when the simulated JDN
|
||||||
|
exceeds `SIM_EPOCH_JDN_END = 1757641`.
|
||||||
|
|
||||||
|
73,047 is well within the safe range of JavaScript integer arithmetic. No
|
||||||
|
floating point risk. No BigInt required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. What Is Never Stored
|
||||||
|
|
||||||
|
Simulated dates are **never stored in the database**.
|
||||||
|
|
||||||
|
`recorded_at` is always real wall-clock UTC in ISO 8601 format. This is the
|
||||||
|
only timestamp column in the schema. Simulated dates are always derived at
|
||||||
|
computation time from `recorded_at` and the session anchor.
|
||||||
|
|
||||||
|
The database stores facts. Derivations are computation. This separation must
|
||||||
|
never be violated.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Session Time Arithmetic
|
||||||
|
|
||||||
|
Each player session has a `started_at` timestamp recorded in `actor_profile`.
|
||||||
|
|
||||||
|
Simulated days elapsed in a session:
|
||||||
|
|
||||||
|
```
|
||||||
|
elapsed_sim_days = floor((now_ms - started_at_ms) / MS_PER_SIM_DAY)
|
||||||
|
```
|
||||||
|
|
||||||
|
This is integer arithmetic throughout. `floor()` ensures no fractional days
|
||||||
|
enter the calculation.
|
||||||
|
|
||||||
|
Current simulated JDN for a session:
|
||||||
|
|
||||||
|
```
|
||||||
|
current_jdn = SIM_EPOCH_JDN_START + elapsed_sim_days
|
||||||
|
```
|
||||||
|
|
||||||
|
Integer addition. No floating point at any step.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Future Route and Scenario Additions
|
||||||
|
|
||||||
|
Before adding any route or timed scenario element, the author must:
|
||||||
|
|
||||||
|
1. Verify that `duration_days` is a positive integer.
|
||||||
|
2. Compute `duration_ms = duration_days × MS_PER_SIM_DAY`.
|
||||||
|
3. Record both values. Never record only one.
|
||||||
|
4. If `MS_PER_SIM_DAY` has changed since the last route was added, recompute
|
||||||
|
all existing `duration_ms` values. The constant is the source of truth.
|
||||||
|
|
||||||
|
Changing `MS_PER_SIM_DAY` is a breaking change. It requires updating every
|
||||||
|
`duration_ms` in `constants.js` and every `duration_days` value in existing
|
||||||
|
player databases. It must not be changed without project owner instruction and
|
||||||
|
a documented migration plan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Confidence and Maturity
|
||||||
|
|
||||||
|
The compression ratio (`MS_PER_SIM_DAY = 3,000`) is a design decision, not a
|
||||||
|
historical fact. It carries no `confidence_tag` because it is not a parameter
|
||||||
|
derived from the Roman world. It is infrastructure.
|
||||||
|
|
||||||
|
The epoch window (100 BCE to 100 CE) is historically grounded in the
|
||||||
|
project's design intent — the period when Rome is most fully itself, as
|
||||||
|
documented in `docs/DESCENSUS-genesis.md`. The JDN values are computed from
|
||||||
|
the proleptic Julian calendar and are verifiable.
|
||||||
|
|
||||||
|
Route durations in simulated days (`duration_days`) are currently tagged
|
||||||
|
`confidence_tag: 'estimated'` in the behavioral record. They reflect
|
||||||
|
compressed game time, not historical journey durations. When the corpus
|
||||||
|
develops grounded data on route travel times, new `duration_days` values will
|
||||||
|
enter the database as updated records. The compression ratio will adjust
|
||||||
|
`duration_ms` accordingly, following the enforcement rule in Section 2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Simulation Clock — 2026-05-02*
|
||||||
|
*TheRON — single contributor. AI assistants implement, document, flag — do not direct.*
|
||||||
|
*A constraint recorded here is a constraint enforced everywhere. No exceptions.*
|
||||||
Reference in New Issue
Block a user