Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto report puzzles with multiple solutions #16293

Merged
merged 26 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c225624
WIP report puzzles with multiple solutions
kraktus Oct 30, 2024
cc972da
WIP: add backend part
kraktus Oct 30, 2024
eba323d
Puzzle report: fix bypassing checkmate puzzles
kraktus Oct 30, 2024
7dc9802
Anon cannot report faulty puzzles
kraktus Oct 30, 2024
0de65eb
Puzzle report: restore the fact the client only send the report once
kraktus Oct 30, 2024
a16b70e
tweak wording
kraktus Oct 30, 2024
09b3137
Puzzle report: fix dialog
kraktus Oct 30, 2024
a317a46
Crash everything
kraktus Oct 30, 2024
b5fe661
fmt
kraktus Oct 31, 2024
5a475b3
remove some outdated comments
kraktus Oct 31, 2024
4086c66
Merge branch 'master' into auto_report_faulty_puzzle
ornicar Oct 31, 2024
538806e
format
kraktus Nov 1, 2024
a6ff0a9
fix build
kraktus Nov 1, 2024
2686f09
WIP finish the dialog
kraktus Nov 1, 2024
dc28cd1
Puzzle report: Ugly but working "disable dialog" button
kraktus Nov 2, 2024
8d69b71
Merge branch 'master' into auto_report_faulty_puzzle
kraktus Nov 2, 2024
507375b
Puzzle report: make multiple solution check stricly identical to the …
kraktus Nov 2, 2024
80ead25
Merge branch 'master' into auto_report_faulty_puzzle
ornicar Nov 4, 2024
141c815
use a simple in-head dedup cache for puzzle reports
ornicar Nov 4, 2024
714a945
tweak puzzle report dialog dom and wording
ornicar Nov 4, 2024
9d9193a
Report puzzle: move to own file
kraktus Nov 4, 2024
cef70b7
Report puzzle: instanciate `Report`
kraktus Nov 4, 2024
cd360b8
Report puzzle: stylisze a bit more the dialog
kraktus Nov 4, 2024
7b29ff7
remove console.log
kraktus Nov 4, 2024
af21a74
Merge branch 'master' into auto_report_faulty_puzzle
ornicar Nov 5, 2024
147d1e3
remove puzzle report circular dependency
ornicar Nov 5, 2024
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
9 changes: 9 additions & 0 deletions app/controllers/Puzzle.scala
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,15 @@ final class Puzzle(env: Env, apiC: => Api) extends LilaController(env):
)
}

def report(id: PuzzleId) = AuthBody { _ ?=> me ?=>
NoBot:
bindForm(env.puzzle.forms.report)(
doubleJsonFormError,
reportText =>
env.puzzle.api.report.upsert(id).flatMap(_.so(env.irc.api.reportPuzzle(me.light, id,reportText))).inject(jsonOkResult)
)
}

def voteTheme(id: PuzzleId, themeStr: String) = AuthBody { _ ?=> me ?=>
NoBot:
PuzzleTheme
Expand Down
1 change: 1 addition & 0 deletions conf/base.conf
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ puzzle {
puzzle = puzzle2_puzzle
round = puzzle2_round
path = puzzle2_path
report = puzzle2_report
kraktus marked this conversation as resolved.
Show resolved Hide resolved
}
}
relay {
Expand Down
1 change: 1 addition & 0 deletions conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ GET /training/:angle/$color<white|black|random> controllers.Puzzle.angleAndCol
GET /training/:angle/$id<\w{5}> controllers.Puzzle.showWithAngle(angle, id: PuzzleId)
POST /training/$numericalId<\d{6,}>/round2 controllers.Puzzle.mobileBcRound(numericalId: Long)
POST /training/$id<\w{5}>/vote controllers.Puzzle.vote(id: PuzzleId)
POST /training/$id<\w{5}>/report controllers.Puzzle.report(id: PuzzleId)
POST /training/$id<\w{5}>/vote/:theme controllers.Puzzle.voteTheme(id: PuzzleId, theme)
POST /training/complete/:theme/$id<\w{5}> controllers.Puzzle.complete(theme, id: PuzzleId)
POST /training/difficulty/:theme controllers.Puzzle.setDifficulty(theme)
Expand Down
4 changes: 4 additions & 0 deletions modules/irc/src/main/IrcApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ final class IrcApi(
zulip(_.content, "/opening edits"):
s"${markdown.userLink(user)} edited ${markdown.lichessLink(s"/opening/$opening/$moves", opening)}"

def reportPuzzle(user: LightUser, puzzleId: PuzzleId, reportText: String): Funit =
zulip(_.content, "puzzle reports"):
s"${markdown.userLink(user)} reported ${markdown.lichessLink(s"/training/$puzzleId", puzzleId)} because $reportText"

def broadcastStart(id: RelayRoundId, fullName: String): Funit =
zulip(_.broadcast, "non-tiered broadcasts"):
s":note: ${markdown.broadcastLink(id, fullName)}"
Expand Down
9 changes: 6 additions & 3 deletions modules/puzzle/src/main/Env.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ private class PuzzleConfig(
@ConfigName("mongodb.uri") val mongoUri: String,
@ConfigName("collection.puzzle") val puzzleColl: CollName,
@ConfigName("collection.round") val roundColl: CollName,
@ConfigName("collection.path") val pathColl: CollName
@ConfigName("collection.path") val pathColl: CollName,
@ConfigName("collection.report") val reportColl: CollName
)

@Module
Expand All @@ -37,7 +38,8 @@ final class Env(
val colls = new PuzzleColls(
puzzle = db(config.puzzleColl),
round = db(config.roundColl),
path = db(config.pathColl)
path = db(config.pathColl),
report = db(config.reportColl)
)

private val gameJson: GameJson = wire[GameJson]
Expand Down Expand Up @@ -112,5 +114,6 @@ final class Env(
final class PuzzleColls(
val puzzle: AsyncColl,
val round: AsyncColl,
val path: AsyncColl
val path: AsyncColl,
val report: AsyncColl
)
19 changes: 19 additions & 0 deletions modules/puzzle/src/main/PuzzleApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ final class PuzzleApi(
def setIssue(id: PuzzleId, issue: String): Fu[Boolean] =
colls.puzzle(_.updateField($id(id), Puzzle.BSONFields.issue, issue).map(_.n > 0))

object report:
// return `true` if missing
def upsert(id: PuzzleId): Fu[Boolean] =
colls
.puzzle(_.exists($id(id)))
.flatMap(
_.so(
colls.report(
_.update
.one(
$id(id),
$doc("reported" -> true),
upsert = true
)
.map(_.upserted.nonEmpty)
)
)
)
kraktus marked this conversation as resolved.
Show resolved Hide resolved

private[puzzle] object round:

def find(user: User, puzzleId: PuzzleId): Fu[Option[PuzzleRound]] =
Expand Down
4 changes: 4 additions & 0 deletions modules/puzzle/src/main/PuzzleForm.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ object PuzzleForm:
single("vote" -> boolean)
)

val report = Form(
single("reason" -> nonEmptyText(1,200))
)

val themeVote = Form(
single("vote" -> optional(boolean))
)
Expand Down
77 changes: 77 additions & 0 deletions ui/puzzle/src/ctrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import moveTest from './moveTest';
import PuzzleSession from './session';
import PuzzleStreak from './streak';
import { throttle } from 'common/timing';
// import * as licon from 'common/licon';
import {
PuzzleOpts,
PuzzleData,
Expand Down Expand Up @@ -78,6 +79,8 @@ export default class PuzzleCtrl implements ParentCtrl {
voteDisabled?: boolean;
isDaily: boolean;
blindfolded = false;
// if local eval suspect multiple solutions, report the puzzle
reportedForMultipleSolutions = false;

constructor(
readonly opts: PuzzleOpts,
Expand Down Expand Up @@ -125,6 +128,7 @@ export default class PuzzleCtrl implements ParentCtrl {
if (!node.threat || node.threat.depth <= threat.depth) node.threat = threat;
} else if (!node.ceval || node.ceval.depth <= ev.depth) node.ceval = ev;
if (work.path === this.path) {
this.reportIfMultipleSolutions(ev);
this.setAutoShapes();
this.redraw();
}
Expand Down Expand Up @@ -463,6 +467,79 @@ export default class PuzzleCtrl implements ParentCtrl {

private isPuzzleData = (d: PuzzleData | ReplayEnd): d is PuzzleData => 'puzzle' in d;

// take the eval as arg instead of taking it from the node to be sure it's the most up to date
// All non-mates puzzle should have one and only one solution, if that is not the case, report it back to backend
private reportIfMultipleSolutions = (ev: Tree.ClientEval): void => {
// if the eval depth is superior to 20, we're on the puzzle solution and two moves are good
// that is an absolute eval superior to 4, then log it
console.log('ev', ev, 'node', this.node);
// first, make sure we're in view mode so we know the solution is the mainline
// do not check, checkmate puzzles
if (
!this.session.userId ||
this.reportedForMultipleSolutions ||
this.mode != 'view' ||
this.threatMode() ||
// the `mate` key theme is not sent, as it is considered redubant with `mateInX`
this.data.puzzle.themes.some((t: ThemeKey) => t.toLowerCase().includes('mate'))
)
return;
console.log('good mode');
// know we want to check that we're evaluating from the opponent side, so that multiPV show moves for the solving
// side. We also want to check we're at the first ply of the puzzle or later.
const node = this.node;
// more resilient than checking the turn directly, if eventually puzzle get generated from position games
const nodeTurn = node.fen.includes(' w ') ? 'white' : 'black';
if (
node.ply >= this.initialNode.ply &&
nodeTurn == this.pov &&
this.mainline.some(n => n.id == node.id)
) {
console.log('correct position!');
// if second pv.cp is > 400, it's a multi solution puzzle
const invertIfBlack = (cp: number) => (this.pov == 'white' ? cp : -cp);
console.log('ev.depth > 20', ev!.depth > 20, 'ev.depth > 20');
if (ev.pvs[1]?.cp) {
console.log(
'ev!.pvs[1]!.cp',
ev.pvs[1].cp,
'ev.pvs[1].cp >= cpThreshold',
invertIfBlack(ev.pvs[1].cp) >= 400,
);
}
// TODO probably check what are really the conditions for a puzzle to have multiple solutions
if (ev.depth > 20 && ev.pvs[1]?.cp && invertIfBlack(ev.pvs[1].cp) >= 400) {
// this.reportedForMultipleSolutions = true;
// //this.reportDialog()
// // POST /training/$id<\w{5}>/report controllers.Puzzle.report(id: PuzzleId)
const reason = `Move ${node.san}, depth ${ev.depth}, pvs ${ev.pvs.map(pv => `${pv.moves[0]}: ${pv.cp}`).join(', ')}`;
xhr.report(this.data.puzzle.id, reason)
}
}
};

// TODO FIXME, does not work, dialogs...
// private reportDialog = () => {
// // html form yes/no report with a checkbox 'do not show this again for a week'
// domDialog({
// show: 'modal',
// htmlText:
// '<div><strong style="font-size:1.5em">' +
// 'Report multiple solutions' +
// '</strong><br /><br />' +
// '<pre>' +
// 'You have found a puzzle with multiple solutions, report it?' +
// '</pre><br />' +
// '<br /><br />' +
// `<button type="reset" class="button button-empty button-red text reset" data-icon="${licon.X}">No</button>` +
// `<button type="submit" class="button button-green text apply" data-icon="${licon.Checkmark}">Yes</button>`,
// }).then(_dlg => {
// // No overload matches this call.
// // $('.reset', dlg.view).on('click', dlg.close());
// // dlg.showModal();
// });
// };
kraktus marked this conversation as resolved.
Show resolved Hide resolved

nextPuzzle = (): void => {
if (this.streak && this.lastFeedback != 'win') {
if (this.lastFeedback == 'fail') site.redirect(router.withLang('/streak'));
Expand Down
6 changes: 6 additions & 0 deletions ui/puzzle/src/xhr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ export const voteTheme = (puzzleId: string, theme: ThemeKey, vote: boolean | und
body: defined(vote) ? xhr.form({ vote }) : undefined,
});

export const report = (puzzleId: string, reason: string): Promise<void> =>
xhr.json(`/training/${puzzleId}/report`, {
method: 'POST',
body: xhr.form({ reason: reason }),
});

export const setZen = throttlePromiseDelay(
() => 1000,
zen =>
Expand Down
Loading