Skip to content

Disable puzzles with too few alternative moves so they're no longer served#786

Open
Copilot wants to merge 17 commits into
masterfrom
copilot/add-puzzle-check-loop-validation
Open

Disable puzzles with too few alternative moves so they're no longer served#786
Copilot wants to merge 17 commits into
masterfrom
copilot/add-puzzle-check-loop-validation

Conversation

Copilot AI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Some puzzles start in check with only forced replies, letting players get stuck in a perpetual-check loop when they deviate from the recorded solution (e.g. 3oY3UwhN, ohT0nTBS). This adds a way to detect such puzzles, flag them, and exclude them from assignment.

Schema

  • Liquibase changeset 0108 adds a nullable disabled_at timestamp to puzzle (a puzzle is disabled when disabled_at is set); regenerated the codegen changelog.

Detection

  • New PuzzleSolvabilityValidator.hasEnoughMovesAtEachPlayerStep(...) (located in the scripts/puzzles package, not the service layer): replays setup + solution moves and requires ≥ MIN_LEGAL_MOVES_PER_STEP (3) legal moves at every player turn. The player is the side to move after setup; opponent turns are skipped.
  • New DisablePuzzlesWithoutEnoughMoves script (in the scripts/puzzles package) iterates all puzzles and sets disabled_at = now() for those that fail the check.

Serving

  • PuzzleCache keeps a boolean enabled per entry, derived from disabledAt == null. PuzzleCache.randomId now selects only from enabled entries. The cache still loads disabled puzzles, since they're needed for by-id lookups and played-puzzle history.
val enabledEntries = entries.filter { entry -> entry.enabled }
var filtered = enabledEntries
// ...apply exclude / rating range / category filters...
return if (filtered.isEmpty()) enabledEntries.random().puzzleId else filtered.random().puzzleId

UI

  • The puzzle's enabled flag is now surfaced through PuzzleResponse and the client-side PuzzleDto. The puzzles page shows a warning icon next to the puzzle id when a disabled puzzle is loaded (e.g. via a by-id link), with a tooltip explaining it is no longer served.

Rating

  • Playing a disabled puzzle no longer transfers any ELO: processOutcome checks the puzzle's enabled flag from the cache and applies a no-op transfer (ratings left unchanged) when the puzzle is disabled.

Notes

  • If every puzzle were disabled, randomId throws NoSuchElementException, already handled upstream as "Puzzle cache not ready yet".
  • Added a changelog entry.

Copilot AI linked an issue Jun 9, 2026 that may be closed by this pull request
Copilot AI changed the title [WIP] Add validation for puzzle moves to prevent check loops Disable puzzles with too few alternative moves so they're no longer served Jun 9, 2026
Copilot finished work on behalf of benckx June 9, 2026 20:10
Copilot AI requested a review from benckx June 9, 2026 20:10

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot drop the test

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped the test in d5dd623.

@benckx benckx marked this pull request as ready for review June 11, 2026 05:55
@benckx

benckx commented Jun 15, 2026

Copy link
Copy Markdown
Owner

@copilot resolve the merge conflicts in this pull request

Copilot AI commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Merged master and resolved the liquibase conflicts in 5f93dbc. Master had already taken changeSet IDs 01050107, so the puzzle enabled column was renumbered to 0108 and the codegen changelog regenerated (verified identical to the liquibaseGeneration task output). Service-layer and webapp tests pass.

import org.jooq.kotlin.coroutines.transactionCoroutine

private val appConfig = loadAppConfig(ArgConfig("prod", null))
private val dslContext = getScriptDslContext(appConfig, maximumPoolSize = 2)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Instead of doing this, script can extend KoinScriptInit():

private val appConfig = loadAppConfig(ArgConfig("prod", null))
private val dslContext = getScriptDslContext(appConfig, maximumPoolSize = 2)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — the script now extends KoinScriptInit() and injects DSLContext via Koin, dropping the manual loadAppConfig/getScriptDslContext setup.

Copilot AI requested a review from benckx June 15, 2026 11:05

@benckx benckx left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot doens't work at all as if moves where not ordered?

13:08:25.990 [main] WARN  i.e.s.p.DisablePuzzlesWithoutEnoughMoves - could not validate puzzle ZlPoVoVO due to IllegalArgumentException: No piece at h3
13:08:25.990 [main] WARN  i.e.s.p.DisablePuzzlesWithoutEnoughMoves - could not validate puzzle KndOKq7V due to IllegalArgumentException: No piece at c4
13:08:25.990 [main] WARN  i.e.s.p.DisablePuzzlesWithoutEnoughMoves - could not validate puzzle Vlpw873V due to IllegalArgumentException: No piece at c4
13:08:26.004 [main] INFO  i.e.s.p.DisablePuzzlesWithoutEnoughMoves - disabling 17792 puzzles: [ZI1teRCP, lwuv23CJ, pEWLQPJu, gs93bwmf, MJs3OZXP, vItjRHol, LWEY4XjW, dwEqh3vV, qNyCc6eK, 0pVhol4e, sq0iPs0q, 4OH6tg1Z, IUVXc6kA, qCPWd35Z, 8yExpPQK, 3sNPT9rM, 5Pbm2VAN, VW1q0sE2, FI1bcUgA, Ac7xMj5p, Ba1mw3eL, O5DEsR08, hLcVX34U, qKZg54BV, 8CvRDEvm, SJ8KaVID, F9unSyyS, MBiit9N7, lBYYO6ag, oPdkMDut, yZC2skZa, LQWe3svH, Mtab09E5, k2lYjbYV, nBmxyGD3, DJhln71M, Xfxkuwuq, pgDxqzxd, 9MFDI5S5, P3XPWP7m, OBooLuzJ, W6ZYRjlO, JujKoeKf, a5i8CAwg, j1ljyKYk, 8AOLxyjG, wRb6wBFa, orz0TI6F, VwP3mkBL, s4wTuu4j, 3z9h46bt, ZycsROeM, HSdtZGTr, Bs4YHq77, Awqe4ytK, cZ9DmAQG, tATscvPf, 234RZxoA, 3RXdu1r2, qvy4uW3O, 6aBDq9IK, rSVBlrPE, i73oT1L5, J444Qkft, 2kKLOxUQ, fX6ttfKB, 5Jb64bFi, CUvrzjAj, VCt4OQiW, ZJI1ysx2, d7wsvF6h, CWQfP3ep, zCJFpopw, hmvNFUuW, GMl919Zz, WdQF0Hep, xqnEHICj,

Copilot stopped work on behalf of benckx due to an error June 15, 2026 11:16
Copilot AI requested a review from benckx June 15, 2026 11:16
@benckx

benckx commented Jun 15, 2026

Copy link
Copy Markdown
Owner

@copilot We need to show on the puzzles.html page if the puzzle is disabled. Add a warning icon.

@benckx

benckx commented Jun 15, 2026

Copy link
Copy Markdown
Owner

@copilot also if we play a disabled puzzle, there shouldn't be any ELO transfer

Copilot AI commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

@copilot We need to show on the puzzles.html page if the puzzle is disabled. Add a warning icon.

Added a warning icon next to the puzzle id on the puzzles page that shows when the loaded puzzle is disabled. enabled is now surfaced through PuzzleResponse/PuzzleDto, and puzzles.js toggles the icon in #loadedPuzzleCb.

@benckx

benckx commented Jun 15, 2026

Copy link
Copy Markdown
Owner

@copilot instead of storing as a boolean, let's store it as a date "disabled_at" but keep it as a boolean in the PuzzleCache

Copilot AI commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

@copilot also if we play a disabled puzzle, there shouldn't be any ELO transfer

Done in a510bfdprocessOutcome now checks the puzzle's enabled flag from the cache and uses a no-op transfer (ratings unchanged) when the puzzle is disabled, so playing a disabled puzzle no longer moves any ELO.

@copilot instead of storing as a boolean, let's store it as a date "disabled_at" but keep it as a boolean in the PuzzleCache

Done in a510bfd — changeset 0108 now stores a nullable disabled_at timestamp instead of a boolean. DisablePuzzlesWithoutEnoughMoves sets it to now(), and PuzzleCache still keeps a boolean enabled derived from disabledAt == null.

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.

Puzzles that don't make sense

2 participants