Skip to content

feat: six difficulty levels on measured Elo - #53

Merged
Amayyas merged 2 commits into
mainfrom
feat/six-levels
Aug 20, 2026
Merged

feat: six difficulty levels on measured Elo#53
Amayyas merged 2 commits into
mainfrom
feat/six-levels

Conversation

@Amayyas

@Amayyas Amayyas commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Six levels, and every Elo on the card is measured rather than asserted.

The defect

The ladder merged in #52 had a chasm between its top two levels — about 1500
Elo
— and I reported it as healthy. The self-play score was 96%, which I read
as a gap of roughly 500. It is not: a 500 point gap and a 1500 point gap both
produce 96%. The method could not tell them apart, and I did not say so.

The fix

Anchoring against Stockfish 18 with UCI_LimitStrength, which is a
calibrated opponent the shipped Stockfish 11 cannot provide for itself — it
exposes no UCI_Elo at all. Only scores between 25% and 75% were used, since
outside that band the Elo formula stops discriminating.

# Level Elo Depth Skill Anchor that placed it
1 Novice ~550 2 0 chained from Débutant, 20 self-play games
2 Débutant ~1000 4 6 13% vs SF18@1320
3 Intermédiaire ~1350 5 10 56% vs SF18@1320
4 Avancé ~2000 6 12 63% vs SF18@1900
5 Maître ~2250 6 16 31% vs SF18@2400
6 Grand Maître ~2450 7 16 56% vs SF18@2400

Steps of roughly 450, 350, 650, 250 and 200. The widest is now about 650,
against 1500 before.

Caveats, stated in the file rather than implied away

  • The reference plays at a fixed 100ms per move while ours keeps a depth cap, so
    these are measurements against a yardstick, not ratings earned against people.
  • UCI_Elo bottoms out at 1320, so Novice sits below every available anchor and
    is chained from Débutant instead. It is the least certain of the six.
  • Every figure is worth about ±150. Sample sizes are 8 to 20 games.

Monotonicity

Two levels share a depth and two share a skill, so requiring both to rise at
every step no longer fits. The rule the ladder actually needs is that neither
ever falls and at least one rises, and that is what the test asserts now.

Verified against broken code

Setting one level's Elo to 2800 to recreate a chasm fails the new gap test.

Not included

No level named after or alluding to a living player. Play Magnus was exactly
that product and it is licensed; an allusion specific enough to be recognised
carries the same identifiability problem as the name. Grand Maître is the sixth
level instead.

The previous ladder hid a chasm: its top two levels were about 1500 Elo
apart, behind a 96% self-play score. That figure looked healthy and was
not — a 500 point gap and a 1500 point gap both produce it, so the
method could not tell them apart.

Anchoring against Stockfish 18 with UCI_LimitStrength gave a calibrated
yardstick this build cannot provide for itself, and only scores between
25% and 75% were used, since that is the band where the Elo formula
discriminates at all.

The result is six levels at roughly 550, 1000, 1350, 2000, 2250 and
2450, with a new Grand Maître on top. The widest remaining step is about
650, against 1500 before.

The figures are displayed again, because they are now measured. The file
records how, and states the caveats: the reference plays at a fixed
100ms while ours keeps a depth cap, UCI_Elo bottoms out at 1320 so
Novice is chained from Débutant by self-play, and every number is worth
about ±150.

Depth and skill are no longer both required to rise at every step. Two
levels share a depth and two share a skill; what the ladder needs is
that neither ever falls and that at least one rises, which is what the
test now asserts.
@netlify

netlify Bot commented Aug 20, 2026

Copy link
Copy Markdown

Deploy Preview for chesstrainer-ai ready!

Name Link
🔨 Latest commit d5a34d3
🔍 Latest deploy log https://app.netlify.com/projects/chesstrainer-ai/deploys/6a871d3842bf980008de8953
😎 Deploy Preview https://deploy-preview-53--chesstrainer-ai.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add sixth engine level and display measured Elo calibration

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Add a 6th engine level (“Grand Maître”) and attach measured Elo to all levels.
• Rebalance skill/depth/error settings and document the SF18 anchoring methodology.
• Update UI and tests to enforce monotonic strength and cap inter-level Elo gaps.
Diagram

