diff --git a/docs/MAINTENANCE.md b/docs/MAINTENANCE.md index 3dbb52b..1f9021e 100644 --- a/docs/MAINTENANCE.md +++ b/docs/MAINTENANCE.md @@ -63,7 +63,7 @@ The share-preview artwork is generated by `scripts/build-social-image.py`, using ## When? scoring versions -New When? practice rounds and daily rounds dated October 16, 2026 onward carry +When? daily rounds dated October 16–23, 2026 and saved version-two rounds carry `timelineScoringVersion: 2` and a frozen `scoringYear`. The zero-point error bound is `max(5, 0.25 * max(0, scoringYear - answerYear))` years. Score linearly as `round(100 * max(0, 1 - abs(guessYear - answerYear) / bound))`. A four-year miss @@ -136,3 +136,15 @@ historical motion questions retain path distance; v2 without the trace marker retains intersection-over-union, including all October 16 daily and saved rounds. Other question objects and game rules are unchanged. Unknown trace versions and trace markers on other games are rejected on restore. + +## Gentler When? allowance (October 24, 2026) + +New practice and daily dates from October 24 carry `timelineScoringVersion: 3`, +with the same frozen `scoringYear` and overall `scoringVersion: 2`. The bound is +`max(10, max(0, scoringYear - answerYear) / 3)` years, using one-third exactly. +Points remain linear: `round(100 * clamp(1 - abs(guess-answer)/bound, 0, 1))`. +A seven-year miss on a 30-year-old event earns 30; on a 60-year-old event, 65; +on a 100-year-old event, 79. A recent four-year miss earns 60. +Version two keeps `max(5, age * .25)`; unversioned questions keep their original +50-year squared scoring. Never rescore by the player's current date. No facts, +assignments, IDs, other game rules or previously generated questions are changed. diff --git a/public/about.html b/public/about.html index 831f5d5..ccffc17 100644 --- a/public/about.html +++ b/public/about.html @@ -46,7 +46,7 @@

Eleven ways to test your judgement

How scoring works

Time uses linear scoring in daily challenges from October 16, 2026, and in new practice rounds. Its zero-point allowance is the greater of 1.25 seconds or 50% of the target duration. Only an exact match earns full accuracy; the minimum allowance is not a free-error window. Earlier daily rounds and saved results keep their original scoring.

-

When? uses linear scoring in daily challenges from October 16, 2026, and in new practice rounds. Its zero-point allowance is the greater of five years or 25% of the event’s age, measured in the challenge year. Within the five-year allowance, each year missed costs 20 points: a four-year miss earns 20. Earlier daily rounds and saved results retain their original scoring.

+

When? uses a zero-point allowance of ten years or one-third of the event’s age, whichever is larger, in new practice rounds and daily challenges from October 24, 2026. Age is measured in the challenge year. For an event up to 30 years old, a four-year miss earns 60 points and a seven-year miss earns 30. October 16–23 challenges and saved version-two rounds keep the greater of five years or 25% of age. Earlier questions retain their original scoring.

All new practice rounds and daily challenges from October 16, 2026 use linear scoring. Each game measures error against its own zero-point bound, then awards 100 times the remaining closeness. Using 10% of that allowance earns 90 points; 25% earns 75; half earns 50. An exact match earns 100, and reaching the bound earns zero.

For the new linear scoring version, 3D view reaches zero points at 90° of rotation error; Angles reaches zero at 180°. A 45° miss earns 50 points in 3D view, while a 90° miss earns 50 in Angles.

Size it up scores width and height independently, each worth up to 50 points, then rounds the combined score. A 5% miss in both dimensions earns 90/100. A 50% error in one dimension earns zero for that dimension. Earlier daily rounds and saved questions retain their original scoring.

diff --git a/public/focus.html b/public/focus.html index 43d28cb..5410891 100644 --- a/public/focus.html +++ b/public/focus.html @@ -70,7 +70,7 @@

The games & their allowances

Angles 180°

Turn the line to the requested angle. Measure counterclockwise from the right-facing baseline.

We use the shortest angular distance. 45° off earns 75; 90° off earns 50; 180° off earns zero.

Color memory One wheel radius

Remember the swatch for two seconds, then choose its color on the wheel. Hue goes around the wheel; vividness increases toward the edge.

We measure straight-line distance between your pick and the target on the wheel. Half a wheel radius away earns 50 points; one radius or more earns zero. This measures wheel position, rather than differences between hex codes.

Time 50% of the target, minimum 1.25 s

Press Start, wait for the requested time, then Stop. There’s no clock until the reveal.

The zero-point miss is half the target duration or 1.25 seconds, whichever is larger. For a 10-second target, 2.5 seconds early or late earns 50; 5 seconds off earns zero. For a 2-second target, the allowance is 1.25 seconds.

-
When? 25% of its age, minimum 5 years

Scroll the year wheel to put your answer in the center. Lock it in to see the answer and source.

The zero-point miss is one-quarter of the event’s age or five years, whichever is larger. For a recent event, four years off still earns 20 points. For an event 100 years old, the allowance is 25 years. Age is fixed to the challenge year, so an old challenge’s scoring stays the same.

+
When? ⅓ of its age, minimum 10 years

Scroll the year wheel to put your answer in the center. Lock it in to see the answer and source.

The zero-point miss is one-third of the event’s age or ten years, whichever is larger. For events up to 30 years old, four years off earns 60 points and seven years off earns 30. For an event 60 years old, the allowance is 20 years; seven years off earns 65 points. Age is fixed to the challenge year, so an old challenge’s scoring stays the same.

This allowance applies to new practice rounds and Daily Fives from October 24, 2026. October 16–23 challenges and previously saved version-two rounds keep the five-year / 25%-of-age allowance. Earlier rounds retain their original rules.

How long? 50% of the answer

Turn the duration dial to your best estimate. Use the unit controls and +/− buttons to refine it, then lock in to see the answer and source.

A guess 50% shorter or longer earns zero. For a two-hour answer, 90 minutes or 2½ hours earns 50; one hour or three hours earns zero. We score the duration shown by the controls.

Touch, mouse & keyboard

Drag, tap, or click the game controls. Focus a control to use the keyboard; arrow keys adjust most answers. Size it up uses left/right for width and up/down for height, with Shift for larger steps. For Line memory, arrows move the cursor and Space adds a point. Color memory also has hue and saturation sliders.

Memory games are silent. Switching away resets an active memory preview or timer.

@@ -274,7 +274,7 @@

The games & their allowances

if(q.scoringVersion!==undefined&&q.scoringVersion!==2)return false; if(q.traceScoringVersion!==undefined&&(q.traceScoringVersion!==3||q.type!=='motion'||q.scoringVersion!==2))return false; if(q.scoringVersion===2&&q.type==='time'&&q.timeScoringVersion!==2)return false; - if(q.scoringVersion===2&&q.type==='timeline'&&q.timelineScoringVersion!==2)return false; + if(q.scoringVersion===2&&q.type==='timeline'&&![2,3].includes(q.timelineScoringVersion))return false; if(q.source&&(!Array.isArray(q.source)||!/^https:\/\//.test(q.source[1])))return false; const pairs=points=>Array.isArray(points)&&points.length>=3&&points.every(p=>Array.isArray(p)&&p.length===2&&p.every(Number.isFinite)); if(q.type==='view3d'&&(!q.shape||!Array.isArray(q.shape.boxes)||q.shape.boxes.length<3||q.shape.boxes.length>5||!q.shape.boxes.every(b=>Array.isArray(b)&&b.length===8&&b.slice(0,6).every(Number.isFinite)&&b.slice(3,6).every(n=>n>0)&&/^#[0-9a-f]{6}$/i.test(b[6])&&Array.isArray(b[7])&&b[7].length===6&&b[7].every(v=>typeof v==='boolean'))))return false; @@ -285,7 +285,7 @@

The games & their allowances

if(q.type==='colour'&&(!Array.isArray(q.answer)||q.answer.length!==3))return false; if(['angle','time','rhythm','balance','timeline','duration'].includes(q.type)&&!q.fruit&&typeof q.answer!=='number')return false; if(q.type==='time'&&q.timeScoringVersion!==undefined&&(q.timeScoringVersion!==2||q.answer<=0))return false; - if(q.type==='timeline'&&q.timelineScoringVersion!==undefined&&(q.timelineScoringVersion!==2||!Number.isInteger(q.scoringYear)||q.scoringYear<1850||q.scoringYear>9999))return false; + if(q.type==='timeline'&&q.timelineScoringVersion!==undefined&&(![2,3].includes(q.timelineScoringVersion)||!Number.isInteger(q.scoringYear)||q.scoringYear<1850||q.scoringYear>9999))return false; if(q.type==='proportion'){ if(isSizeQuestion(q)){ if(!Number.isInteger(q.wide)||q.wide<1||q.wide>5||!Number.isInteger(q.tall)||q.tall<1||q.tall>4||!Number.isInteger(q.reference)||q.reference<32||q.reference>54||q.answer?.width!==q.wide*q.reference||q.answer?.height!==q.tall*q.reference||q.answer.width>260||q.answer.height>175||!q.start||q.start.width<32||q.start.width>304||q.start.height<32||q.start.height>207)return false; @@ -985,7 +985,7 @@

The games & their allowances

function scheduledTriviaQuestion(id,challengeDay=null){ const f=TRIVIA_BY_ID[id];if(!f)throw new Error('Missing scheduled trivia'); const q={id:f.id,type:f.type,skill:f.type==='timeline'?'When?':'How long?',title:f.title,answer:f.answer,caption:f.caption,explain:f.explain,source:[f.source.name,f.source.url],tip:'Answer: '+(f.type==='timeline'?f.answer:durationText(f.answer))}; - if(f.type==='timeline'&&(challengeDay===null||dayNumber(challengeDay)>=dayNumber(TIMELINE_SCORING_START))){q.timelineScoringVersion=2;q.scoringYear=Number((challengeDay||calendarDay()).split('-')[0]);} + if(f.type==='timeline'&&(challengeDay===null||dayNumber(challengeDay)>=dayNumber(TIMELINE_SCORING_START))){q.timelineScoringVersion=challengeDay===null||dayNumber(challengeDay)>=dayNumber(TIMELINE_GENTLE_START)?3:2;q.scoringYear=Number((challengeDay||calendarDay()).split('-')[0]);} return q; } @@ -1153,11 +1153,12 @@

The games & their allowances

function perspectiveDiagonal(q){return q.scoringVersion===2?PERSPECTIVE_INNER_DIAGONAL:Math.hypot(400,300);} const TIMELINE_SCORING_START=LINEAR_SCORING_START; +const TIMELINE_GENTLE_START='2026-10-24'; const TIME_SCORING_START=LINEAR_SCORING_START; function usesRelativeTime(q){return q.type==='time'&&q.timeScoringVersion===2;} function timeErrorBound(q){return usesRelativeTime(q)?Math.max(1.25,q.answer*.5):SCORING_RANGES.time;} -function usesAgeBasedTimeline(q){return q.type==='timeline'&&q.timelineScoringVersion===2;} -function timelineErrorBound(q){return usesAgeBasedTimeline(q)?Math.max(5,Math.max(0,q.scoringYear-q.answer)*.25):SCORING_RANGES.timeline;} +function usesAgeBasedTimeline(q){return q.type==='timeline'&&[2,3].includes(q.timelineScoringVersion);} +function timelineErrorBound(q){const age=Math.max(0,q.scoringYear-q.answer);return q.timelineScoringVersion===3?Math.max(10,age/3):usesAgeBasedTimeline(q)?Math.max(5,age*.25):SCORING_RANGES.timeline;} function linearScoreFromError(fraction){return Number.isFinite(fraction)?Math.round(100*Math.max(0,Math.min(1,1-fraction))):0;} function wheelDistance(a,b){ const position=rgb=>{const {h,s}=rgbHsv(rgb),angle=h*Math.PI/180; diff --git a/public/index.html b/public/index.html index 43d28cb..5410891 100644 --- a/public/index.html +++ b/public/index.html @@ -70,7 +70,7 @@

The games & their allowances

Angles 180°

Turn the line to the requested angle. Measure counterclockwise from the right-facing baseline.

We use the shortest angular distance. 45° off earns 75; 90° off earns 50; 180° off earns zero.

Color memory One wheel radius

Remember the swatch for two seconds, then choose its color on the wheel. Hue goes around the wheel; vividness increases toward the edge.

We measure straight-line distance between your pick and the target on the wheel. Half a wheel radius away earns 50 points; one radius or more earns zero. This measures wheel position, rather than differences between hex codes.

Time 50% of the target, minimum 1.25 s

Press Start, wait for the requested time, then Stop. There’s no clock until the reveal.

The zero-point miss is half the target duration or 1.25 seconds, whichever is larger. For a 10-second target, 2.5 seconds early or late earns 50; 5 seconds off earns zero. For a 2-second target, the allowance is 1.25 seconds.

-
When? 25% of its age, minimum 5 years

Scroll the year wheel to put your answer in the center. Lock it in to see the answer and source.

The zero-point miss is one-quarter of the event’s age or five years, whichever is larger. For a recent event, four years off still earns 20 points. For an event 100 years old, the allowance is 25 years. Age is fixed to the challenge year, so an old challenge’s scoring stays the same.

+
When? ⅓ of its age, minimum 10 years

Scroll the year wheel to put your answer in the center. Lock it in to see the answer and source.

The zero-point miss is one-third of the event’s age or ten years, whichever is larger. For events up to 30 years old, four years off earns 60 points and seven years off earns 30. For an event 60 years old, the allowance is 20 years; seven years off earns 65 points. Age is fixed to the challenge year, so an old challenge’s scoring stays the same.

This allowance applies to new practice rounds and Daily Fives from October 24, 2026. October 16–23 challenges and previously saved version-two rounds keep the five-year / 25%-of-age allowance. Earlier rounds retain their original rules.

How long? 50% of the answer

Turn the duration dial to your best estimate. Use the unit controls and +/− buttons to refine it, then lock in to see the answer and source.

A guess 50% shorter or longer earns zero. For a two-hour answer, 90 minutes or 2½ hours earns 50; one hour or three hours earns zero. We score the duration shown by the controls.

Touch, mouse & keyboard

Drag, tap, or click the game controls. Focus a control to use the keyboard; arrow keys adjust most answers. Size it up uses left/right for width and up/down for height, with Shift for larger steps. For Line memory, arrows move the cursor and Space adds a point. Color memory also has hue and saturation sliders.

Memory games are silent. Switching away resets an active memory preview or timer.

@@ -274,7 +274,7 @@

The games & their allowances

if(q.scoringVersion!==undefined&&q.scoringVersion!==2)return false; if(q.traceScoringVersion!==undefined&&(q.traceScoringVersion!==3||q.type!=='motion'||q.scoringVersion!==2))return false; if(q.scoringVersion===2&&q.type==='time'&&q.timeScoringVersion!==2)return false; - if(q.scoringVersion===2&&q.type==='timeline'&&q.timelineScoringVersion!==2)return false; + if(q.scoringVersion===2&&q.type==='timeline'&&![2,3].includes(q.timelineScoringVersion))return false; if(q.source&&(!Array.isArray(q.source)||!/^https:\/\//.test(q.source[1])))return false; const pairs=points=>Array.isArray(points)&&points.length>=3&&points.every(p=>Array.isArray(p)&&p.length===2&&p.every(Number.isFinite)); if(q.type==='view3d'&&(!q.shape||!Array.isArray(q.shape.boxes)||q.shape.boxes.length<3||q.shape.boxes.length>5||!q.shape.boxes.every(b=>Array.isArray(b)&&b.length===8&&b.slice(0,6).every(Number.isFinite)&&b.slice(3,6).every(n=>n>0)&&/^#[0-9a-f]{6}$/i.test(b[6])&&Array.isArray(b[7])&&b[7].length===6&&b[7].every(v=>typeof v==='boolean'))))return false; @@ -285,7 +285,7 @@

The games & their allowances

if(q.type==='colour'&&(!Array.isArray(q.answer)||q.answer.length!==3))return false; if(['angle','time','rhythm','balance','timeline','duration'].includes(q.type)&&!q.fruit&&typeof q.answer!=='number')return false; if(q.type==='time'&&q.timeScoringVersion!==undefined&&(q.timeScoringVersion!==2||q.answer<=0))return false; - if(q.type==='timeline'&&q.timelineScoringVersion!==undefined&&(q.timelineScoringVersion!==2||!Number.isInteger(q.scoringYear)||q.scoringYear<1850||q.scoringYear>9999))return false; + if(q.type==='timeline'&&q.timelineScoringVersion!==undefined&&(![2,3].includes(q.timelineScoringVersion)||!Number.isInteger(q.scoringYear)||q.scoringYear<1850||q.scoringYear>9999))return false; if(q.type==='proportion'){ if(isSizeQuestion(q)){ if(!Number.isInteger(q.wide)||q.wide<1||q.wide>5||!Number.isInteger(q.tall)||q.tall<1||q.tall>4||!Number.isInteger(q.reference)||q.reference<32||q.reference>54||q.answer?.width!==q.wide*q.reference||q.answer?.height!==q.tall*q.reference||q.answer.width>260||q.answer.height>175||!q.start||q.start.width<32||q.start.width>304||q.start.height<32||q.start.height>207)return false; @@ -985,7 +985,7 @@

The games & their allowances

function scheduledTriviaQuestion(id,challengeDay=null){ const f=TRIVIA_BY_ID[id];if(!f)throw new Error('Missing scheduled trivia'); const q={id:f.id,type:f.type,skill:f.type==='timeline'?'When?':'How long?',title:f.title,answer:f.answer,caption:f.caption,explain:f.explain,source:[f.source.name,f.source.url],tip:'Answer: '+(f.type==='timeline'?f.answer:durationText(f.answer))}; - if(f.type==='timeline'&&(challengeDay===null||dayNumber(challengeDay)>=dayNumber(TIMELINE_SCORING_START))){q.timelineScoringVersion=2;q.scoringYear=Number((challengeDay||calendarDay()).split('-')[0]);} + if(f.type==='timeline'&&(challengeDay===null||dayNumber(challengeDay)>=dayNumber(TIMELINE_SCORING_START))){q.timelineScoringVersion=challengeDay===null||dayNumber(challengeDay)>=dayNumber(TIMELINE_GENTLE_START)?3:2;q.scoringYear=Number((challengeDay||calendarDay()).split('-')[0]);} return q; } @@ -1153,11 +1153,12 @@

The games & their allowances

function perspectiveDiagonal(q){return q.scoringVersion===2?PERSPECTIVE_INNER_DIAGONAL:Math.hypot(400,300);} const TIMELINE_SCORING_START=LINEAR_SCORING_START; +const TIMELINE_GENTLE_START='2026-10-24'; const TIME_SCORING_START=LINEAR_SCORING_START; function usesRelativeTime(q){return q.type==='time'&&q.timeScoringVersion===2;} function timeErrorBound(q){return usesRelativeTime(q)?Math.max(1.25,q.answer*.5):SCORING_RANGES.time;} -function usesAgeBasedTimeline(q){return q.type==='timeline'&&q.timelineScoringVersion===2;} -function timelineErrorBound(q){return usesAgeBasedTimeline(q)?Math.max(5,Math.max(0,q.scoringYear-q.answer)*.25):SCORING_RANGES.timeline;} +function usesAgeBasedTimeline(q){return q.type==='timeline'&&[2,3].includes(q.timelineScoringVersion);} +function timelineErrorBound(q){const age=Math.max(0,q.scoringYear-q.answer);return q.timelineScoringVersion===3?Math.max(10,age/3):usesAgeBasedTimeline(q)?Math.max(5,age*.25):SCORING_RANGES.timeline;} function linearScoreFromError(fraction){return Number.isFinite(fraction)?Math.round(100*Math.max(0,Math.min(1,1-fraction))):0;} function wheelDistance(a,b){ const position=rgb=>{const {h,s}=rgbHsv(rgb),angle=h*Math.PI/180; diff --git a/public/night-lab.html b/public/night-lab.html index 43d28cb..5410891 100644 --- a/public/night-lab.html +++ b/public/night-lab.html @@ -70,7 +70,7 @@

The games & their allowances

Angles 180°

Turn the line to the requested angle. Measure counterclockwise from the right-facing baseline.

We use the shortest angular distance. 45° off earns 75; 90° off earns 50; 180° off earns zero.

Color memory One wheel radius

Remember the swatch for two seconds, then choose its color on the wheel. Hue goes around the wheel; vividness increases toward the edge.

We measure straight-line distance between your pick and the target on the wheel. Half a wheel radius away earns 50 points; one radius or more earns zero. This measures wheel position, rather than differences between hex codes.

Time 50% of the target, minimum 1.25 s

Press Start, wait for the requested time, then Stop. There’s no clock until the reveal.

The zero-point miss is half the target duration or 1.25 seconds, whichever is larger. For a 10-second target, 2.5 seconds early or late earns 50; 5 seconds off earns zero. For a 2-second target, the allowance is 1.25 seconds.

-
When? 25% of its age, minimum 5 years

Scroll the year wheel to put your answer in the center. Lock it in to see the answer and source.

The zero-point miss is one-quarter of the event’s age or five years, whichever is larger. For a recent event, four years off still earns 20 points. For an event 100 years old, the allowance is 25 years. Age is fixed to the challenge year, so an old challenge’s scoring stays the same.

+
When? ⅓ of its age, minimum 10 years

Scroll the year wheel to put your answer in the center. Lock it in to see the answer and source.

The zero-point miss is one-third of the event’s age or ten years, whichever is larger. For events up to 30 years old, four years off earns 60 points and seven years off earns 30. For an event 60 years old, the allowance is 20 years; seven years off earns 65 points. Age is fixed to the challenge year, so an old challenge’s scoring stays the same.

This allowance applies to new practice rounds and Daily Fives from October 24, 2026. October 16–23 challenges and previously saved version-two rounds keep the five-year / 25%-of-age allowance. Earlier rounds retain their original rules.

How long? 50% of the answer

Turn the duration dial to your best estimate. Use the unit controls and +/− buttons to refine it, then lock in to see the answer and source.

A guess 50% shorter or longer earns zero. For a two-hour answer, 90 minutes or 2½ hours earns 50; one hour or three hours earns zero. We score the duration shown by the controls.

Touch, mouse & keyboard

Drag, tap, or click the game controls. Focus a control to use the keyboard; arrow keys adjust most answers. Size it up uses left/right for width and up/down for height, with Shift for larger steps. For Line memory, arrows move the cursor and Space adds a point. Color memory also has hue and saturation sliders.

Memory games are silent. Switching away resets an active memory preview or timer.

@@ -274,7 +274,7 @@

The games & their allowances

if(q.scoringVersion!==undefined&&q.scoringVersion!==2)return false; if(q.traceScoringVersion!==undefined&&(q.traceScoringVersion!==3||q.type!=='motion'||q.scoringVersion!==2))return false; if(q.scoringVersion===2&&q.type==='time'&&q.timeScoringVersion!==2)return false; - if(q.scoringVersion===2&&q.type==='timeline'&&q.timelineScoringVersion!==2)return false; + if(q.scoringVersion===2&&q.type==='timeline'&&![2,3].includes(q.timelineScoringVersion))return false; if(q.source&&(!Array.isArray(q.source)||!/^https:\/\//.test(q.source[1])))return false; const pairs=points=>Array.isArray(points)&&points.length>=3&&points.every(p=>Array.isArray(p)&&p.length===2&&p.every(Number.isFinite)); if(q.type==='view3d'&&(!q.shape||!Array.isArray(q.shape.boxes)||q.shape.boxes.length<3||q.shape.boxes.length>5||!q.shape.boxes.every(b=>Array.isArray(b)&&b.length===8&&b.slice(0,6).every(Number.isFinite)&&b.slice(3,6).every(n=>n>0)&&/^#[0-9a-f]{6}$/i.test(b[6])&&Array.isArray(b[7])&&b[7].length===6&&b[7].every(v=>typeof v==='boolean'))))return false; @@ -285,7 +285,7 @@

The games & their allowances

if(q.type==='colour'&&(!Array.isArray(q.answer)||q.answer.length!==3))return false; if(['angle','time','rhythm','balance','timeline','duration'].includes(q.type)&&!q.fruit&&typeof q.answer!=='number')return false; if(q.type==='time'&&q.timeScoringVersion!==undefined&&(q.timeScoringVersion!==2||q.answer<=0))return false; - if(q.type==='timeline'&&q.timelineScoringVersion!==undefined&&(q.timelineScoringVersion!==2||!Number.isInteger(q.scoringYear)||q.scoringYear<1850||q.scoringYear>9999))return false; + if(q.type==='timeline'&&q.timelineScoringVersion!==undefined&&(![2,3].includes(q.timelineScoringVersion)||!Number.isInteger(q.scoringYear)||q.scoringYear<1850||q.scoringYear>9999))return false; if(q.type==='proportion'){ if(isSizeQuestion(q)){ if(!Number.isInteger(q.wide)||q.wide<1||q.wide>5||!Number.isInteger(q.tall)||q.tall<1||q.tall>4||!Number.isInteger(q.reference)||q.reference<32||q.reference>54||q.answer?.width!==q.wide*q.reference||q.answer?.height!==q.tall*q.reference||q.answer.width>260||q.answer.height>175||!q.start||q.start.width<32||q.start.width>304||q.start.height<32||q.start.height>207)return false; @@ -985,7 +985,7 @@

The games & their allowances

function scheduledTriviaQuestion(id,challengeDay=null){ const f=TRIVIA_BY_ID[id];if(!f)throw new Error('Missing scheduled trivia'); const q={id:f.id,type:f.type,skill:f.type==='timeline'?'When?':'How long?',title:f.title,answer:f.answer,caption:f.caption,explain:f.explain,source:[f.source.name,f.source.url],tip:'Answer: '+(f.type==='timeline'?f.answer:durationText(f.answer))}; - if(f.type==='timeline'&&(challengeDay===null||dayNumber(challengeDay)>=dayNumber(TIMELINE_SCORING_START))){q.timelineScoringVersion=2;q.scoringYear=Number((challengeDay||calendarDay()).split('-')[0]);} + if(f.type==='timeline'&&(challengeDay===null||dayNumber(challengeDay)>=dayNumber(TIMELINE_SCORING_START))){q.timelineScoringVersion=challengeDay===null||dayNumber(challengeDay)>=dayNumber(TIMELINE_GENTLE_START)?3:2;q.scoringYear=Number((challengeDay||calendarDay()).split('-')[0]);} return q; } @@ -1153,11 +1153,12 @@

The games & their allowances

function perspectiveDiagonal(q){return q.scoringVersion===2?PERSPECTIVE_INNER_DIAGONAL:Math.hypot(400,300);} const TIMELINE_SCORING_START=LINEAR_SCORING_START; +const TIMELINE_GENTLE_START='2026-10-24'; const TIME_SCORING_START=LINEAR_SCORING_START; function usesRelativeTime(q){return q.type==='time'&&q.timeScoringVersion===2;} function timeErrorBound(q){return usesRelativeTime(q)?Math.max(1.25,q.answer*.5):SCORING_RANGES.time;} -function usesAgeBasedTimeline(q){return q.type==='timeline'&&q.timelineScoringVersion===2;} -function timelineErrorBound(q){return usesAgeBasedTimeline(q)?Math.max(5,Math.max(0,q.scoringYear-q.answer)*.25):SCORING_RANGES.timeline;} +function usesAgeBasedTimeline(q){return q.type==='timeline'&&[2,3].includes(q.timelineScoringVersion);} +function timelineErrorBound(q){const age=Math.max(0,q.scoringYear-q.answer);return q.timelineScoringVersion===3?Math.max(10,age/3):usesAgeBasedTimeline(q)?Math.max(5,age*.25):SCORING_RANGES.timeline;} function linearScoreFromError(fraction){return Number.isFinite(fraction)?Math.round(100*Math.max(0,Math.min(1,1-fraction))):0;} function wheelDistance(a,b){ const position=rgb=>{const {h,s}=rgbHsv(rgb),angle=h*Math.PI/180; diff --git a/public/studio.html b/public/studio.html index 43d28cb..5410891 100644 --- a/public/studio.html +++ b/public/studio.html @@ -70,7 +70,7 @@

The games & their allowances

Angles 180°

Turn the line to the requested angle. Measure counterclockwise from the right-facing baseline.

We use the shortest angular distance. 45° off earns 75; 90° off earns 50; 180° off earns zero.

Color memory One wheel radius

Remember the swatch for two seconds, then choose its color on the wheel. Hue goes around the wheel; vividness increases toward the edge.

We measure straight-line distance between your pick and the target on the wheel. Half a wheel radius away earns 50 points; one radius or more earns zero. This measures wheel position, rather than differences between hex codes.

Time 50% of the target, minimum 1.25 s

Press Start, wait for the requested time, then Stop. There’s no clock until the reveal.

The zero-point miss is half the target duration or 1.25 seconds, whichever is larger. For a 10-second target, 2.5 seconds early or late earns 50; 5 seconds off earns zero. For a 2-second target, the allowance is 1.25 seconds.

-
When? 25% of its age, minimum 5 years

Scroll the year wheel to put your answer in the center. Lock it in to see the answer and source.

The zero-point miss is one-quarter of the event’s age or five years, whichever is larger. For a recent event, four years off still earns 20 points. For an event 100 years old, the allowance is 25 years. Age is fixed to the challenge year, so an old challenge’s scoring stays the same.

+
When? ⅓ of its age, minimum 10 years

Scroll the year wheel to put your answer in the center. Lock it in to see the answer and source.

The zero-point miss is one-third of the event’s age or ten years, whichever is larger. For events up to 30 years old, four years off earns 60 points and seven years off earns 30. For an event 60 years old, the allowance is 20 years; seven years off earns 65 points. Age is fixed to the challenge year, so an old challenge’s scoring stays the same.

This allowance applies to new practice rounds and Daily Fives from October 24, 2026. October 16–23 challenges and previously saved version-two rounds keep the five-year / 25%-of-age allowance. Earlier rounds retain their original rules.

How long? 50% of the answer

Turn the duration dial to your best estimate. Use the unit controls and +/− buttons to refine it, then lock in to see the answer and source.

A guess 50% shorter or longer earns zero. For a two-hour answer, 90 minutes or 2½ hours earns 50; one hour or three hours earns zero. We score the duration shown by the controls.

Touch, mouse & keyboard

Drag, tap, or click the game controls. Focus a control to use the keyboard; arrow keys adjust most answers. Size it up uses left/right for width and up/down for height, with Shift for larger steps. For Line memory, arrows move the cursor and Space adds a point. Color memory also has hue and saturation sliders.

Memory games are silent. Switching away resets an active memory preview or timer.

@@ -274,7 +274,7 @@

The games & their allowances

if(q.scoringVersion!==undefined&&q.scoringVersion!==2)return false; if(q.traceScoringVersion!==undefined&&(q.traceScoringVersion!==3||q.type!=='motion'||q.scoringVersion!==2))return false; if(q.scoringVersion===2&&q.type==='time'&&q.timeScoringVersion!==2)return false; - if(q.scoringVersion===2&&q.type==='timeline'&&q.timelineScoringVersion!==2)return false; + if(q.scoringVersion===2&&q.type==='timeline'&&![2,3].includes(q.timelineScoringVersion))return false; if(q.source&&(!Array.isArray(q.source)||!/^https:\/\//.test(q.source[1])))return false; const pairs=points=>Array.isArray(points)&&points.length>=3&&points.every(p=>Array.isArray(p)&&p.length===2&&p.every(Number.isFinite)); if(q.type==='view3d'&&(!q.shape||!Array.isArray(q.shape.boxes)||q.shape.boxes.length<3||q.shape.boxes.length>5||!q.shape.boxes.every(b=>Array.isArray(b)&&b.length===8&&b.slice(0,6).every(Number.isFinite)&&b.slice(3,6).every(n=>n>0)&&/^#[0-9a-f]{6}$/i.test(b[6])&&Array.isArray(b[7])&&b[7].length===6&&b[7].every(v=>typeof v==='boolean'))))return false; @@ -285,7 +285,7 @@

The games & their allowances

if(q.type==='colour'&&(!Array.isArray(q.answer)||q.answer.length!==3))return false; if(['angle','time','rhythm','balance','timeline','duration'].includes(q.type)&&!q.fruit&&typeof q.answer!=='number')return false; if(q.type==='time'&&q.timeScoringVersion!==undefined&&(q.timeScoringVersion!==2||q.answer<=0))return false; - if(q.type==='timeline'&&q.timelineScoringVersion!==undefined&&(q.timelineScoringVersion!==2||!Number.isInteger(q.scoringYear)||q.scoringYear<1850||q.scoringYear>9999))return false; + if(q.type==='timeline'&&q.timelineScoringVersion!==undefined&&(![2,3].includes(q.timelineScoringVersion)||!Number.isInteger(q.scoringYear)||q.scoringYear<1850||q.scoringYear>9999))return false; if(q.type==='proportion'){ if(isSizeQuestion(q)){ if(!Number.isInteger(q.wide)||q.wide<1||q.wide>5||!Number.isInteger(q.tall)||q.tall<1||q.tall>4||!Number.isInteger(q.reference)||q.reference<32||q.reference>54||q.answer?.width!==q.wide*q.reference||q.answer?.height!==q.tall*q.reference||q.answer.width>260||q.answer.height>175||!q.start||q.start.width<32||q.start.width>304||q.start.height<32||q.start.height>207)return false; @@ -985,7 +985,7 @@

The games & their allowances

function scheduledTriviaQuestion(id,challengeDay=null){ const f=TRIVIA_BY_ID[id];if(!f)throw new Error('Missing scheduled trivia'); const q={id:f.id,type:f.type,skill:f.type==='timeline'?'When?':'How long?',title:f.title,answer:f.answer,caption:f.caption,explain:f.explain,source:[f.source.name,f.source.url],tip:'Answer: '+(f.type==='timeline'?f.answer:durationText(f.answer))}; - if(f.type==='timeline'&&(challengeDay===null||dayNumber(challengeDay)>=dayNumber(TIMELINE_SCORING_START))){q.timelineScoringVersion=2;q.scoringYear=Number((challengeDay||calendarDay()).split('-')[0]);} + if(f.type==='timeline'&&(challengeDay===null||dayNumber(challengeDay)>=dayNumber(TIMELINE_SCORING_START))){q.timelineScoringVersion=challengeDay===null||dayNumber(challengeDay)>=dayNumber(TIMELINE_GENTLE_START)?3:2;q.scoringYear=Number((challengeDay||calendarDay()).split('-')[0]);} return q; } @@ -1153,11 +1153,12 @@

The games & their allowances

function perspectiveDiagonal(q){return q.scoringVersion===2?PERSPECTIVE_INNER_DIAGONAL:Math.hypot(400,300);} const TIMELINE_SCORING_START=LINEAR_SCORING_START; +const TIMELINE_GENTLE_START='2026-10-24'; const TIME_SCORING_START=LINEAR_SCORING_START; function usesRelativeTime(q){return q.type==='time'&&q.timeScoringVersion===2;} function timeErrorBound(q){return usesRelativeTime(q)?Math.max(1.25,q.answer*.5):SCORING_RANGES.time;} -function usesAgeBasedTimeline(q){return q.type==='timeline'&&q.timelineScoringVersion===2;} -function timelineErrorBound(q){return usesAgeBasedTimeline(q)?Math.max(5,Math.max(0,q.scoringYear-q.answer)*.25):SCORING_RANGES.timeline;} +function usesAgeBasedTimeline(q){return q.type==='timeline'&&[2,3].includes(q.timelineScoringVersion);} +function timelineErrorBound(q){const age=Math.max(0,q.scoringYear-q.answer);return q.timelineScoringVersion===3?Math.max(10,age/3):usesAgeBasedTimeline(q)?Math.max(5,age*.25):SCORING_RANGES.timeline;} function linearScoreFromError(fraction){return Number.isFinite(fraction)?Math.round(100*Math.max(0,Math.min(1,1-fraction))):0;} function wheelDistance(a,b){ const position=rgb=>{const {h,s}=rgbHsv(rgb),angle=h*Math.PI/180; diff --git a/src/design-options/about.html b/src/design-options/about.html index a18167c..940a72a 100644 --- a/src/design-options/about.html +++ b/src/design-options/about.html @@ -27,7 +27,7 @@

Eleven ways to test your judgement

How scoring works

Time uses linear scoring in daily challenges from October 16, 2026, and in new practice rounds. Its zero-point allowance is the greater of 1.25 seconds or 50% of the target duration. Only an exact match earns full accuracy; the minimum allowance is not a free-error window. Earlier daily rounds and saved results keep their original scoring.

-

When? uses linear scoring in daily challenges from October 16, 2026, and in new practice rounds. Its zero-point allowance is the greater of five years or 25% of the event’s age, measured in the challenge year. Within the five-year allowance, each year missed costs 20 points: a four-year miss earns 20. Earlier daily rounds and saved results retain their original scoring.

+

When? uses a zero-point allowance of ten years or one-third of the event’s age, whichever is larger, in new practice rounds and daily challenges from October 24, 2026. Age is measured in the challenge year. For an event up to 30 years old, a four-year miss earns 60 points and a seven-year miss earns 30. October 16–23 challenges and saved version-two rounds keep the greater of five years or 25% of age. Earlier questions retain their original scoring.

All new practice rounds and daily challenges from October 16, 2026 use linear scoring. Each game measures error against its own zero-point bound, then awards 100 times the remaining closeness. Using 10% of that allowance earns 90 points; 25% earns 75; half earns 50. An exact match earns 100, and reaching the bound earns zero.

For the new linear scoring version, 3D view reaches zero points at 90° of rotation error; Angles reaches zero at 180°. A 45° miss earns 50 points in 3D view, while a 90° miss earns 50 in Angles.

Size it up scores width and height independently, each worth up to 50 points, then rounds the combined score. A 5% miss in both dimensions earns 90/100. A 50% error in one dimension earns zero for that dimension. Earlier daily rounds and saved questions retain their original scoring.

diff --git a/src/design-options/generated-games.js b/src/design-options/generated-games.js index 669923f..1369e8f 100644 --- a/src/design-options/generated-games.js +++ b/src/design-options/generated-games.js @@ -116,7 +116,7 @@ function makeBlockShape(random){ function scheduledTriviaQuestion(id,challengeDay=null){ const f=TRIVIA_BY_ID[id];if(!f)throw new Error('Missing scheduled trivia'); const q={id:f.id,type:f.type,skill:f.type==='timeline'?'When?':'How long?',title:f.title,answer:f.answer,caption:f.caption,explain:f.explain,source:[f.source.name,f.source.url],tip:'Answer: '+(f.type==='timeline'?f.answer:durationText(f.answer))}; - if(f.type==='timeline'&&(challengeDay===null||dayNumber(challengeDay)>=dayNumber(TIMELINE_SCORING_START))){q.timelineScoringVersion=2;q.scoringYear=Number((challengeDay||calendarDay()).split('-')[0]);} + if(f.type==='timeline'&&(challengeDay===null||dayNumber(challengeDay)>=dayNumber(TIMELINE_SCORING_START))){q.timelineScoringVersion=challengeDay===null||dayNumber(challengeDay)>=dayNumber(TIMELINE_GENTLE_START)?3:2;q.scoringYear=Number((challengeDay||calendarDay()).split('-')[0]);} return q; } diff --git a/src/design-options/how-to-play.html b/src/design-options/how-to-play.html index b01e7e7..8bf44f6 100644 --- a/src/design-options/how-to-play.html +++ b/src/design-options/how-to-play.html @@ -28,7 +28,7 @@

The games & their allowances

Angles 180°

Turn the line to the requested angle. Measure counterclockwise from the right-facing baseline.

We use the shortest angular distance. 45° off earns 75; 90° off earns 50; 180° off earns zero.

Color memory One wheel radius

Remember the swatch for two seconds, then choose its color on the wheel. Hue goes around the wheel; vividness increases toward the edge.

We measure straight-line distance between your pick and the target on the wheel. Half a wheel radius away earns 50 points; one radius or more earns zero. This measures wheel position, rather than differences between hex codes.

Time 50% of the target, minimum 1.25 s

Press Start, wait for the requested time, then Stop. There’s no clock until the reveal.

The zero-point miss is half the target duration or 1.25 seconds, whichever is larger. For a 10-second target, 2.5 seconds early or late earns 50; 5 seconds off earns zero. For a 2-second target, the allowance is 1.25 seconds.

-
When? 25% of its age, minimum 5 years

Scroll the year wheel to put your answer in the center. Lock it in to see the answer and source.

The zero-point miss is one-quarter of the event’s age or five years, whichever is larger. For a recent event, four years off still earns 20 points. For an event 100 years old, the allowance is 25 years. Age is fixed to the challenge year, so an old challenge’s scoring stays the same.

+
When? ⅓ of its age, minimum 10 years

Scroll the year wheel to put your answer in the center. Lock it in to see the answer and source.

The zero-point miss is one-third of the event’s age or ten years, whichever is larger. For events up to 30 years old, four years off earns 60 points and seven years off earns 30. For an event 60 years old, the allowance is 20 years; seven years off earns 65 points. Age is fixed to the challenge year, so an old challenge’s scoring stays the same.

This allowance applies to new practice rounds and Daily Fives from October 24, 2026. October 16–23 challenges and previously saved version-two rounds keep the five-year / 25%-of-age allowance. Earlier rounds retain their original rules.

How long? 50% of the answer

Turn the duration dial to your best estimate. Use the unit controls and +/− buttons to refine it, then lock in to see the answer and source.

A guess 50% shorter or longer earns zero. For a two-hour answer, 90 minutes or 2½ hours earns 50; one hour or three hours earns zero. We score the duration shown by the controls.

Touch, mouse & keyboard

Drag, tap, or click the game controls. Focus a control to use the keyboard; arrow keys adjust most answers. Size it up uses left/right for width and up/down for height, with Shift for larger steps. For Line memory, arrows move the cursor and Space adds a point. Color memory also has hue and saturation sliders.

Memory games are silent. Switching away resets an active memory preview or timer.

diff --git a/src/design-options/player-storage.js b/src/design-options/player-storage.js index 5a513e4..32d290f 100644 --- a/src/design-options/player-storage.js +++ b/src/design-options/player-storage.js @@ -45,7 +45,7 @@ function validSavedGame(game){ if(q.scoringVersion!==undefined&&q.scoringVersion!==2)return false; if(q.traceScoringVersion!==undefined&&(q.traceScoringVersion!==3||q.type!=='motion'||q.scoringVersion!==2))return false; if(q.scoringVersion===2&&q.type==='time'&&q.timeScoringVersion!==2)return false; - if(q.scoringVersion===2&&q.type==='timeline'&&q.timelineScoringVersion!==2)return false; + if(q.scoringVersion===2&&q.type==='timeline'&&![2,3].includes(q.timelineScoringVersion))return false; if(q.source&&(!Array.isArray(q.source)||!/^https:\/\//.test(q.source[1])))return false; const pairs=points=>Array.isArray(points)&&points.length>=3&&points.every(p=>Array.isArray(p)&&p.length===2&&p.every(Number.isFinite)); if(q.type==='view3d'&&(!q.shape||!Array.isArray(q.shape.boxes)||q.shape.boxes.length<3||q.shape.boxes.length>5||!q.shape.boxes.every(b=>Array.isArray(b)&&b.length===8&&b.slice(0,6).every(Number.isFinite)&&b.slice(3,6).every(n=>n>0)&&/^#[0-9a-f]{6}$/i.test(b[6])&&Array.isArray(b[7])&&b[7].length===6&&b[7].every(v=>typeof v==='boolean'))))return false; @@ -56,7 +56,7 @@ function validSavedGame(game){ if(q.type==='colour'&&(!Array.isArray(q.answer)||q.answer.length!==3))return false; if(['angle','time','rhythm','balance','timeline','duration'].includes(q.type)&&!q.fruit&&typeof q.answer!=='number')return false; if(q.type==='time'&&q.timeScoringVersion!==undefined&&(q.timeScoringVersion!==2||q.answer<=0))return false; - if(q.type==='timeline'&&q.timelineScoringVersion!==undefined&&(q.timelineScoringVersion!==2||!Number.isInteger(q.scoringYear)||q.scoringYear<1850||q.scoringYear>9999))return false; + if(q.type==='timeline'&&q.timelineScoringVersion!==undefined&&(![2,3].includes(q.timelineScoringVersion)||!Number.isInteger(q.scoringYear)||q.scoringYear<1850||q.scoringYear>9999))return false; if(q.type==='proportion'){ if(isSizeQuestion(q)){ if(!Number.isInteger(q.wide)||q.wide<1||q.wide>5||!Number.isInteger(q.tall)||q.tall<1||q.tall>4||!Number.isInteger(q.reference)||q.reference<32||q.reference>54||q.answer?.width!==q.wide*q.reference||q.answer?.height!==q.tall*q.reference||q.answer.width>260||q.answer.height>175||!q.start||q.start.width<32||q.start.width>304||q.start.height<32||q.start.height>207)return false; diff --git a/src/design-options/scoring.js b/src/design-options/scoring.js index d211d9c..416bcb1 100644 --- a/src/design-options/scoring.js +++ b/src/design-options/scoring.js @@ -54,11 +54,12 @@ function traceCoverageResult(points,target){ function perspectiveDiagonal(q){return q.scoringVersion===2?PERSPECTIVE_INNER_DIAGONAL:Math.hypot(400,300);} const TIMELINE_SCORING_START=LINEAR_SCORING_START; +const TIMELINE_GENTLE_START='2026-10-24'; const TIME_SCORING_START=LINEAR_SCORING_START; function usesRelativeTime(q){return q.type==='time'&&q.timeScoringVersion===2;} function timeErrorBound(q){return usesRelativeTime(q)?Math.max(1.25,q.answer*.5):SCORING_RANGES.time;} -function usesAgeBasedTimeline(q){return q.type==='timeline'&&q.timelineScoringVersion===2;} -function timelineErrorBound(q){return usesAgeBasedTimeline(q)?Math.max(5,Math.max(0,q.scoringYear-q.answer)*.25):SCORING_RANGES.timeline;} +function usesAgeBasedTimeline(q){return q.type==='timeline'&&[2,3].includes(q.timelineScoringVersion);} +function timelineErrorBound(q){const age=Math.max(0,q.scoringYear-q.answer);return q.timelineScoringVersion===3?Math.max(10,age/3):usesAgeBasedTimeline(q)?Math.max(5,age*.25):SCORING_RANGES.timeline;} function linearScoreFromError(fraction){return Number.isFinite(fraction)?Math.round(100*Math.max(0,Math.min(1,1-fraction))):0;} function wheelDistance(a,b){ const position=rgb=>{const {h,s}=rgbHsv(rgb),angle=h*Math.PI/180; diff --git a/tests/browser-timeline-scoring.cjs b/tests/browser-timeline-scoring.cjs index 745db3a..c0fb36a 100644 --- a/tests/browser-timeline-scoring.cjs +++ b/tests/browser-timeline-scoring.cjs @@ -1,5 +1,16 @@ // Requires a local build server on port 4175 and Playwright/Chromium. const {chromium}=require('playwright'),assert=require('assert'),fs=require('fs'); -(async()=>{const browser=await chromium.launch({executablePath:process.env.CHROMIUM_PATH||'/usr/bin/chromium',args:['--no-sandbox']});try{const page=await browser.newPage({viewport:{width:375,height:667},isMobile:true,hasTouch:true,reducedMotion:'reduce'}),errors=[];page.on('pageerror',e=>errors.push(e.message));await page.route('**/*',r=>new URL(r.request().url()).hostname==='127.0.0.1'?r.continue():r.abort());await page.goto('http://127.0.0.1:4175/?game=timeline'); -for(const legacy of [false,true]){await page.evaluate(legacy=>{const id=Object.values(TRIVIA_BY_ID).find(f=>f.type==='timeline').id,q={...scheduledTriviaQuestion(id,legacy?'2026-10-15':'2026-10-16'),title:'A recent event',answer:2025};deck=Array(5).fill(q);index=0;results=[];render();},legacy);await page.locator('#year-wheel').press('End');for(let i=0;i<5;i++)await page.locator('#year-wheel').press('ArrowUp');await page.locator('#trivia-lock').click();assert.equal(await page.evaluate(()=>results[0].guess),2021);assert.equal(await page.evaluate(()=>results[0].score),legacy?85:20);const committed=await page.evaluate(()=>JSON.stringify(results));for(const theme of ['focus','night-lab']){await page.selectOption('#theme-select',theme);await page.waitForTimeout(80);assert.equal(await page.evaluate(()=>JSON.stringify(results)),committed);assert(await page.evaluate(()=>document.body.scrollHeight<=innerHeight+1));const text=await page.locator('#actionarea').innerText();assert.equal(text.includes('0 points at 5 years off'),!legacy);if(!legacy){const out=process.env.PERCEPTION_SCREENSHOTS||'/workspace/artifacts/timeline-scoring';fs.mkdirSync(out,{recursive:true});await page.screenshot({path:out+'/'+theme+'.png'});}}} -assert.deepEqual(errors,[]);console.log('Verified actual keyboard selection: four-year miss earns 20 under the new baseline and 85 under legacy scoring; both themes preserve the result and fit a small phone.');}finally{await browser.close();}})().catch(e=>{console.error(e);process.exit(1)}); +(async()=>{const browser=await chromium.launch({executablePath:process.env.CHROMIUM_PATH||'/usr/bin/chromium',args:['--no-sandbox']});try{ + const page=await browser.newPage({viewport:{width:375,height:667},isMobile:true,hasTouch:true,reducedMotion:'reduce'}),errors=[];page.on('pageerror',e=>errors.push(e.message));await page.route('**/*',r=>new URL(r.request().url()).hostname==='127.0.0.1'?r.continue():r.abort());await page.goto('http://127.0.0.1:4175/?game=timeline'); + for(const [day,expected,bound]of [['2026-10-15',85,null],['2026-10-16',20,5],['2026-10-24',60,10]]){ + await page.evaluate(day=>{const id=Object.values(TRIVIA_BY_ID).find(f=>f.type==='timeline').id,q={...scheduledTriviaQuestion(id,day),title:'A recent event',answer:2025};deck=Array(5).fill(q);index=0;results=[];render();},day); + await page.locator('#year-wheel').press('End');for(let i=0;i<5;i++)await page.locator('#year-wheel').press('ArrowUp');await page.locator('#trivia-lock').click();assert.equal(await page.evaluate(()=>results[0].guess),2021);assert.equal(await page.evaluate(()=>results[0].score),expected); + const committed=await page.evaluate(()=>JSON.stringify(results));for(const theme of ['focus','night-lab']){await page.selectOption('#theme-select',theme);await page.waitForTimeout(80);assert.equal(await page.evaluate(()=>JSON.stringify(results)),committed);assert(await page.evaluate(()=>document.body.scrollHeight<=innerHeight+1));const text=await page.locator('#actionarea').innerText();if(bound)assert(text.includes('0 points at '+bound+' years off'));else assert(!text.includes('0 points at'));} + } + for(const theme of ['focus','night-lab']){ + await page.selectOption('#theme-select',theme);await page.evaluate(()=>{const q=scheduledTriviaQuestion('timeline-pokemon-first-released','2026-10-24');deck=Array(5).fill(q);index=0;results=[];render();}); + await page.locator('#year-wheel').press('End');for(let i=0;i<37;i++)await page.locator('#year-wheel').press('ArrowUp');await page.locator('#trivia-lock').click();assert.equal(await page.evaluate(()=>results[0].guess),1989);assert.equal(await page.evaluate(()=>results[0].score),30);assert((await page.locator('#actionarea').innerText()).includes('0 points at 10 years off'));assert(await page.locator('#next').evaluate(b=>b.getBoundingClientRect().bottom<=innerHeight)); + const out=process.env.PERCEPTION_SCREENSHOTS||'/workspace/artifacts/gentler-timeline';fs.mkdirSync(out,{recursive:true});await page.screenshot({path:out+'/'+theme+'.png'}); + } + assert.deepEqual(errors,[]);console.log('Verified actual keyboard guesses under all three scoring versions, Pokemon seven-year miss = 30, visible bound and Next button, and theme/state persistence on a small phone.'); +}finally{await browser.close();}})().catch(e=>{console.error(e);process.exit(1)}); diff --git a/tests/verify-timeline-scoring.cjs b/tests/verify-timeline-scoring.cjs index afed525..c2352c6 100644 --- a/tests/verify-timeline-scoring.cjs +++ b/tests/verify-timeline-scoring.cjs @@ -16,9 +16,22 @@ const oldFact=run("scheduledTriviaQuestion(factId,'2026-10-15')");assert.equal(o const future=run("scheduledTriviaQuestion(factId,'2026-10-16')");assert.equal(future.timelineScoringVersion,2);assert.equal(future.scoringYear,2026); assert.equal(run("scheduledTriviaQuestion(factId,'2027-1-1').scoringYear"),2027); for(const day of ['2026-10-14','2026-10-15']){ctx.testDay=day;assert(run("makeDeck(testDay,'timeline').every(q=>q.timelineScoringVersion===undefined)"));assert(run("makeDeck(testDay,'mix').filter(q=>q.type==='timeline').every(q=>q.timelineScoringVersion===undefined)"));} -run("calendarDay=()=> '2026-10-15'");assert(run("makeDeck('practice:new-timeline','timeline').every(q=>q.timelineScoringVersion===2&&q.scoringYear===2026)")); -for(const [day,entry]of Object.entries(JSON.parse(fs.readFileSync('data/daily-trivia.json')).days))if(day>='2026-10-16'&&entry.types.includes('timeline')){ctx.testDay=day;const q=run("makeDeck(testDay,'mix').find(q=>q.type==='timeline')");assert.equal(q.timelineScoringVersion,2);assert.equal(q.scoringYear,Number(day.slice(0,4)));assert.deepEqual(plain(run("makeDeck(testDay,'mix')")),plain(run("makeDeck(testDay,'mix')")));} +run("calendarDay=()=> '2026-10-15'");assert(run("makeDeck('practice:new-timeline','timeline').every(q=>q.timelineScoringVersion===3&&q.scoringYear===2026)")); +for(const [day,entry]of Object.entries(JSON.parse(fs.readFileSync('data/daily-trivia.json')).days))if(day>='2026-10-16'&&entry.types.includes('timeline')){ctx.testDay=day;const q=run("makeDeck(testDay,'mix').find(q=>q.type==='timeline')");assert.equal(q.timelineScoringVersion,day>='2026-10-24'?3:2);assert.equal(q.scoringYear,Number(day.slice(0,4)));assert.deepEqual(plain(run("makeDeck(testDay,'mix')")),plain(run("makeDeck(testDay,'mix')")));} ctx.future=future;assert(run('validSavedGame({deck:Array(5).fill(future),results:[]})')); -assert(!run('validSavedGame({deck:Array(5).fill({...future,timelineScoringVersion:3}),results:[]})')); +assert(!run('validSavedGame({deck:Array(5).fill({...future,timelineScoringVersion:4}),results:[]})')); assert(!run('validSavedGame({deck:Array(5).fill({...future,scoringYear:NaN}),results:[]})')); console.log('Verified five-year linear baseline (four years off = 20), age-based bounds, symmetry, clamps, frozen challenge years, future daily/practice generation, saved version validation, and legacy scoring.'); + +const gentle={type:'timeline',answer:2025,timelineScoringVersion:3,scoringYear:2026};ctx.gentle=gentle; +assert.equal(run('timelineErrorBound(gentle)'),10); +for(const [error,score]of [[0,100],[1,90],[4,60],[5,50],[7,30],[9,10],[10,0],[20,0]])for(const sign of [-1,1]){ctx.value=gentle.answer+error*sign;assert.equal(run('scoreFor(value,gentle)'),score);} +for(const [age,bound,sevenScore]of [[0,10,30],[10,10,30],[30,10,30],[60,20,65],[100,100/3,79]]){ctx.age=age;assert.equal(run('timelineErrorBound({...gentle,answer:2026-age})'),bound);assert.equal(run('scoreFor(2026-age-7,{...gentle,answer:2026-age})'),sevenScore);} +assert.equal(run('timelineErrorBound({...gentle,answer:2030})'),10); +assert.equal(run('scoreFor(NaN,gentle)'),0);assert.equal(run('scoreFor(Infinity,gentle)'),0); +run("calendarDay=()=> '2099-01-01'");assert.equal(run('scoreFor(2018,gentle)'),30,'frozen year must survive replay decades later'); +const cutover=run("scheduledTriviaQuestion(factId,'2026-10-24')");ctx.cutover=cutover;assert.equal(cutover.timelineScoringVersion,3);assert.equal(cutover.scoringYear,2026);assert(run('validSavedGame({deck:Array(5).fill(cutover),results:[]})')); +assert.equal(run("scheduledTriviaQuestion(factId,'2026-10-23').timelineScoringVersion"),2); +assert(run("makeDeck('2026-10-24','timeline').every(q=>q.timelineScoringVersion===3)")); +const pokemon=run("scheduledTriviaQuestion('timeline-pokemon-first-released','2026-10-23')");ctx.pokemon=pokemon;assert.equal(run('scoreFor(1989,pokemon)'),7);assert.equal(run('scoreFor(1989,{...pokemon,timelineScoringVersion:3})'),30); +console.log('Verified gentler ten-year / one-third-age bounds, seven-year Pokemon miss = 30, versioned cutover, frozen years, restored v2/v3 questions, clamps, and unchanged historical scores.');