Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ MELBCUPSIM is a digital preservation and computational simulation project, not a

## Features

- **Historical result replay:** 2023 (Without A Fight), 2024 (Knight's Choice) and 2025 (Half Yours). Published finishing orders are fixed; animated sectional motion is synthetic, never claimed as observed.
- **Historical preservation:** 11 races, spanning **1861–2025**. The 1988 Empire Rose and 2005 Makybe Diva Cups include documented full-field finishing orders. The six other historic additions are explicitly marked **TOP 3**: they depict only verified podium finishers, not entire fields. The 2023–25 replays remain intact. All intermediate motion is synthetic.
- **What-if races:** seeded synthetic performance profiles coded in balanced ternary (-1/0/+1), Good/Soft/Heavy scenarios and runner-specific aggressive/balanced/conservative tactics. Results are entertainment, not predictions.
- **Arcade:** 8-bit pixel horses with jockey silks, top-down circuit, leaderboard, commentary, speed, pause, tick advance, seek, and a follow-horse selector.
- **Proof-friendly:** integer centimetre state, deterministic PRNG, fixed 420-tick horizon, shared-config links, downloadable JSON evidence and SHA-256 of the canonical complete race timeline. Identical settings + model version => identical results.
Expand All @@ -32,6 +32,23 @@ node --test tests/*.test.mjs

Node is used only for built-in, dependency-free regression tests. **The game does not use Node.js.**

## Historic archive extension — v0.2.0

| Year | Finish positions represented | Archive scope | Evidence |
|---|---|---|---|
| 1861 | Archer, Mormon, Prince | Top three of 17 | [Australian Heritage Database](https://www.agriculture.gov.au/sites/default/files/documents/flemington.pdf) |
| 1930 | Phar Lap, Second Wind, Shadow King | Top three of 15 | [Racing and Sports](https://www.racingandsports.com.au/news/racing/melbourne-cup/0001-01-01/previous-melbourne-cup-winners/670826) |
| 1983 | Kiwi, Noble Comment, Mr Jazz | Top three of 24 | [Victoria Racing Club](https://www.vrc.com.au/racing/group-1-honour-board/) |
| 1987 | Kensei, Empire Rose, Rosedale | Top three of 21 | [Races.com.au](https://www.races.com.au/melbourne-cup/melbourne-cup-history/past-winners/1981-to-1990-melbourne-cup/) |
| 1988 | Empire Rose, Natski, Na Botto… | Full 22 | [Published full field](https://en.wikipedia.org/wiki/1988_Melbourne_Cup) + [VRC corroboration](https://www.vrc.com.au/racing/melbourne-cup-winners-honour-roll/1981-2000/) |
| 1989 | Tawrrific, Super Impose, Kudz | Top three of 23 | [Races.com.au](https://www.races.com.au/melbourne-cup/melbourne-cup-history/past-winners/1981-to-1990-melbourne-cup/) |
| 1990 | Kingston Rule, The Phantom, Mr Brooker | Top three of 24 | [Racing and Sports](https://www.racingandsports.com.au/news/racing/melbourne-cup/0001-01-01/previous-melbourne-cup-winners/670826) |
| 2005 | Makybe Diva, On A Jeune, Xcellent… | Full 24 | [Furlong Files](https://www.furlongfiles.com/melbourne-cup/results/?cup=2005) |

**Scope guarantee:** A TOP 3 exhibit shows three verified finishers from a historically larger field. It is *not* a three-runner historical race. No unsourced names or finishing places are invented; what-if mode is disabled for partial fields. Historical movement remains a deterministic visual reconstruction, not observed sectional or tracking data.

**1988 caveat:** The complete 22-place order comes from a secondary published field table. Empire Rose's victory and Natski's runner-up placing are independently corroborated by VRC; the entire order has not yet been reconciled against a primary race-book or Cavanough's complete historical results. [State Library Victoria](https://guides.slv.vic.gov.au/melbournecup/fields) identifies those archival resources. Older Cups also used period-specific distances and track layouts; the app's 3200-metre diagram is a universal schematic display, not an exact restoration.

## Accuracy contract and sources

| Cup | Preserved result source |
Expand Down
9 changes: 9 additions & 0 deletions docs/INVARIANTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@
- Track visual is an approximate stylized oval and does not claim to match the Flemington geometry or 3200m path precisely.
- All race positions are monotonic, integer centimetres, bounded by 0..320000. Timelines are 421 states, ticks 0..420. Date, random clock time and real-time networking are not model inputs.
- Changes to physics, PRNG, source archive, canonical serialization or rounding require an engine version bump and test fixtures to preserve compatibility.

## Historical archive scope v0.2.0
- Every race has evidence links, a documented field-size count, and coverage full or podium (legacy 2023–2025 records implicitly full).
- Podium mode displays **only verified first-three**, without inventing missing horses or treating three as total starters.
- What-if mode requires full coverage, enforced in engine as well as UI.
- All historical animation, lane positions and progress are simulated, never claimed GPS or contemporaneous sectionals.
- The 1988 full result is sourced to a secondary 22-place table with VRC podium corroboration; a complete independent primary-source reconciliation remains outstanding.
- Earlier Cups may have used different course geometry and nominal distances. The modern 3200m oval remains schematic.
- The historical archive update does not change the deterministic physics for original 2023–2025 records.
5 changes: 4 additions & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
- [x] Build-free GitHub Pages publishing and pure JS tests

## v0.2.0 — archival depth
- [ ] Extend the validated Cup archive year by year, keeping sources and DNF/dead-heat semantics
- [x] Add 1861, 1930, 1983, 1987–1990, 2005 historical records
- [x] Full 1988 Empire Rose (22) and 2005 Makybe Diva (24) finishing orders with evidence links
- [x] Mark partial podium exhibits and block what-if without known full field
- [ ] Extend archives year by year and independently cross-check 1988 full places against primary sources
- [ ] Add sourced barrier, weight and track condition metadata with explicit missing-value masks
- [ ] Add voluntary import of third-party licensed sectional timing where provenance permits
- [ ] Add a user-facing archive importer with strict schema, versioning and validation
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<aside class="panel controls">
<div class="section-title"><span>01</span> RACE CONTROL <small>CONFIGURE</small></div>
<label for="mode">RACE MODE</label><select id="mode"><option value="replay">Historical result replay</option><option value="whatif">What-if simulation</option></select>
<label for="year">CUP ARCHIVE</label><select id="year"><option value="2025">2025 · HALF YOURS</option><option value="2024">2024 · KNIGHT'S CHOICE</option><option value="2023">2023 · WITHOUT A FIGHT</option></select>
<label for="year">CUP ARCHIVE</label><select id="year"></select>
<label for="going">TRACK CONDITIONS</label><select id="going"><option value="good">GOOD · FAST</option><option value="soft" selected>SOFT · GIVE</option><option value="heavy">HEAVY · SLOG</option></select>
<label for="seed">DETERMINISTIC SEED</label><input id="seed" maxlength="64" value="QSOL-MCUP-001" spellcheck="false" autocomplete="off">
<label for="runner">FOLLOW RUNNER / TACTICAL CONTROL</label><select id="runner"></select>
Expand Down
10 changes: 10 additions & 0 deletions src/data.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
// Source-backed final placings; names are identifiers, NOT licensed footage or telemetry.
// 2025 includes a documented dead heat at 16th; 2023 includes a DNF.
// Partial podium exhibits never imply a three-runner historical field.
export const ARCHIVE = Object.freeze({
1861: {"date":"1861-11-07","title":"ARCHER WINS THE CUP","winner":"Archer","coverage":"podium","fieldSize":17,"source":"https://www.agriculture.gov.au/sites/default/files/documents/flemington.pdf","sourceLabel":"Australian Heritage Database","secondarySource":"https://www.nma.gov.au/defining-moments/resources/melbourne-cup","names":["Archer","Mormon","Prince"],"places":[1,2,3],"status":[]},
1930: {"date":"1930-11-04","title":"PHAR LAP WINS THE CUP","winner":"Phar Lap","coverage":"podium","fieldSize":15,"source":"https://www.racingandsports.com.au/news/racing/melbourne-cup/0001-01-01/previous-melbourne-cup-winners/670826","sourceLabel":"Racing and Sports","secondarySource":"https://www.vrc.com.au/racing/melbourne-cup-winners-honour-roll/1921-1940/","names":["Phar Lap","Second Wind","Shadow King"],"places":[1,2,3],"status":[]},
1983: {"date":"1983-11-01","title":"KIWI WINS THE CUP","winner":"Kiwi","coverage":"podium","fieldSize":24,"source":"https://www.vrc.com.au/racing/group-1-honour-board/","sourceLabel":"Victoria Racing Club","secondarySource":"https://www.vrc.com.au/latest-news/magical-milestones-kiwi/","names":["Kiwi","Noble Comment","Mr. Jazz"],"places":[1,2,3],"status":[]},
1987: {"date":"1987-11-03","title":"KENSEI WINS THE CUP","winner":"Kensei","coverage":"podium","fieldSize":21,"source":"https://www.races.com.au/melbourne-cup/melbourne-cup-history/past-winners/1981-to-1990-melbourne-cup/","sourceLabel":"Races.com.au","secondarySource":"https://www.vrc.com.au/racing/melbourne-cup-winners-honour-roll/1981-2000/","names":["Kensei","Empire Rose","Rosedale"],"places":[1,2,3],"status":[]},
1988: {"date":"1988-11-01","title":"EMPIRE ROSE WINS THE CUP","winner":"Empire Rose","coverage":"full","fieldSize":22,"source":"https://en.wikipedia.org/wiki/1988_Melbourne_Cup","sourceLabel":"1988 Melbourne Cup · published full field","secondarySource":"https://www.vrc.com.au/racing/melbourne-cup-winners-honour-roll/1981-2000/","names":["Empire Rose","Natski","Na Botto","Apollo Run","Round The World","Banderol","Ideal Centeman","Mr. Bunnythorpe","Full At Last","Kensei","Our Classic Boy","Tawrrific","Shantaroun","Copatonic","Secret Seal","Congressman","It's Candide","Algonquin Club","Authaal","Lord Hybrow","Balciano","Might"],"places":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22],"status":[]},
1989: {"date":"1989-11-07","title":"TAWRRIFIC WINS THE CUP","winner":"Tawrrific","coverage":"podium","fieldSize":23,"source":"https://www.races.com.au/melbourne-cup/melbourne-cup-history/past-winners/1981-to-1990-melbourne-cup/","sourceLabel":"Races.com.au","secondarySource":"https://www.vrc.com.au/racing/melbourne-cup-winners-honour-roll/1981-2000/","names":["Tawrrific","Super Impose","Kudz"],"places":[1,2,3],"status":[]},
1990: {"date":"1990-11-06","title":"KINGSTON RULE WINS THE CUP","winner":"Kingston Rule","coverage":"podium","fieldSize":24,"source":"https://www.racingandsports.com.au/news/racing/melbourne-cup/0001-01-01/previous-melbourne-cup-winners/670826","sourceLabel":"Racing and Sports","secondarySource":"https://www.vrc.com.au/racing/melbourne-cup-winners-honour-roll/1981-2000/","names":["Kingston Rule","The Phantom","Mr Brooker"],"places":[1,2,3],"status":[]},
2005: {"date":"2005-11-01","title":"MAKYBE DIVA WINS THE CUP","winner":"Makybe Diva","coverage":"full","fieldSize":24,"source":"https://www.furlongfiles.com/melbourne-cup/results/?cup=2005","sourceLabel":"Furlong Files · 2005 full result","secondarySource":"https://www.vrc.com.au/racing/melbourne-cup-winners-honour-roll/2001-2020/","names":["Makybe Diva","On A Jeune","Xcellent","Leica Falcon","Lachlan River","Portland Singa","Envoy","Vinnie Roe","Kindjhal","Rizon","Dizelle","Eye Popper","Demerger","Railings","Vouvray","Kamsky","Greys Inn","Strasbourg","Distinction","Umbula","Hugs Dancer","Bazelle","Mr Celebrity","Franklins Gardens"],"places":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24],"status":[]},

2025: {
date:'2025-11-04', title:'HALF YOURS TAKES THE CUP', winner:'Half Yours',
source:'https://www.abc.net.au/news/2025-11-01/melbourne-cup-2025-field-horses-form-guide-and-betting/105895400',
Expand Down
3 changes: 2 additions & 1 deletion src/engine.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function normalise(input={}){
const year=Number(input.year??2025);
getArchive(year);
const mode=input.mode==='whatif'?'whatif':'replay';
if(mode==='whatif' && getArchive(year).coverage==='podium') throw new RangeError('What-if requires a complete recorded field');
const going=['good','soft','heavy'].includes(input.going)?input.going:'soft';
const strategy=[-1,0,1].includes(Number(input.strategy))?Number(input.strategy):0;
const seed=String(input.seed??'QSOL-MCUP-001').slice(0,64)||'QSOL-MCUP-001';
Expand Down Expand Up @@ -89,7 +90,7 @@ export function orderAt(result,tick){
export function receiptPayload(result){
const {settings,frames,placings,version,archive}=result;
return {schema:'qsol-melbcupsim/1',engine:version,settings,
historicalProvenance:{year:settings.year,date:archive.date,source:archive.source,observed:'published finish positions only',syntheticMotion:true},
historicalProvenance:{year:settings.year,date:archive.date,source:archive.source,secondarySource:archive.secondarySource??null,coverage:archive.coverage??'full',recordedRunners:archive.names.length,fieldSize:archive.fieldSize??archive.names.length,observed:archive.coverage==='podium'?'published top three only':'published full finishing order',syntheticMotion:true},
outcome:placings,checkpoints:[0,100,200,300,420].map(tick=>({tick,positionsCm:frames[tick]})),
replayInstructions:'Open simulator with settings; rebuild timeline; compare sha256 of JSON.stringify({engine,settings,frames,outcome}).'};
}
Expand Down
26 changes: 19 additions & 7 deletions src/main.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import {getArchive} from './data.mjs';
import {ARCHIVE,getArchive} from './data.mjs';
import {ENGINE_VERSION,TRACK_CM,LAST_TICK,buildRace,orderAt,receiptPayload,canonicalRace} from './engine.mjs';
const $=id=>document.getElementById(id);
const canvas=$('track'),ctx=canvas.getContext('2d',{alpha:false});
const colors=['#ffe37e','#ff7e9b','#7fece1','#e5a0ff','#8cc8ff','#ffbc72','#d3fc7a','#fff','#ff9676','#9ee7d3','#b5a9ff','#f9d16c'];
let result,tick=0,playing=false,lastFrame=0,accum=0,digest='';let serial=0;
$('version').textContent=ENGINE_VERSION;
const params=new URLSearchParams(location.search);
// Populate the archive from source data; do not duplicate historical records in HTML.
for(const year of Object.keys(ARCHIVE).map(Number).sort((a,b)=>b-a)){
const archive=ARCHIVE[year],option=document.createElement('option');
option.value=String(year);
option.textContent=year+' · '+archive.winner.toUpperCase()+(archive.coverage==='podium'?' [TOP 3]':' [FULL]');
$('year').append(option);
}
$('year').value='2025';
for(const id of ['mode','year','going','seed','strategy']) if(params.has(id))$(id).value=params.get(id);
function config(){return {mode:$('mode').value,year:$('year').value,going:$('going').value,seed:$('seed').value,runner:$('runner').value,strategy:Number($('strategy').value)};}
function syncRunners(){const previous=$('runner').value;const archive=getArchive($('year').value);$('runner').replaceChildren();archive.names.forEach((name,i)=>{const o=document.createElement('option');o.value=name;o.textContent=String(i+1).padStart(2,'0')+' · '+name;$('runner').append(o);});const wanted=params.get('runner');$('runner').value=archive.names.includes(previous)?previous:archive.names.includes(wanted)?wanted:archive.names[0];}
Expand All @@ -14,10 +22,11 @@ function makeRace(){
playing=false;tick=0;accum=0;lastFrame=0;syncMode();
result=buildRace(config());digest='';$('seek').max=LAST_TICK;$('seek').value=0;
$('raceTitle').textContent=result.settings.year+' MELBOURNE CUP';
$('resultNote').textContent=result.settings.mode==='replay'?'PUBLISHED FINISH · SYNTHETIC MOTION':'ALTERNATE TIMELINE · SYNTHETIC PERFORMANCE';
const partial=result.archive.coverage==='podium';
$('resultNote').textContent=result.settings.mode==='replay'?(partial?'TOP-3 EXHIBIT · '+result.archive.fieldSize+' HISTORICAL STARTERS · SYNTHETIC MOTION':'PUBLISHED FULL FIELD · SYNTHETIC MOTION'):'ALTERNATE TIMELINE · SYNTHETIC PERFORMANCE';
$('sourceLink').href=result.archive.source;$('sourceLink').textContent=result.archive.sourceLabel+' ↗';
$('play').textContent='▶ START RACE';$('simStatus').textContent=result.settings.mode==='replay'?'ARCHIVE · FIXED OUTCOME':'WHAT-IF · SIMULATED';
paint();updateHUD();statement('READY · '+result.archive.names.length+' STARTERS · '+ENGINE_VERSION);
paint();updateHUD();statement('READY · '+result.archive.names.length+(partial?' VERIFIED PODIUM FINISHERS OF '+result.archive.fieldSize+' HISTORICAL STARTERS':' DOCUMENTED RUNNERS')+' · '+ENGINE_VERSION);
const ticket=++serial;
if(globalThis.crypto?.subtle){
const data=new TextEncoder().encode(canonicalRace(result));
Expand All @@ -29,9 +38,12 @@ function makeRace(){
}else{statement('Race ready · cryptographic hashing requires HTTPS or localhost');}
}
function syncMode(){
const partial=getArchive($('year').value).coverage==='podium';
$('mode').querySelector('option[value="whatif"]').disabled=partial;
if(partial) $('mode').value='replay';
const fixed=$('mode').value==='replay';
$('going').disabled=fixed;$('strategy').disabled=fixed;
$('modeHint').textContent=fixed?'Published finishing order is fixed. Seed changes only the illustrative mid-race animation. Track and tactics do not alter published results.':'Counterfactual play. Ternary (-1/0/+1) profiles are fabricated for gameplay. Choose a runner, adjust conditions and tactics, then share the seed.';
$('modeHint').textContent=partial?'PODIUM SHOWCASE ONLY: the three displayed horses were part of a larger historical field. Unknown finishing places are not invented. What-if requires a complete field.':fixed?'Published finishing order is fixed. Seed affects illustrative animation only. Track and tactics do not change results.':'Counterfactual play: ternary horse traits are synthetic. Choose a runner, conditions and tactics.';
}
function start(){if(tick>=LAST_TICK)tick=0;playing=!playing;lastFrame=0;accum=0;$('play').textContent=playing?'Ⅱ PAUSE':'▶ RESUME';updateHUD();}
$('play').addEventListener('click',start);
Expand Down Expand Up @@ -82,7 +94,7 @@ function drawCourse(){
text('FINISH',835,212,12,'#ffdf89','center');
text('MARIBYRNONG RIVER',45,477,10,'#8ccac4');
text('PIXEL CIRCUIT / NOT TO SCALE',30,79,10,'#d2ddbe');
text(result.settings.mode==='replay'?'ARCHIVAL RECONSTRUCTION':'ALTERNATE UNIVERSE',30,96,10,'#c7f586');
text(result.archive.coverage==='podium'?'TOP THREE ONLY / OTHERS NOT DEPICTED':result.settings.mode==='replay'?'ARCHIVAL RECONSTRUCTION':'ALTERNATE UNIVERSE',30,96,10,'#c7f586');
}
function horse(x,y,index,angle,watched,step){
ctx.save();ctx.translate(whole(x),whole(y));ctx.rotate(angle);
Expand Down Expand Up @@ -131,8 +143,8 @@ function updateHUD(){
row.append(p,n,v);return row;
}));
let message;
if(tick===0)message='THE RUNNERS ARE LOADING INTO THE BARRIER. GOOD LUCK IN THE PIXEL PADDOCK!';
else if(complete)message=result.settings.mode==='replay'?'HISTORY HOLDS! '+result.archive.winner.toUpperCase()+' WINS THE '+result.settings.year+' CUP.':'CHECK THE PHOTO FINISH! '+ordered[0].name.toUpperCase()+' WINS THIS ALTERNATE TIMELINE.';
if(tick===0)message=result.archive.coverage==='podium'?'PODIUM EXHIBIT: THREE VERIFIED FINISHERS ONLY. '+(result.archive.fieldSize-3)+' OTHER HISTORICAL STARTERS NOT SHOWN.':'THE RUNNERS ARE LOADING INTO THE BARRIER. GOOD LUCK IN THE PIXEL PADDOCK!';
else if(complete)message=result.settings.mode==='replay'?'HISTORY HOLDS! '+result.archive.winner.toUpperCase()+' WINS THE '+result.settings.year+' CUP. '+(result.archive.coverage==='podium'?'ONLY THE TOP THREE WERE DEPICTED.':''):'CHECK THE PHOTO FINISH! '+ordered[0].name.toUpperCase()+' WINS THIS ALTERNATE TIMELINE.';
else if(percent<25)message='AND THEY\'RE OFF! '+front.name.toUpperCase()+' SHOWS EARLY SPEED.';
else if(percent<60)message='SETTLING INTO THE BACK STRETCH. '+front.name.toUpperCase()+' HAS THE ADVANTAGE.';
else if(percent<85)message='THE FIELD IS TURNING FOR HOME! '+front.name.toUpperCase()+' LEADS THE CHARGE.';
Expand Down
Loading
Loading