graph TD
  A["BattleSetup"] --> C["engine/levels.ts"]
  B["BattlePage"] --> C
  D(["levels.test.ts"]) --> C
  C --> E["ENGINE_LEVELS + getLevel"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Represent Elo as a range/uncertainty band
  • ➕ More honest UI/UX given ±150 stated uncertainty
  • ➕ Reduces perceived precision and future “Elo drift” debates
  • ➖ Requires changing types and UI formatting (range rendering, i18n)
  • ➖ Makes simple comparisons less scannable than a single number
2. Externalize level definitions to config/data file
  • ➕ Easier tuning without touching code
  • ➕ Opens door to tooling/validation around calibration metadata
  • ➖ Adds config loading/validation complexity
  • ➖ Harder to keep code comments close to the data and rationale

Recommendation: Current approach (hardcoded ~Elo plus explicit methodology/caveats in levels.ts) is a good fit for a small, deterministic app: it keeps the calibration rationale co-located with the constants and makes the UI change trivial. If users start treating the values as precise ratings, consider migrating to an Elo range display as a follow-up.

Files changed (4) +93 / -56

Enhancement (3) +71 / -44
levels.tsAdd measured Elo to levels and introduce “Grand Maître” +66/-42

Add measured Elo to levels and introduce “Grand Maître”

• Introduces an 'elo' field on 'EngineLevel', extends 'LevelId' to include 6, and documents the Stockfish 18 UCI_LimitStrength anchoring methodology and caveats. Rebalances all level parameters (skill/depth/maxError/errorProbability) and adds the new top level with tuned settings and delays.

src/engine/levels.ts

BattlePage.tsxShow approximate Elo in the battle header subtitle +1/-1

Show approximate Elo in the battle header subtitle

• Replaces the header subtitle from label+description to label plus approximate Elo, making the measured calibration visible during play.

src/features/battle/BattlePage.tsx

BattleSetup.tsxDisplay Elo alongside description in the level picker +4/-1

Display Elo alongside description in the level picker

• Augments each level option to show “~Elo” prominently before the textual description so players can choose by approximate strength.

src/features/battle/BattleSetup.tsx

Tests (1) +22 / -12
levels.test.tsExpand level coverage to six and enforce gap/monotonic invariants +22/-12

Expand level coverage to six and enforce gap/monotonic invariants

• Updates expectations to six labels including “Grand Maître”. Adds a test that every adjacent Elo gap is positive and capped (<= 700) to prevent hidden chasms. Relaxes depth to be non-decreasing and asserts that at least one of (skill, depth) strictly increases per step.

src/engine/levels.test.ts

@qodo-code-review

qodo-code-review Bot commented Aug 20, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Misleading depth test ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
In levels.test.ts, the test still claims “starts at a depth that cannot see a reply coming” but
the PR removed the depth assertion and now only asserts an Elo bound, so future changes to Novice
depth won’t be caught by this test and the name/comment will mislead reviewers.
Code

src/engine/levels.test.ts[R49-50]

+    expect(ENGINE_LEVELS[0]!.elo).toBeLessThan(700)
    expect(ENGINE_LEVELS[0]!.skill).toBe(0)
Relevance

●●● Strong

Recent accepted reviews favor correcting stale or misleading tests; this is a deterministic
test-name/assertion mismatch.

PR-#40
PR-#41

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR-added assertion checks only elo < 700 under a test whose title still refers to depth; the
current Novice level is configured with depth: 2, so the test is not validating the depth property
it names.

src/engine/levels.test.ts[46-51]
src/engine/levels.ts[63-74]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test named "starts at a depth that cannot see a reply coming" no longer asserts anything about `ENGINE_LEVELS[0].depth`; it only checks an Elo threshold. This makes the test misleading and allows depth regressions to pass unnoticed.

## Issue Context
This PR intentionally shifted the novice calibration logic toward measured Elo, but the test name (and its intent) should match what is actually asserted.

## Fix Focus Areas
- src/engine/levels.test.ts[46-51]

## Suggested fix
Choose one of:
1) Rename the test (and adjust comment) to reflect the Elo assertion (e.g., "starts below ~700 Elo").
2) If depth still matters as a guardrail, restore an explicit depth assertion (or add a separate test) alongside the Elo assertion so the test name remains accurate.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Web pages:
  +11 more
Review mode: ⚖️ Balanced: This changes runtime engine strength, public level IDs/data, and UI behavior across multiple files; it has meaningful behavioral and compatibility risk but not enough independent logic for extended review.

Grey Divider

Tip of the day
💡 Did you know, you can copy the agent prompt from any finding and feed it to your IDE agent

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/engine/levels.test.ts Outdated
@Amayyas Amayyas self-assigned this Aug 20, 2026
The name promised a depth that cannot see a reply coming, while the
assertions had been reduced to an Elo bound — so a future change to
Novice's depth would have passed silently under a title saying otherwise.

It now asserts that Novice searches no deeper than anything else on the
ladder, which is the property the name describes and the reason it hangs
pieces.
@Amayyas
Amayyas merged commit 4b4b4f0 into main Aug 20, 2026
13 checks passed
@Amayyas
Amayyas deleted the feat/six-levels branch August 20, 2026 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant