From c42316d9d8614305684a75adee05ef2078af73bc Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Thu, 12 Nov 2026 02:31:25 +0100 Subject: [PATCH 1/6] Plan des corrections : plis du prompt d'un coup, test Firefox instable Co-Authored-By: Claude Opus 5.5 --- .../2026-11-11-corrections-plis-firefox.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/plans/2026-11-11-corrections-plis-firefox.md diff --git a/docs/plans/2026-11-11-corrections-plis-firefox.md b/docs/plans/2026-11-11-corrections-plis-firefox.md new file mode 100644 index 0000000..e01af39 --- /dev/null +++ b/docs/plans/2026-11-11-corrections-plis-firefox.md @@ -0,0 +1,49 @@ +# Corrections : niveau des plis dans le prompt d'un coup, test Firefox instable + +- **Date** : 2026-11-11 +- **Étape** : 1f (suites) +- **Statut** : en cours +- **Exécutant** : sous-agent general-purpose + +## Objectif + +Le prompt d'un coup ne dit que ce que le moteur a calculé, au bon niveau, avec +les mêmes équités que l'application ; le test navigateur « mes parties » ne +dépend plus de la vitesse d'IndexedDB sous Firefox. + +## Consignes + +1. **Niveau d'évaluation dans le prompt d'un coup.** Le prompt + (`docs/prompts/coup.md`, `app/src/domain/prompts/`) dit en titre + « Classement de GNU Backgammon (analyse à 2 plis) », mais chaque ligne dit + « évalué à 1 pli » (ouverture 3-1 : « 8/5 6/5 : équité +0,204, écart 0,000, + évalué à 1 pli »). Trouver ce que vaut le champ `ply` des coups renvoyés par + le moteur (`engine.h`, `engine_best_moves`, commande `moves` d'engine-cli, + `rankMoves`) : décalage de convention, ou coups vraiment évalués à 1 pli, + auquel cas le classement n'est pas celui du conseil (`analyseMove`, 2 plis). + Vérifier que les équités du prompt sont celles de la feuille du maître et de + l'analyse (règle de 1f). Comparer au gnubg de bureau de référence. Corriger + la cause ; tester la cohérence du niveau du titre et des lignes, et + l'égalité des équités avec l'analyse. Si `engine.c` change (jamais + `engine/gnubg/`), recompiler une seule fois et ajouter une référence. +2. **Test instable sous Firefox.** `tests/browser/play.mjs` échoue parfois + sous Firefox, section « mes parties » (« la nouvelle partie n'a pas été + enregistrée », vers la ligne 1624) et passe à la relance. Déterminer s'il + s'agit d'un vrai défaut (sauvegarde perdue, course entre « nouvelle partie » + et l'enregistrement), à corriger dans l'app, ou d'un test qui lit trop tôt, + à corriger en attendant l'état observable plutôt qu'un délai. + +Limites : CSP stricte (aucun `.style.`), aucune nouvelle dépendance, `-j2` +au maximum, vérifier `docker ps` avant `make test-browser` (un autre agent +travaille en parallèle). + +## Critères de réussite + +- `make lint`, `make test`, `make test-browser` passent ; +- `SCRIPTS=play BROWSERS=firefox make test-browser` passe deux fois de suite ; +- un test vérifie la cohérence du niveau (titre et lignes) et l'égalité des + équités du prompt avec l'analyse. + +## Résultat + +Rempli à la clôture, dans la description de la merge request. -- 2.54.0 From 1d21ac15d22b41c387d63be8cf99ef18e37f87d2 Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Thu, 12 Nov 2026 02:47:38 +0100 Subject: [PATCH 2/6] Moteur : classement de l'analyse d'un coup, comme gnubg de bureau MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit engine_analyse_move rend les dix premiers coups de la liste de l'analyse, chacun au niveau où gnubg l'a évalué, et refait d'abord l'analyse de la chance comme AnalyzeMove : son cache change les coups restés à 0 pli. Les références d'analyse gagnent le niveau de chaque coup du classement, comparé par compare.py. Co-Authored-By: Claude Opus 5.5 --- engine-cli/main.c | 12 +- engine/engine.c | 40 ++++-- engine/include/engine.h | 22 +++- tests/reference/README.md | 10 +- .../analysis/move-bearoff-doubles.json | 2 + .../analysis/move-blitz-doubles.json | 119 ++++++++++++++++++ tests/reference/analysis/move-crawford.json | 20 +++ tests/reference/analysis/move-forced.json | 2 + tests/reference/analysis/move-jacoby.json | 24 ++++ tests/reference/analysis/move-luck-lucky.json | 20 +++ .../analysis/move-luck-unlucky-bearoff.json | 8 ++ .../reference/analysis/move-luck-unlucky.json | 20 +++ tests/reference/analysis/move-match.json | 20 +++ .../analysis/move-middlegame-blunder.json | 24 ++++ .../analysis/move-middlegame-close.json | 20 +++ .../analysis/move-opening-31-best.json | 20 +++ .../analysis/move-opening-31-error.json | 20 +++ .../analysis/move-opening-65-error.json | 14 +++ .../analysis/move-opening-double.json | 20 +++ tests/reference/analysis/move-race.json | 39 ++++++ tests/reference/compare.py | 30 +++++ tests/reference/generate.py | 29 +++-- tests/test_reference.sh | 3 +- 23 files changed, 516 insertions(+), 22 deletions(-) diff --git a/engine-cli/main.c b/engine-cli/main.c index bd05cda..421133e 100644 --- a/engine-cli/main.c +++ b/engine-cli/main.c @@ -43,7 +43,8 @@ * analyse du coup joué (POSITION_JOUÉE : * position après le coup, vue du joueur * qui l'a joué, comme position_id dans - * moves et legal) + * moves et legal), avec le classement + * de l'analyse (dix coups au plus) * analysecube POSITION_ID MATCH_ID DÉCISION * analyse d'une décision de videau : * no_double ou double (joueur au trait), @@ -684,7 +685,7 @@ run_analyse_move_command(const options *opt) const char *played_id = opt->args[3]; engine_move_analysis a; engine_status status; - int die1, die2; + int die1, die2, i; if (opt->arg_count != 4) return usage_error("arguments attendus : POSITION_ID MATCH_ID DÉS POSITION_JOUÉE"); @@ -705,6 +706,13 @@ run_analyse_move_command(const options *opt) printf(", \"best\": "); print_move(&a.best, 1); } + printf(", \"ranking\": ["); + for (i = 0; i < a.ranking_count; i++) { + if (i > 0) + printf(", "); + print_move(&a.ranking[i], 1); + } + printf("]"); printf(", \"equity_loss\": " NUMBER ", \"skill\": \"%s\"}\n", a.equity_loss, skill_names[a.skill]); return EXIT_SUCCESS; diff --git a/engine/engine.c b/engine/engine.c index d038d74..b40be99 100644 --- a/engine/engine.c +++ b/engine/engine.c @@ -512,6 +512,8 @@ engine_level_best_moves(const char *position_id, const char *match_id, int die1, /* ------------------------------------------------- Analyse d'un coup joué */ +static float luck_analysis(const char *position_id, position *pos, int die1, int die2); + engine_status engine_analyse_move(const char *position_id, const char *match_id, int die1, int die2, const char *played_position_id, int ply, engine_move_analysis *analysis) @@ -546,6 +548,17 @@ engine_analyse_move(const char *position_id, const char *match_id, int die1, int * premier. */ memset(analysis, 0, sizeof *analysis); + /* + * gnubg analyse la chance du jet juste avant le coup (AnalyzeMove, + * l. 679-682), à 0 pli sans élagage, et garde ces évaluations dans son + * cache, qui ne distingue pas une évaluation à 0 pli avec ou sans + * réseaux d'élagage : le tri à 0 pli de FindnSaveBestMoves les reprend + * ensuite, ce qui change l'équité des coups restés à 0 pli, et parfois + * ceux que les filtres retiennent. Faire de même, sans garder le + * résultat, donne le classement de gnubg de bureau. + */ + if (luck_analysis(position_id, &pos, die1, die2) == ERR_VAL) + return ENGINE_ERROR_INTERNAL; if (FindnSaveBestMoves(&list, die1, die2, (ConstTanBoard) pos.board, &played_key, TRUE, DOUBTFUL_THRESHOLD, &pos.cube, &context, aaamfMoveFilterSettings[0]) < 0) @@ -565,6 +578,10 @@ engine_analyse_move(const char *position_id, const char *match_id, int die1, int status = ENGINE_OK; break; } + /* Le classement : le début de la même liste, dans l'ordre de gnubg. */ + for (i = 0; i < list.cMoves && i < ENGINE_ANALYSIS_RANKING; i++) + fill_move(&analysis->ranking[i], &list.amMoves[i], &pos, 1); + analysis->ranking_count = (int) i; g_free(list.amMoves); return status; } @@ -1005,13 +1022,25 @@ luck_first(ConstTanBoard board, int i0, int j0, cubeinfo *cube) return equities[i0][j0] - mean / 30.0f; } +/* LuckAnalysis (analysis.c l. 252-268) : dés rangés du plus fort au plus + * faible ; LuckFirst à la position de départ hors doubles. */ +static float +luck_analysis(const char *position_id, position *pos, int die1, int die2) +{ + int high = (die1 > die2 ? die1 : die2) - 1; + int low = (die1 > die2 ? die2 : die1) - 1; + + if (strcmp(position_id, START_POSITION_ID) == 0 && high != low) + return luck_first((ConstTanBoard) pos->board, high, low, &pos->cube); + return luck_normal((ConstTanBoard) pos->board, high, low, &pos->cube); +} + engine_status engine_luck_of(const char *position_id, const char *match_id, int die1, int die2, engine_luck *luck) { position pos; engine_status status; - int high, low; float value; status = decode_position(position_id, match_id, &pos); @@ -1021,14 +1050,7 @@ engine_luck_of(const char *position_id, const char *match_id, int die1, int die2 return status; start_evaluation(); - /* LuckAnalysis (analysis.c l. 252-268) : dés rangés du plus fort au plus - * faible ; LuckFirst à la position de départ hors doubles. */ - high = (die1 > die2 ? die1 : die2) - 1; - low = (die1 > die2 ? die2 : die1) - 1; - if (strcmp(position_id, START_POSITION_ID) == 0 && high != low) - value = luck_first((ConstTanBoard) pos.board, high, low, &pos.cube); - else - value = luck_normal((ConstTanBoard) pos.board, high, low, &pos.cube); + value = luck_analysis(position_id, &pos, die1, die2); if (value == ERR_VAL) return ENGINE_ERROR_INTERNAL; luck->luck = value; diff --git a/engine/include/engine.h b/engine/include/engine.h index 4e99ed4..66197ad 100644 --- a/engine/include/engine.h +++ b/engine/include/engine.h @@ -207,6 +207,10 @@ typedef struct { * (double_take <= double_pass) */ } engine_cube; +/* Coups gardés dans le classement d'une analyse : les dix premiers, comme + * les positions de référence (tests/reference/README.md). */ +#define ENGINE_ANALYSIS_RANKING 10 + /* Analyse d'un coup joué, comme l'analyse de partie de gnubg. */ typedef struct { int move_count; /* nombre de coups légaux (0 : aucun coup @@ -218,6 +222,14 @@ typedef struct { float equity_loss; /* équité du coup joué moins celle du meilleur : * 0 ou négative */ engine_skill skill; /* appréciation de gnubg pour cette perte */ + /* Le classement de l'analyse, du meilleur au moins bon : les premiers + * coups de la liste que gnubg garde pour l'analyse de partie (ce que + * montre son panneau d'analyse), chacun au niveau où gnubg l'a + * évalué. Le meilleur coup et le coup joué y ont les mêmes évaluations + * que best et played ; le coup joué n'y figure que s'il est parmi les + * premiers. */ + engine_move ranking[ENGINE_ANALYSIS_RANKING]; + int ranking_count; } engine_move_analysis; /* Analyse d'une décision de videau, comme l'analyse de partie de gnubg. */ @@ -452,7 +464,15 @@ engine_luck_rating engine_luck_rating_of(float luck); * Analyse le coup joué par le joueur au trait avec le jet die1, die2, au * niveau ply, comme l'analyse de partie de gnubg (AnalyzeMove dans * analysis.c : FindnSaveBestMoves avec le coup joué, seuil « douteux », - * filtre « Normal », sans RefreshMoveList). played_position_id est la + * filtre « Normal », sans RefreshMoveList), classement compris. + * + * Niveaux : gnubg n'évalue au niveau ply que les coups retenus par ses + * filtres ; les autres gardent l'évaluation d'un niveau inférieur (champ + * ply de chaque coup, même convention que gnubg de bureau : 0 = statique). + * Si un seul coup passe le filtre du niveau ply - 1, gnubg s'arrête là, et + * tout le classement reste à ce niveau, sauf quand le coup joué est une + * erreur (écart au meilleur supérieur au seuil « douteux ») : le meilleur + * coup et le coup joué sont alors réévalués au niveau ply. played_position_id est la * position après le coup, vue du joueur qui vient de le jouer (champ * position_id d'engine_move). ENGINE_ERROR_MOVE si ce n'est pas un coup * légal ; si aucun coup n'est possible, move_count vaut 0. diff --git a/tests/reference/README.md b/tests/reference/README.md index 31b7f88..302285a 100644 --- a/tests/reference/README.md +++ b/tests/reference/README.md @@ -256,7 +256,11 @@ Un fichier de résultat reprend le cas, avec `type` (`move`, `cube` ou gnubg plus 1), `played_equity`, `best_move`, `best_equity`, `equity_loss` (perte du coup joué, 0 ou négative : statistique « error-skill » de gnubg changée de signe), `skill` (`none`, `doubtful`, `bad`, `very_bad`) et - `ranking` (les dix premiers coups, et jusqu'au coup joué) ; plus, une fois + `ranking` (les dix premiers coups, et jusqu'au coup joué, chacun avec + `move`, `ply` et `equity` : comme pour `hint`, `ply` est le niveau auquel + gnubg a réellement évalué le coup ; à la différence de `hint`, l'analyse + réévalue au niveau demandé le meilleur coup et le coup joué quand celui-ci + est une erreur, au-delà du seuil « douteux ») ; plus, une fois pour toutes, `luck` : `value` (chance du jet) et `rating` (`very_unlucky`, `unlucky`, `none`, `lucky`, `very_lucky`) ; - **videau** : `analysed` (gnubg analyse-t-il cette décision ? pas à la @@ -337,6 +341,10 @@ Pour chaque analyse (`engine-cli analysemove`, `analysecube`, `luck`, et équité que le coup joué, à la tolérance près : remarque), même meilleur coup sauf égalité, et `played_equity`, `best_equity`, `equity_loss` à la tolérance près ; +- classement de l'analyse (`ranking`, dix coups au plus) : chaque coup de + gnubg présent (sauf égalité avec le dixième), au même `ply` et à la même + équité à la tolérance près ; son premier coup et le coup joué y sont + exactement `best` et `played` ; - chance : `value` à la tolérance près ; - videau : même `analysed`, même décision et équités à la tolérance près, `equity_loss` à la tolérance près ; diff --git a/tests/reference/analysis/move-bearoff-doubles.json b/tests/reference/analysis/move-bearoff-doubles.json index cb72f8f..115bd53 100644 --- a/tests/reference/analysis/move-bearoff-doubles.json +++ b/tests/reference/analysis/move-bearoff-doubles.json @@ -25,6 +25,7 @@ "ranking": [ { "move": "6/off(2) 4/off 1/off", + "ply": 0, "equity": 0.8888838 } ] @@ -42,6 +43,7 @@ "ranking": [ { "move": "6/off(2) 4/off 1/off", + "ply": 0, "equity": 0.8888838 } ] diff --git a/tests/reference/analysis/move-blitz-doubles.json b/tests/reference/analysis/move-blitz-doubles.json index 3360416..0d73843 100644 --- a/tests/reference/analysis/move-blitz-doubles.json +++ b/tests/reference/analysis/move-blitz-doubles.json @@ -25,438 +25,547 @@ "ranking": [ { "move": "13/10 6/3 4/1*(2)", + "ply": 0, "equity": 0.9465638 }, { "move": "8/5 6/3 4/1*(2)", + "ply": 0, "equity": 0.9422797 }, { "move": "13/7 13/10 6/3", + "ply": 0, "equity": 0.94128984 }, { "move": "13/7 8/5 6/3", + "ply": 0, "equity": 0.9370698 }, { "move": "13/10(2) 7/4 6/3", + "ply": 0, "equity": 0.93615985 }, { "move": "13/10(2) 8/5 6/3", + "ply": 0, "equity": 0.9330082 }, { "move": "13/10 8/5 7/4 6/3", + "ply": 0, "equity": 0.932737 }, { "move": "13/10 7/1* 6/3", + "ply": 0, "equity": 0.9313148 }, { "move": "8/5 7/1* 6/3", + "ply": 0, "equity": 0.93129885 }, { "move": "13/4 6/3", + "ply": 0, "equity": 0.92980635 }, { "move": "13/10 8/5(2) 6/3", + "ply": 0, "equity": 0.9264537 }, { "move": "13/10(3) 6/3", + "ply": 0, "equity": 0.9262361 }, { "move": "13/10(2) 4/1*(2)", + "ply": 0, "equity": 0.92529553 }, { "move": "8/5(2) 7/4 6/3", + "ply": 0, "equity": 0.9252107 }, { "move": "7/1* 6/3 4/1", + "ply": 0, "equity": 0.92395884 }, { "move": "13/10 6/3 5/2(2)", + "ply": 0, "equity": 0.9204093 }, { "move": "13/7 4/1*(2)", + "ply": 0, "equity": 0.9190106 }, { "move": "13/7(2)", + "ply": 0, "equity": 0.9184398 }, { "move": "13/10 8/5 4/1*(2)", + "ply": 0, "equity": 0.9182539 }, { "move": "7/4 6/3 5/2(2)", + "ply": 0, "equity": 0.91752934 }, { "move": "13/7 13/10(2)", + "ply": 0, "equity": 0.91483337 }, { "move": "13/10 8/2 6/3", + "ply": 0, "equity": 0.912639 }, { "move": "13/7 13/10 8/5", + "ply": 0, "equity": 0.91152656 }, { "move": "13/10(2) 7/1*", + "ply": 0, "equity": 0.9115156 }, { "move": "13/1*", + "ply": 0, "equity": 0.9111869 }, { "move": "13/10(2) 6/3 4/1*", + "ply": 0, "equity": 0.91055506 }, { "move": "8/2 8/5 6/3", + "ply": 0, "equity": 0.9091699 }, { "move": "8/2 7/4 6/3", + "ply": 0, "equity": 0.90897554 }, { "move": "13/7 8/5(2)", + "ply": 0, "equity": 0.90817225 }, { "move": "13/7 6/3 4/1*", + "ply": 0, "equity": 0.90714073 }, { "move": "13/10(2) 8/5 7/4", + "ply": 0, "equity": 0.9070756 }, { "move": "8/2(2)", + "ply": 0, "equity": 0.9065211 }, { "move": "13/10 8/5 7/1*", + "ply": 0, "equity": 0.90571 }, { "move": "13/10(3) 7/4", + "ply": 0, "equity": 0.9040942 }, { "move": "13/10 8/5 6/3 4/1*", + "ply": 0, "equity": 0.90343916 }, { "move": "8/5(2) 4/1*(2)", + "ply": 0, "equity": 0.90325713 }, { "move": "5/2(2) 4/1*(2)", + "ply": 0, "equity": 0.90183336 }, { "move": "13/10 7/1* 4/1", + "ply": 0, "equity": 0.9013542 }, { "move": "13/4 7/4", + "ply": 0, "equity": 0.9010268 }, { "move": "13/7 5/2(2)", + "ply": 0, "equity": 0.90037674 }, { "move": "8/5(2) 7/1*", + "ply": 0, "equity": 0.89956194 }, { "move": "13/10 8/5(2) 7/4", + "ply": 0, "equity": 0.8988826 }, { "move": "8/2 6/3 5/2", + "ply": 0, "equity": 0.8984738 }, { "move": "13/4 13/10", + "ply": 0, "equity": 0.89749956 }, { "move": "6/3(2) 4/1*(2)", + "ply": 0, "equity": 0.8951012 }, { "move": "13/10(2) 8/5(2)", + "ply": 0, "equity": 0.89493525 }, { "move": "13/7 8/2", + "ply": 0, "equity": 0.8946096 }, { "move": "13/7 6/3(2)", + "ply": 0, "equity": 0.89403474 }, { "move": "7/1* 5/2(2)", + "ply": 0, "equity": 0.8924208 }, { "move": "8/5 7/1* 4/1", + "ply": 0, "equity": 0.891961 }, { "move": "13/10(2) 6/3(2)", + "ply": 0, "equity": 0.89077055 }, { "move": "13/4 8/5", + "ply": 0, "equity": 0.8907301 }, { "move": "8/5(2) 6/3 4/1*", + "ply": 0, "equity": 0.8900244 }, { "move": "13/10(2) 5/2(2)", + "ply": 0, "equity": 0.8890666 }, { "move": "7/4 6/3(3)", + "ply": 0, "equity": 0.8890004 }, { "move": "13/10 7/4 5/2(2)", + "ply": 0, "equity": 0.8888954 }, { "move": "8/2 8/5 7/4", + "ply": 0, "equity": 0.8876569 }, { "move": "13/10(4)", + "ply": 0, "equity": 0.88716996 }, { "move": "7/1* 6/3(2)", + "ply": 0, "equity": 0.8856077 }, { "move": "8/2 4/1*(2)", + "ply": 0, "equity": 0.8853629 }, { "move": "13/10 6/3(3)", + "ply": 0, "equity": 0.8853096 }, { "move": "13/7 6/3 5/2", + "ply": 0, "equity": 0.8831558 }, { "move": "13/10 8/2 7/4", + "ply": 0, "equity": 0.8819176 }, { "move": "7/1* 4/1(2)", + "ply": 0, "equity": 0.88113916 }, { "move": "13/10 7/4 6/3(2)", + "ply": 0, "equity": 0.88064635 }, { "move": "13/10(3) 8/5", + "ply": 0, "equity": 0.88049895 }, { "move": "6/3 5/2(2) 4/1*", + "ply": 0, "equity": 0.8790827 }, { "move": "13/10(2) 8/2", + "ply": 0, "equity": 0.8782023 }, { "move": "6/3 5/2 4/1*(2)", + "ply": 0, "equity": 0.8774586 }, { "move": "8/5 6/3(3)", + "ply": 0, "equity": 0.8774268 }, { "move": "8/2 7/1*", + "ply": 0, "equity": 0.8772669 }, { "move": "13/7 13/10 4/1*", + "ply": 0, "equity": 0.87700987 }, { "move": "13/10 8/5 6/3(2)", + "ply": 0, "equity": 0.8757389 }, { "move": "13/10 8/2 8/5", + "ply": 0, "equity": 0.87530077 }, { "move": "13/10(2) 6/3 5/2", + "ply": 0, "equity": 0.87215745 }, { "move": "13/7 8/5 4/1*", + "ply": 0, "equity": 0.87202764 }, { "move": "8/5 7/4 6/3(2)", + "ply": 0, "equity": 0.86896765 }, { "move": "7/1* 6/3 5/2", + "ply": 0, "equity": 0.8687051 }, { "move": "13/10 7/4 6/3 5/2", + "ply": 0, "equity": 0.8679464 }, { "move": "8/2 6/3 4/1*", + "ply": 0, "equity": 0.86750233 }, { "move": "13/10(2) 8/5 4/1*", + "ply": 0, "equity": 0.86701244 }, { "move": "8/5(2) 6/3(2)", + "ply": 0, "equity": 0.86167765 }, { "move": "13/10(3) 4/1*", + "ply": 0, "equity": 0.8568015 }, { "move": "6/3(2) 5/2(2)", + "ply": 0, "equity": 0.85501754 }, { "move": "8/2 7/4 5/2", + "ply": 0, "equity": 0.8537997 }, { "move": "8/2 5/2(2)", + "ply": 0, "equity": 0.8524791 }, { "move": "13/10 8/5(2) 4/1*", + "ply": 0, "equity": 0.84969926 }, { "move": "13/10 8/2 5/2", + "ply": 0, "equity": 0.8466199 }, { "move": "8/2 6/3(2)", + "ply": 0, "equity": 0.80814815 }, { "move": "13/10 5/2(2) 4/1*", + "ply": 0, "equity": 0.8037353 }, { "move": "13/10 5/2 4/1*(2)", + "ply": 0, "equity": 0.77350885 }, { "move": "13/10 6/3(2) 4/1*", + "ply": 0, "equity": 0.74067366 }, { "move": "13/7 13/10 5/2", + "ply": 0, "equity": 0.72703636 }, { "move": "13/10(2) 7/4 5/2", + "ply": 0, "equity": 0.7053956 }, { "move": "6/3(3) 4/1*", + "ply": 0, "equity": 0.66575944 }, { "move": "13/10 7/1* 5/2", + "ply": 0, "equity": 0.6462204 }, { "move": "8/5 6/3(2) 4/1*", + "ply": 0, "equity": 0.6374039 }, { "move": "8/2 8/5 4/1*", + "ply": 0, "equity": 0.62353325 }, { "move": "13/10 8/2 4/1*", + "ply": 0, "equity": 0.61257124 }, { "move": "13/10 6/3 5/2 4/1*", + "ply": 0, "equity": 0.5349428 }, { "move": "7/1* 5/2 4/1", + "ply": 0, "equity": 0.45721298 }, { "move": "8/2 5/2 4/1*", + "ply": 0, "equity": 0.44362232 }, { "move": "13/4 5/2", + "ply": 0, "equity": 0.43828273 }, { "move": "13/10(3) 5/2", + "ply": 0, "equity": 0.2798175 }, { "move": "13/10 6/3(2) 5/2", + "ply": 0, "equity": 0.24207813 }, { "move": "6/3(3) 5/2", + "ply": 0, "equity": 0.23385207 }, { "move": "7/4 6/3(2) 5/2", + "ply": 0, "equity": 0.22096781 }, { "move": "13/7 5/2 4/1*", + "ply": 0, "equity": 0.18849733 }, { "move": "13/10(2) 5/2 4/1*", + "ply": 0, "equity": 0.084023334 } ] @@ -474,42 +583,52 @@ "ranking": [ { "move": "13/10 7/1* 6/3", + "ply": 2, "equity": 1.0 }, { "move": "13/7 13/10 6/3", + "ply": 2, "equity": 1.0 }, { "move": "13/10(2) 8/5 6/3", + "ply": 2, "equity": 1.0 }, { "move": "13/7 8/5 6/3", + "ply": 2, "equity": 1.0 }, { "move": "13/10(2) 7/4 6/3", + "ply": 2, "equity": 1.0 }, { "move": "13/10 8/5 7/4 6/3", + "ply": 2, "equity": 1.0 }, { "move": "13/10(2) 5/2 4/1*", + "ply": 2, "equity": -0.022027964 }, { "move": "13/10 8/5(2) 6/3", + "ply": 2, "equity": 1.0 }, { "move": "13/4 6/3", + "ply": 2, "equity": 1.0 }, { "move": "13/10 6/3 4/1*(2)", + "ply": 2, "equity": 0.99692726 } ] diff --git a/tests/reference/analysis/move-crawford.json b/tests/reference/analysis/move-crawford.json index 90985fc..2ebbe8e 100644 --- a/tests/reference/analysis/move-crawford.json +++ b/tests/reference/analysis/move-crawford.json @@ -25,42 +25,52 @@ "ranking": [ { "move": "8/5 6/2", + "ply": 0, "equity": 0.7345603 }, { "move": "5/2 5/1*", + "ply": 0, "equity": 0.7229391 }, { "move": "8/1*", + "ply": 0, "equity": 0.71705425 }, { "move": "13/10 6/2", + "ply": 0, "equity": 0.7142835 }, { "move": "13/9 8/5", + "ply": 0, "equity": 0.6963019 }, { "move": "8/5 8/4", + "ply": 0, "equity": 0.68610644 }, { "move": "13/10 8/4", + "ply": 0, "equity": 0.6855368 }, { "move": "13/10 13/9", + "ply": 0, "equity": 0.6818689 }, { "move": "13/6", + "ply": 0, "equity": 0.6633271 }, { "move": "13/9 5/2", + "ply": 0, "equity": 0.6538005 } ] @@ -78,42 +88,52 @@ "ranking": [ { "move": "8/5 6/2", + "ply": 2, "equity": 0.7697691 }, { "move": "13/10 6/2", + "ply": 2, "equity": 0.7615671 }, { "move": "13/9 8/5", + "ply": 2, "equity": 0.69608325 }, { "move": "5/2 5/1*", + "ply": 2, "equity": 0.6911188 }, { "move": "8/1*", + "ply": 2, "equity": 0.6836242 }, { "move": "13/10 13/9", + "ply": 2, "equity": 0.6807338 }, { "move": "13/10 8/4", + "ply": 1, "equity": 0.6733323 }, { "move": "13/6", + "ply": 1, "equity": 0.66055346 }, { "move": "8/5 8/4", + "ply": 1, "equity": 0.6556259 }, { "move": "13/9 5/2", + "ply": 1, "equity": 0.645951 } ] diff --git a/tests/reference/analysis/move-forced.json b/tests/reference/analysis/move-forced.json index 755a2e4..acdd967 100644 --- a/tests/reference/analysis/move-forced.json +++ b/tests/reference/analysis/move-forced.json @@ -25,6 +25,7 @@ "ranking": [ { "move": "bar/24", + "ply": 0, "equity": -1.0 } ] @@ -42,6 +43,7 @@ "ranking": [ { "move": "bar/24", + "ply": 0, "equity": -1.0 } ] diff --git a/tests/reference/analysis/move-jacoby.json b/tests/reference/analysis/move-jacoby.json index 1deab70..2a98cb3 100644 --- a/tests/reference/analysis/move-jacoby.json +++ b/tests/reference/analysis/move-jacoby.json @@ -25,58 +25,72 @@ "ranking": [ { "move": "13/3", + "ply": 0, "equity": 0.9328213 }, { "move": "13/9 13/7", + "ply": 0, "equity": 0.9175232 }, { "move": "8/2 7/3", + "ply": 0, "equity": 0.91263336 }, { "move": "13/7 8/4", + "ply": 0, "equity": 0.90736973 }, { "move": "13/9 7/1*", + "ply": 0, "equity": 0.9053596 }, { "move": "8/2 6/2", + "ply": 0, "equity": 0.9017469 }, { "move": "8/4 7/1*", + "ply": 0, "equity": 0.89993495 }, { "move": "13/7 6/2", + "ply": 0, "equity": 0.8962312 }, { "move": "7/1* 5/1", + "ply": 0, "equity": 0.8907822 }, { "move": "7/1* 6/2", + "ply": 0, "equity": 0.87481093 }, { "move": "13/9 8/2", + "ply": 0, "equity": 0.87251335 }, { "move": "8/4 8/2", + "ply": 0, "equity": 0.8716274 }, { "move": "13/7 5/1*", + "ply": 0, "equity": 0.87005275 }, { "move": "8/2 5/1*", + "ply": 0, "equity": 0.50889754 } ] @@ -94,42 +108,52 @@ "ranking": [ { "move": "13/3", + "ply": 2, "equity": 1.0 }, { "move": "13/9 13/7", + "ply": 2, "equity": 0.99894416 }, { "move": "8/2 7/3", + "ply": 2, "equity": 0.99641544 }, { "move": "13/7 8/4", + "ply": 2, "equity": 0.98718643 }, { "move": "13/7 6/2", + "ply": 2, "equity": 0.9808178 }, { "move": "13/9 7/1*", + "ply": 2, "equity": 0.9452111 }, { "move": "8/2 5/1*", + "ply": 2, "equity": 0.3304975 }, { "move": "8/4 7/1*", + "ply": 1, "equity": 0.93114305 }, { "move": "7/1* 5/1", + "ply": 1, "equity": 0.91711015 }, { "move": "8/2 6/2", + "ply": 1, "equity": 0.9160911 } ] diff --git a/tests/reference/analysis/move-luck-lucky.json b/tests/reference/analysis/move-luck-lucky.json index f98e789..892af6b 100644 --- a/tests/reference/analysis/move-luck-lucky.json +++ b/tests/reference/analysis/move-luck-lucky.json @@ -25,42 +25,52 @@ "ranking": [ { "move": "13/5 11/3", + "ply": 0, "equity": 0.16933289 }, { "move": "13/9(2) 11/3", + "ply": 0, "equity": 0.16701066 }, { "move": "13/5 9/5 7/3", + "ply": 0, "equity": 0.16552198 }, { "move": "13/5(2)", + "ply": 0, "equity": 0.16473158 }, { "move": "13/5 13/9 11/7", + "ply": 0, "equity": 0.16365689 }, { "move": "13/5 13/9 7/3", + "ply": 0, "equity": 0.16245355 }, { "move": "13/5 11/7 9/5", + "ply": 0, "equity": 0.16167049 }, { "move": "11/3 9/5(2)", + "ply": 0, "equity": 0.16043647 }, { "move": "13/9 11/3 11/7", + "ply": 0, "equity": 0.1603657 }, { "move": "13/5 11/7(2)", + "ply": 0, "equity": 0.1602792 } ] @@ -78,42 +88,52 @@ "ranking": [ { "move": "13/5 11/3", + "ply": 2, "equity": 0.15129912 }, { "move": "13/5 13/9 11/7", + "ply": 2, "equity": 0.15059008 }, { "move": "13/9(2) 11/3", + "ply": 2, "equity": 0.14950007 }, { "move": "13/5 11/7 9/5", + "ply": 2, "equity": 0.14858595 }, { "move": "11/3 11/7 9/5", + "ply": 2, "equity": 0.14829463 }, { "move": "13/9 11/3 11/7", + "ply": 2, "equity": 0.1463402 }, { "move": "13/5 9/5 7/3", + "ply": 1, "equity": 0.13938732 }, { "move": "13/5 11/7(2)", + "ply": 1, "equity": 0.13921705 }, { "move": "13/9(2) 11/7(2)", + "ply": 1, "equity": 0.13873637 }, { "move": "13/5 13/9 6/2", + "ply": 1, "equity": 0.13852078 } ] diff --git a/tests/reference/analysis/move-luck-unlucky-bearoff.json b/tests/reference/analysis/move-luck-unlucky-bearoff.json index a96a1ff..2d15e71 100644 --- a/tests/reference/analysis/move-luck-unlucky-bearoff.json +++ b/tests/reference/analysis/move-luck-unlucky-bearoff.json @@ -25,18 +25,22 @@ "ranking": [ { "move": "3/off", + "ply": 0, "equity": -0.8607614 }, { "move": "2/off 1/off", + "ply": 0, "equity": -0.8607614 }, { "move": "2/1 2/off", + "ply": 0, "equity": -0.95561147 }, { "move": "3/1 2/1", + "ply": 0, "equity": -1.0 } ] @@ -54,18 +58,22 @@ "ranking": [ { "move": "3/off", + "ply": 2, "equity": -0.8607614 }, { "move": "2/off 1/off", + "ply": 2, "equity": -0.8607614 }, { "move": "2/1 2/off", + "ply": 2, "equity": -0.95561147 }, { "move": "3/1 2/1", + "ply": 1, "equity": -1.0 } ] diff --git a/tests/reference/analysis/move-luck-unlucky.json b/tests/reference/analysis/move-luck-unlucky.json index 16cb6c9..9298b95 100644 --- a/tests/reference/analysis/move-luck-unlucky.json +++ b/tests/reference/analysis/move-luck-unlucky.json @@ -25,42 +25,52 @@ "ranking": [ { "move": "18/9", + "ply": 0, "equity": 0.2544027 }, { "move": "18/14 13/8", + "ply": 0, "equity": 0.22496489 }, { "move": "24/20 18/13", + "ply": 0, "equity": 0.20747662 }, { "move": "24/20 13/8", + "ply": 0, "equity": 0.1158272 }, { "move": "13/9 13/8", + "ply": 0, "equity": 0.09201278 }, { "move": "24/15", + "ply": 0, "equity": 0.061017293 }, { "move": "18/13 6/2", + "ply": 0, "equity": 0.03581623 }, { "move": "13/4", + "ply": 0, "equity": -0.028887823 }, { "move": "13/8 6/2", + "ply": 0, "equity": -0.033572298 }, { "move": "18/13 8/4", + "ply": 0, "equity": -0.09662818 } ] @@ -78,42 +88,52 @@ "ranking": [ { "move": "18/9", + "ply": 2, "equity": 0.27687487 }, { "move": "18/14 13/8", + "ply": 2, "equity": 0.21801135 }, { "move": "24/20 18/13", + "ply": 2, "equity": 0.21000355 }, { "move": "24/20 13/8", + "ply": 2, "equity": 0.1144446 }, { "move": "13/9 13/8", + "ply": 2, "equity": 0.09412725 }, { "move": "24/15", + "ply": 2, "equity": 0.07892294 }, { "move": "18/13 6/2", + "ply": 1, "equity": -0.020656409 }, { "move": "13/4", + "ply": 0, "equity": -0.028887823 }, { "move": "13/8 6/2", + "ply": 0, "equity": -0.033572298 }, { "move": "18/13 8/4", + "ply": 0, "equity": -0.09662818 } ] diff --git a/tests/reference/analysis/move-match.json b/tests/reference/analysis/move-match.json index 6eb7a82..dc63991 100644 --- a/tests/reference/analysis/move-match.json +++ b/tests/reference/analysis/move-match.json @@ -25,42 +25,52 @@ "ranking": [ { "move": "13/5", + "ply": 0, "equity": 0.7553502 }, { "move": "18/13 8/5", + "ply": 0, "equity": 0.5814649 }, { "move": "24/16*", + "ply": 0, "equity": 0.4581785 }, { "move": "18/10", + "ply": 0, "equity": 0.31719947 }, { "move": "8/5 8/3", + "ply": 0, "equity": 0.31371918 }, { "move": "18/15 13/8", + "ply": 0, "equity": 0.26037523 }, { "move": "24/21 18/13", + "ply": 0, "equity": 0.23547156 }, { "move": "8/3 6/3", + "ply": 0, "equity": 0.15432969 }, { "move": "13/10 13/8", + "ply": 0, "equity": 0.13837479 }, { "move": "18/13 5/2", + "ply": 0, "equity": 0.12815686 } ] @@ -78,42 +88,52 @@ "ranking": [ { "move": "13/5", + "ply": 2, "equity": 0.85497326 }, { "move": "24/16*", + "ply": 2, "equity": 0.46805125 }, { "move": "18/13 8/5", + "ply": 1, "equity": 0.6511238 }, { "move": "18/10", + "ply": 0, "equity": 0.31719947 }, { "move": "8/5 8/3", + "ply": 0, "equity": 0.31371918 }, { "move": "18/15 13/8", + "ply": 0, "equity": 0.26037523 }, { "move": "24/21 18/13", + "ply": 0, "equity": 0.23547156 }, { "move": "8/3 6/3", + "ply": 0, "equity": 0.15432969 }, { "move": "13/10 13/8", + "ply": 0, "equity": 0.13837479 }, { "move": "18/13 5/2", + "ply": 0, "equity": 0.12815686 } ] diff --git a/tests/reference/analysis/move-middlegame-blunder.json b/tests/reference/analysis/move-middlegame-blunder.json index e581af8..3861a4d 100644 --- a/tests/reference/analysis/move-middlegame-blunder.json +++ b/tests/reference/analysis/move-middlegame-blunder.json @@ -25,58 +25,72 @@ "ranking": [ { "move": "24/14", + "ply": 0, "equity": 0.15439394 }, { "move": "8/2 6/2", + "ply": 0, "equity": 0.10821653 }, { "move": "24/18 13/9", + "ply": 0, "equity": 0.10337136 }, { "move": "24/20 24/18", + "ply": 0, "equity": 0.072762795 }, { "move": "13/9 13/7", + "ply": 0, "equity": 0.0029048775 }, { "move": "24/20 13/7", + "ply": 0, "equity": 9.1438415e-05 }, { "move": "13/3", + "ply": 0, "equity": -0.012697897 }, { "move": "24/18 6/2", + "ply": 0, "equity": -0.075399 }, { "move": "24/18 8/4", + "ply": 0, "equity": -0.088734664 }, { "move": "24/20 8/2", + "ply": 0, "equity": -0.095189385 }, { "move": "13/9 8/2", + "ply": 0, "equity": -0.09528468 }, { "move": "13/7 8/4", + "ply": 0, "equity": -0.12819159 }, { "move": "13/7 6/2", + "ply": 0, "equity": -0.14467186 }, { "move": "8/4 8/2", + "ply": 0, "equity": -0.35321844 } ] @@ -94,42 +108,52 @@ "ranking": [ { "move": "24/14", + "ply": 2, "equity": 0.13111727 }, { "move": "8/2 6/2", + "ply": 2, "equity": 0.11303549 }, { "move": "24/18 13/9", + "ply": 2, "equity": 0.105096616 }, { "move": "24/20 24/18", + "ply": 2, "equity": 0.049068402 }, { "move": "13/3", + "ply": 2, "equity": -0.0040049786 }, { "move": "8/4 8/2", + "ply": 2, "equity": -0.36824828 }, { "move": "13/9 13/7", + "ply": 2, "equity": -0.014309813 }, { "move": "24/20 13/7", + "ply": 1, "equity": -0.029367238 }, { "move": "24/18 8/4", + "ply": 1, "equity": -0.081171095 }, { "move": "24/18 6/2", + "ply": 1, "equity": -0.09313507 } ] diff --git a/tests/reference/analysis/move-middlegame-close.json b/tests/reference/analysis/move-middlegame-close.json index 59487b5..1ef6e82 100644 --- a/tests/reference/analysis/move-middlegame-close.json +++ b/tests/reference/analysis/move-middlegame-close.json @@ -25,42 +25,52 @@ "ranking": [ { "move": "24/14", + "ply": 0, "equity": 0.15439394 }, { "move": "8/2 6/2", + "ply": 0, "equity": 0.10821653 }, { "move": "24/18 13/9", + "ply": 0, "equity": 0.10337136 }, { "move": "24/20 24/18", + "ply": 0, "equity": 0.072762795 }, { "move": "13/9 13/7", + "ply": 0, "equity": 0.0029048775 }, { "move": "24/20 13/7", + "ply": 0, "equity": 9.1438415e-05 }, { "move": "13/3", + "ply": 0, "equity": -0.012697897 }, { "move": "24/18 6/2", + "ply": 0, "equity": -0.075399 }, { "move": "24/18 8/4", + "ply": 0, "equity": -0.088734664 }, { "move": "24/20 8/2", + "ply": 0, "equity": -0.095189385 } ] @@ -78,42 +88,52 @@ "ranking": [ { "move": "24/14", + "ply": 2, "equity": 0.13111727 }, { "move": "8/2 6/2", + "ply": 2, "equity": 0.11303549 }, { "move": "24/18 13/9", + "ply": 2, "equity": 0.105096616 }, { "move": "24/20 24/18", + "ply": 2, "equity": 0.049068402 }, { "move": "13/3", + "ply": 2, "equity": -0.0040049786 }, { "move": "13/9 13/7", + "ply": 2, "equity": -0.014309813 }, { "move": "24/20 13/7", + "ply": 1, "equity": -0.029367238 }, { "move": "24/18 8/4", + "ply": 1, "equity": -0.081171095 }, { "move": "24/18 6/2", + "ply": 1, "equity": -0.09313507 }, { "move": "24/20 8/2", + "ply": 0, "equity": -0.095189385 } ] diff --git a/tests/reference/analysis/move-opening-31-best.json b/tests/reference/analysis/move-opening-31-best.json index 50dbcd5..ba22887 100644 --- a/tests/reference/analysis/move-opening-31-best.json +++ b/tests/reference/analysis/move-opening-31-best.json @@ -25,42 +25,52 @@ "ranking": [ { "move": "8/5 6/5", + "ply": 0, "equity": 0.21258597 }, { "move": "24/23 13/10", + "ply": 0, "equity": -0.010789523 }, { "move": "24/20", + "ply": 0, "equity": -0.017392516 }, { "move": "24/23 24/21", + "ply": 0, "equity": -0.023609947 }, { "move": "13/9", + "ply": 0, "equity": -0.027675275 }, { "move": "24/21 6/5", + "ply": 0, "equity": -0.04369401 }, { "move": "13/10 6/5", + "ply": 0, "equity": -0.04984435 }, { "move": "24/23 8/5", + "ply": 0, "equity": -0.104100645 }, { "move": "24/23 6/3", + "ply": 0, "equity": -0.10542354 }, { "move": "24/21 8/7", + "ply": 0, "equity": -0.107585564 } ] @@ -78,42 +88,52 @@ "ranking": [ { "move": "8/5 6/5", + "ply": 1, "equity": 0.20373425 }, { "move": "24/23 13/10", + "ply": 1, "equity": -0.022293653 }, { "move": "24/20", + "ply": 1, "equity": -0.031027086 }, { "move": "24/23 24/21", + "ply": 1, "equity": -0.03213631 }, { "move": "13/9", + "ply": 1, "equity": -0.040205758 }, { "move": "24/21 6/5", + "ply": 0, "equity": -0.04369401 }, { "move": "13/10 6/5", + "ply": 0, "equity": -0.04984435 }, { "move": "24/23 8/5", + "ply": 0, "equity": -0.104100645 }, { "move": "24/23 6/3", + "ply": 0, "equity": -0.10542354 }, { "move": "24/21 8/7", + "ply": 0, "equity": -0.107585564 } ] diff --git a/tests/reference/analysis/move-opening-31-error.json b/tests/reference/analysis/move-opening-31-error.json index 0ac69a9..7ac6e98 100644 --- a/tests/reference/analysis/move-opening-31-error.json +++ b/tests/reference/analysis/move-opening-31-error.json @@ -25,42 +25,52 @@ "ranking": [ { "move": "8/5 6/5", + "ply": 0, "equity": 0.21258597 }, { "move": "24/23 13/10", + "ply": 0, "equity": -0.010789523 }, { "move": "24/20", + "ply": 0, "equity": -0.017392516 }, { "move": "24/23 24/21", + "ply": 0, "equity": -0.023609947 }, { "move": "13/9", + "ply": 0, "equity": -0.027675275 }, { "move": "24/21 6/5", + "ply": 0, "equity": -0.04369401 }, { "move": "13/10 6/5", + "ply": 0, "equity": -0.04984435 }, { "move": "24/23 8/5", + "ply": 0, "equity": -0.104100645 }, { "move": "24/23 6/3", + "ply": 0, "equity": -0.10542354 }, { "move": "24/21 8/7", + "ply": 0, "equity": -0.107585564 } ] @@ -78,42 +88,52 @@ "ranking": [ { "move": "8/5 6/5", + "ply": 2, "equity": 0.1998143 }, { "move": "24/23 13/10", + "ply": 2, "equity": -0.0118339015 }, { "move": "24/20", + "ply": 1, "equity": -0.031027086 }, { "move": "24/23 24/21", + "ply": 1, "equity": -0.03213631 }, { "move": "13/9", + "ply": 1, "equity": -0.040205758 }, { "move": "24/21 6/5", + "ply": 0, "equity": -0.04369401 }, { "move": "13/10 6/5", + "ply": 0, "equity": -0.04984435 }, { "move": "24/23 8/5", + "ply": 0, "equity": -0.104100645 }, { "move": "24/23 6/3", + "ply": 0, "equity": -0.10542354 }, { "move": "24/21 8/7", + "ply": 0, "equity": -0.107585564 } ] diff --git a/tests/reference/analysis/move-opening-65-error.json b/tests/reference/analysis/move-opening-65-error.json index 0fbce8c..b7fdd2e 100644 --- a/tests/reference/analysis/move-opening-65-error.json +++ b/tests/reference/analysis/move-opening-65-error.json @@ -25,30 +25,37 @@ "ranking": [ { "move": "24/13", + "ply": 0, "equity": 0.082985915 }, { "move": "24/18 13/8", + "ply": 0, "equity": 0.03631018 }, { "move": "13/8 13/7", + "ply": 0, "equity": -0.05142035 }, { "move": "13/2", + "ply": 0, "equity": -0.13522762 }, { "move": "24/18 8/3", + "ply": 0, "equity": -0.14635906 }, { "move": "13/7 8/3", + "ply": 0, "equity": -0.17384717 }, { "move": "8/3 8/2", + "ply": 0, "equity": -0.37779096 } ] @@ -66,30 +73,37 @@ "ranking": [ { "move": "24/13", + "ply": 2, "equity": 0.07996891 }, { "move": "24/18 13/8", + "ply": 2, "equity": 0.03882884 }, { "move": "13/8 13/7", + "ply": 2, "equity": -0.061850794 }, { "move": "13/2", + "ply": 1, "equity": -0.12083713 }, { "move": "24/18 8/3", + "ply": 1, "equity": -0.13999933 }, { "move": "13/7 8/3", + "ply": 0, "equity": -0.17384717 }, { "move": "8/3 8/2", + "ply": 0, "equity": -0.37779096 } ] diff --git a/tests/reference/analysis/move-opening-double.json b/tests/reference/analysis/move-opening-double.json index 5ab51f1..c0b255e 100644 --- a/tests/reference/analysis/move-opening-double.json +++ b/tests/reference/analysis/move-opening-double.json @@ -25,42 +25,52 @@ "ranking": [ { "move": "24/18(2) 13/7(2)", + "ply": 0, "equity": 0.539388 }, { "move": "24/18 13/7(3)", + "ply": 0, "equity": 0.33099693 }, { "move": "24/18(2) 8/2(2)", + "ply": 0, "equity": 0.27119082 }, { "move": "13/7(2) 8/2(2)", + "ply": 0, "equity": 0.1563462 }, { "move": "24/18 13/7(2) 8/2", + "ply": 0, "equity": 0.14750561 }, { "move": "13/7(4)", + "ply": 0, "equity": 0.14310652 }, { "move": "24/18(2) 13/7 8/2", + "ply": 0, "equity": 0.12353878 }, { "move": "13/7(3) 8/2", + "ply": 0, "equity": 0.101389736 }, { "move": "24/18 13/7 8/2(2)", + "ply": 0, "equity": -0.05435923 }, { "move": "24/18 8/2(3)", + "ply": 0, "equity": -0.12482335 } ] @@ -78,42 +88,52 @@ "ranking": [ { "move": "24/18(2) 13/7(2)", + "ply": 1, "equity": 0.5886327 }, { "move": "24/18 13/7(3)", + "ply": 1, "equity": 0.33312234 }, { "move": "24/18(2) 8/2(2)", + "ply": 0, "equity": 0.27119082 }, { "move": "13/7(2) 8/2(2)", + "ply": 0, "equity": 0.1563462 }, { "move": "24/18 13/7(2) 8/2", + "ply": 0, "equity": 0.14750561 }, { "move": "13/7(4)", + "ply": 0, "equity": 0.14310652 }, { "move": "24/18(2) 13/7 8/2", + "ply": 0, "equity": 0.12353878 }, { "move": "13/7(3) 8/2", + "ply": 0, "equity": 0.101389736 }, { "move": "24/18 13/7 8/2(2)", + "ply": 0, "equity": -0.05435923 }, { "move": "24/18 8/2(3)", + "ply": 0, "equity": -0.12482335 } ] diff --git a/tests/reference/analysis/move-race.json b/tests/reference/analysis/move-race.json index 09dea1e..bd3b039 100644 --- a/tests/reference/analysis/move-race.json +++ b/tests/reference/analysis/move-race.json @@ -25,118 +25,147 @@ "ranking": [ { "move": "13/9 11/6", + "ply": 0, "equity": -0.22230339 }, { "move": "11/6 9/5", + "ply": 0, "equity": -0.22993731 }, { "move": "13/8 9/5", + "ply": 0, "equity": -0.23005769 }, { "move": "13/9 13/8", + "ply": 0, "equity": -0.23073325 }, { "move": "13/8 11/7", + "ply": 0, "equity": -0.23153114 }, { "move": "11/7 11/6", + "ply": 0, "equity": -0.2335976 }, { "move": "13/8 7/3", + "ply": 0, "equity": -0.23374501 }, { "move": "11/6 7/3", + "ply": 0, "equity": -0.23394687 }, { "move": "13/4", + "ply": 0, "equity": -0.23507257 }, { "move": "13/9 7/2", + "ply": 0, "equity": -0.23529215 }, { "move": "7/3 7/2", + "ply": 0, "equity": -0.23544219 }, { "move": "11/2", + "ply": 0, "equity": -0.235758 }, { "move": "11/7 9/4", + "ply": 0, "equity": -0.23589191 }, { "move": "13/8 6/2", + "ply": 0, "equity": -0.24026787 }, { "move": "9/5 7/2", + "ply": 0, "equity": -0.24087214 }, { "move": "13/8 5/1", + "ply": 0, "equity": -0.24252185 }, { "move": "9/5 9/4", + "ply": 0, "equity": -0.2440444 }, { "move": "9/4 6/2", + "ply": 0, "equity": -0.24487257 }, { "move": "9/4 7/3", + "ply": 0, "equity": -0.24512678 }, { "move": "11/6 5/1", + "ply": 0, "equity": -0.25016445 }, { "move": "9/4 5/1", + "ply": 0, "equity": -0.25572774 }, { "move": "7/2 5/1", + "ply": 0, "equity": -0.258138 }, { "move": "7/3 6/1", + "ply": 0, "equity": -0.25878713 }, { "move": "7/2 6/2", + "ply": 0, "equity": -0.26088732 }, { "move": "13/9 6/1", + "ply": 0, "equity": -0.26104733 }, { "move": "9/5 6/1", + "ply": 0, "equity": -0.26680842 }, { "move": "11/7 6/1", + "ply": 0, "equity": -0.2670043 }, { "move": "6/2 6/1", + "ply": 0, "equity": -0.27360544 }, { "move": "6/1 5/1", + "ply": 0, "equity": -0.31466243 } ] @@ -154,42 +183,52 @@ "ranking": [ { "move": "13/9 11/6", + "ply": 2, "equity": -0.21922025 }, { "move": "13/9 13/8", + "ply": 2, "equity": -0.2202243 }, { "move": "11/6 9/5", + "ply": 2, "equity": -0.22022447 }, { "move": "13/8 9/5", + "ply": 2, "equity": -0.22088149 }, { "move": "11/7 11/6", + "ply": 2, "equity": -0.22277355 }, { "move": "13/4", + "ply": 2, "equity": -0.2230124 }, { "move": "6/1 5/1", + "ply": 2, "equity": -0.29347342 }, { "move": "11/7 9/4", + "ply": 1, "equity": -0.22985312 }, { "move": "13/8 11/7", + "ply": 1, "equity": -0.23169218 }, { "move": "13/9 7/2", + "ply": 1, "equity": -0.23561361 } ] diff --git a/tests/reference/compare.py b/tests/reference/compare.py index 677da81..8bdd272 100644 --- a/tests/reference/compare.py +++ b/tests/reference/compare.py @@ -210,6 +210,36 @@ def compare_move_analysis(report, cli, case): report.number(where, "equity_loss", expected["equity_loss"], actual["equity_loss"]) same_label(report, where, "skill", expected["skill"], actual["skill"], expected["equity_loss"], SKILL_THRESHOLDS) + compare_analysis_ranking(report, where, expected, actual) + + +def compare_analysis_ranking(report, where, expected, actual): + """Classement de l'analyse : les dix premiers coups de gnubg (le moteur + n'en rend pas plus), au même niveau et à la même équité, à la tolérance + près ; le meilleur coup et le coup joué y ont exactement les évaluations + de best et played.""" + engine = actual["ranking"] + reference = expected["ranking"][:10] + report.same(where, "taille du classement", len(reference), len(engine)) + by_move = {m["move"]: m for m in engine} + for m in reference: + other = by_move.get(m["move"]) + here = "%s classement %s" % (where, m["move"]) + if other is None: + # Un coup à égalité avec le dixième peut changer de côté. + tied = abs(m["equity"] - reference[-1]["equity"]) <= report.tolerance + if not tied: + report.same(here, "présent", True, False) + continue + report.same(here, "ply", m["ply"], other["ply"]) + report.number(here, "equity", m["equity"], other["equity"]) + if engine: + report.same(where, "classement, meilleur coup", actual["best"], engine[0]) + for m in engine: + if m["move"] == actual["played"]["move"]: + report.same(where, "classement, coup joué", actual["played"], m) + report.same(where, "classement, rang du coup joué", actual["rank"], + engine.index(m) + 1) def compare_cube_analysis(report, cli, case): diff --git a/tests/reference/generate.py b/tests/reference/generate.py index 858abaf..2a6b9b6 100644 --- a/tests/reference/generate.py +++ b/tests/reference/generate.py @@ -221,15 +221,25 @@ def set_analysis(ply): def analyse_game(): - """Analyse la partie en cours ; renvoie ses enregistrements et ses - statistiques par joueur (« X », « O »). Le cache est vidé avant : les + """Analyse la partie en cours ; renvoie ses enregistrements, ses + statistiques par joueur (« X », « O ») et les informations du match. Le cache est vidé avant : les évaluations à 2 plis faites pour choisir un coup y laissent des résultats à 0 pli des réseaux d'élagage, que l'analyse à 0 pli reprendrait (voir analyse_move).""" gnubg.command("clear cache") gnubg.command("analyse game") - game = gnubg.match(analysis=1, verbose=1, statistics=1, boards=1)["games"][-1] - return game["game"], game["stats"] + match = gnubg.match(analysis=1, verbose=1, statistics=1, boards=1) + game = match["games"][-1] + return game["game"], game["stats"], match["match-info"] + + +def move_plies(match_info, move): + """Niveau auquel gnubg a évalué un coup de l'analyse : gnubg.match ne + donne le réglage d'un coup (« evalcontext ») que s'il diffère du réglage + par défaut de la partie (« default-eval-context », celui du premier coup + évalué rencontré).""" + context = move.get("evalcontext", {}) + return context.get("plies", match_info["default-eval-context"]["plies"]) def last_record(records, action): @@ -280,7 +290,7 @@ def analyse_move(case, ply): # vient de jouer est la position retournée. played_id = gnubg.positionid(swapped(gnubg.board())) set_analysis(ply) - records, stats = analyse_game() + records, stats, match_info = analyse_game() record = last_record(records, "move") analysis = record["analysis"] moves, index = analysis["moves"], analysis["imove"] @@ -304,7 +314,10 @@ def analyse_move(case, ply): # comptée en positif. "equity_loss": f32(-stats[record["player"]]["moves"]["error-skill"]), "skill": SKILLS[analysis.get("skill")], - "ranking": [{"move": notation(m["move"]), "equity": f32(m["score"])} for m in kept], + # Avec le niveau de chaque coup : les coups écartés par les filtres + # gardent celui d'un niveau inférieur (voir README.md). + "ranking": [{"move": notation(m["move"]), "ply": move_plies(match_info, m), + "equity": f32(m["score"])} for m in kept], } luck = {"value": f32(analysis["luck-value"]), "rating": LUCKS[analysis.get("luck")]} return level, luck @@ -322,7 +335,7 @@ def analyse_cube(case, ply): # meilleur coup, l'analyse du videau portant sur la position avant. gnubg.command("set dice 2 1") gnubg.command("move " + best_move()) - records, stats = analyse_game() + records, stats, _ = analyse_game() record = last_record(records, "move") analysed = "nd-cubeful-eq" in record["analysis"] skill = record["analysis"].get("cube-skill") @@ -332,7 +345,7 @@ def analyse_cube(case, ply): # du double. gnubg.command("double") gnubg.command("drop" if action == "pass" else "take") - records, stats = analyse_game() + records, stats, _ = analyse_game() analysed = "nd-cubeful-eq" in last_record(records, "double").get("analysis", {}) record = last_record(records, {"double": "double", "take": "take", "pass": "drop"}[action]) skill = record.get("analysis", {}).get("skill") diff --git a/tests/test_reference.sh b/tests/test_reference.sh index 12bb5db..ca49b0c 100644 --- a/tests/test_reference.sh +++ b/tests/test_reference.sh @@ -130,7 +130,8 @@ if .type == "move" then and (.played_position_id | test("^[A-Za-z0-9+/]{14}$")) and (.played_equity | nombre) and (.best_move | type) == "string" and (.best_equity | nombre) and (.equity_loss | nombre) and .equity_loss <= 0 and (.skill | skill) - and (.ranking | length) >= ([.rank, ([.move_count, 10] | min)] | max)) + and (.ranking | length) >= ([.rank, ([.move_count, 10] | min)] | max) + and all(.ranking[]; (.move | type) == "string" and (.ply | nombre) and (.equity | nombre))) then empty else "analyse de coup incomplète" end) elif .type == "cube" then (if (.plies | keys) == ["0", "2"] and (.action | IN("no_double", "double", "take", "pass")) -- 2.54.0 From 99294900a00b44e02cfb402bdefee889c597db27 Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Thu, 12 Nov 2026 02:47:38 +0100 Subject: [PATCH 3/6] =?UTF-8?q?Prompt=20d'un=20coup=20:=20le=20classement?= =?UTF-8?q?=20de=20l'analyse,=20au=20niveau=20calcul=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le classement du conseil vient de l'analyse du coup joué (conseils en version 2) : mêmes équités que la feuille du maître et l'analyse. Le titre dit le niveau du meilleur coup ; une ligne plus basse le dit aussi. Co-Authored-By: Claude Opus 5.5 --- app/src/domain/analysis/analyse.ts | 7 +- app/src/domain/analysis/numbers.ts | 37 ++++++-- app/src/domain/game/advice.ts | 10 +- app/src/domain/game/engine.ts | 17 ++-- app/src/domain/prompts/prompts.ts | 23 ++++- app/src/engine/wasm-engine.ts | 10 +- app/src/ui/advice-texts.ts | 4 +- app/src/ui/advice-view.ts | 4 +- app/tests/engine-analysis.test.ts | 29 ++++++ app/tests/game.test.ts | 1 - app/tests/prompts.test.ts | 92 ++++++++++++++++++- ...007-architecture-et-conventions-de-code.md | 18 ++++ docs/prompts/README.md | 2 +- docs/prompts/coup.md | 10 +- 14 files changed, 216 insertions(+), 48 deletions(-) diff --git a/app/src/domain/analysis/analyse.ts b/app/src/domain/analysis/analyse.ts index 665249b..3f563b8 100644 --- a/app/src/domain/analysis/analyse.ts +++ b/app/src/domain/analysis/analyse.ts @@ -102,10 +102,9 @@ export async function analyseDecision( if (analysis === null) { return null; } - const ranking = - options.ranking === false - ? [] - : await engine.rankMoves(before.position, cube, before.roll, ply); + // Le classement est celui de l'analyse même : mêmes coups, mêmes + // niveaux et mêmes équités que le coup joué et le meilleur coup. + const ranking = options.ranking === false ? [] : analysis.ranking; const start = await engine.board(before.position, cube); const facts = async (move: EvaluatedMove) => factsOf(start.onRoll, start.opponent, await engine.board(move.position, cube)); diff --git a/app/src/domain/analysis/numbers.ts b/app/src/domain/analysis/numbers.ts index 8c29335..3798562 100644 --- a/app/src/domain/analysis/numbers.ts +++ b/app/src/domain/analysis/numbers.ts @@ -7,6 +7,7 @@ // Licence : GPLv3 ou ultérieure (voir LICENSE). import type { AdvisedMove, MoveAdvice } from "../game/advice.js"; +import type { Ply } from "../game/engine.js"; /** Un nombre à la française, signé, à trois décimales : « −0,123 ». */ export function formatEquity(value: number): string { @@ -34,18 +35,21 @@ export interface RankingRow { /** * Le classement d'un conseil tel que l'affiche la feuille du maître : les - * coups du classement du moteur, chacun avec son écart par rapport au - * premier ; puis le coup joué, s'il n'y figure pas, avec l'écart de - * l'analyse du coup joué. + * coups du classement de l'analyse, chacun avec son écart par rapport au + * premier ; puis le coup joué, s'il n'y figure pas. Le coup joué a toujours + * l'écart de l'analyse (equityLoss), celui que dit le maître. */ export function rankingRows(advice: MoveAdvice): RankingRow[] { const first = advice.ranking[0]?.equity ?? advice.best.equity; - const rows = advice.ranking.map((move, i) => ({ - move, - rank: i + 1, - loss: move.equity - first, - played: move.notation === advice.played.notation, - })); + const rows = advice.ranking.map((move, i) => { + const played = move.notation === advice.played.notation; + return { + move, + rank: i + 1, + loss: played ? advice.equityLoss : move.equity - first, + played, + }; + }); if (!rows.some((row) => row.played)) { rows.push({ move: advice.played, @@ -56,3 +60,18 @@ export function rankingRows(advice: MoveAdvice): RankingRow[] { } return rows; } + +/** + * Le niveau du classement d'un conseil : celui auquel le moteur a évalué le + * meilleur coup. C'est le niveau demandé (advice.ply), sauf quand gnubg + * s'arrête avant, parce qu'un seul coup a passé ses filtres : tout le + * classement reste alors au niveau inférieur. + */ +export function rankingPly(advice: MoveAdvice): Ply { + return advice.best.ply; +} + +/** « 1 pli », « 2 plis » : gnubg compte les plis à partir de 0. */ +export function pliesText(ply: Ply): string { + return `${ply} ${ply > 1 ? "plis" : "pli"}`; +} diff --git a/app/src/domain/game/advice.ts b/app/src/domain/game/advice.ts index c9a876c..8eb96d2 100644 --- a/app/src/domain/game/advice.ts +++ b/app/src/domain/game/advice.ts @@ -15,8 +15,10 @@ import type { HalfMove, PositionId } from "./position.js"; import type { Roll } from "./roll.js"; /** Version du format d'un conseil enregistré. Un conseil d'une autre - * version est ignoré à la reprise (il peut être recalculé). */ -export const ADVICE_VERSION = 1; + * version est ignoré à la reprise (il peut être recalculé). Version 2 : le + * classement des coups vient de l'analyse du coup joué, et non plus d'un + * second classement comme la commande « hint » de gnubg. */ +export const ADVICE_VERSION = 2; /** * Le conseil du maître sur une décision du joueur : un coup ou une décision @@ -66,8 +68,8 @@ export interface MoveAdvice { readonly best: AdvisedMove; readonly playedFacts: MoveFacts; readonly bestFacts: MoveFacts; - /** Les meilleurs coups du classement du moteur, du meilleur au moins - * bon (cinq au plus). */ + /** Les meilleurs coups du classement de l'analyse, du meilleur au + * moins bon (cinq au plus), chacun au niveau où le moteur l'a évalué. */ readonly ranking: readonly AdvisedMove[]; /** Équité du coup joué moins celle du meilleur : 0 ou négative. */ readonly equityLoss: number; diff --git a/app/src/domain/game/engine.ts b/app/src/domain/game/engine.ts index e8429e1..b667d17 100644 --- a/app/src/domain/game/engine.ts +++ b/app/src/domain/game/engine.ts @@ -80,17 +80,6 @@ export interface Engine { ply: Ply, ): Promise; - /** Les coups légaux d'un jet classés du meilleur au moins bon, au - * niveau ply, comme la commande « hint » de gnubg (les coups écartés - * par ses filtres gardent l'évaluation d'un niveau inférieur, voir leur - * ply). */ - rankMoves( - position: PositionId, - cube: EngineCube, - roll: Roll, - ply: Ply, - ): Promise; - /** L'analyse d'une décision de videau, position vue du joueur au trait * (celui qui double ou non ; take et pass sont la réponse de son * adversaire). */ @@ -222,6 +211,12 @@ export interface MoveAnalysis { /** Équité du coup joué moins celle du meilleur : 0 ou négative. */ readonly equityLoss: number; readonly skill: Skill; + /** Le classement de l'analyse, du meilleur au moins bon (dix coups au + * plus) : la liste de l'analyse de partie de gnubg, chaque coup au + * niveau où gnubg l'a évalué (ply). Les coups écartés par ses filtres + * gardent l'évaluation d'un niveau inférieur ; le meilleur coup et le + * coup joué y ont les évaluations de best et played. */ + readonly ranking: readonly EvaluatedMove[]; } /** Décision de videau : doubler ou non (joueur au trait), prendre ou passer diff --git a/app/src/domain/prompts/prompts.ts b/app/src/domain/prompts/prompts.ts index 15f82c6..881bca0 100644 --- a/app/src/domain/prompts/prompts.ts +++ b/app/src/domain/prompts/prompts.ts @@ -20,7 +20,13 @@ import { type ReviewedDecision, summarizeAnalysis, } from "../analysis/game-analysis.js"; -import { formatEquity, formatPercent, rankingRows } from "../analysis/numbers.js"; +import { + formatEquity, + formatPercent, + pliesText, + rankingPly, + rankingRows, +} from "../analysis/numbers.js"; import { diceWords, type MasterPhrases, masterWords } from "../analysis/phrases.js"; import { type Advice, @@ -83,7 +89,7 @@ const plural = (n: number, one: string, many: string) => `${n} ${n > 1 ? many : /** « analyse à 2 plis ». */ export function plyText(ply: Ply): string { - return `analyse à ${ply} ${ply > 1 ? "plis" : "pli"}`; + return `analyse à ${pliesText(ply)}`; } /** Nombre de décisions analysées à partir duquel le niveau du joueur se @@ -146,8 +152,14 @@ export function decisionPrompt( niveau_eval: plyText(data.advice.ply), appreciation: masterWords(phrases, data.advice).word, }; + // Le niveau d'un classement de coups : celui de son meilleur coup, que + // le moteur a pu évaluer moins profondément que demandé. return data.advice.kind === "move" - ? fillPrompt(templates.move, { ...common, ...moveValues(data.advice) }) + ? fillPrompt(templates.move, { + ...common, + niveau_eval: plyText(rankingPly(data.advice)), + ...moveValues(data.advice), + }) : fillPrompt(templates.cube, { ...common, ...cubeValues(data.advice) }); } @@ -163,7 +175,10 @@ function moveValues(advice: MoveAdvice): Record { // de « Voir les chiffres du moteur », avec les mêmes chiffres. const rows = rankingRows(advice).filter((row) => row.rank <= 3 || row.played); const ranking = rows.map((row) => { - const ply = row.move.ply === advice.ply ? "" : `, évalué à ${row.move.ply} pli`; + // Un coup écarté par les filtres du moteur garde l'évaluation d'un + // niveau inférieur à celui du classement : la ligne le dit. + const ply = + row.move.ply === rankingPly(advice) ? "" : `, évalué à ${pliesText(row.move.ply)}`; const mine = row.played ? " (mon coup)" : ""; return `${row.rank}. ${row.move.notation}${mine} : équité ${formatEquity(row.move.equity)}, écart ${formatEquity(row.loss)}${ply}`; }); diff --git a/app/src/engine/wasm-engine.ts b/app/src/engine/wasm-engine.ts index 58e73f1..7712aec 100644 --- a/app/src/engine/wasm-engine.ts +++ b/app/src/engine/wasm-engine.ts @@ -182,6 +182,7 @@ export function createWasmEngine(run: RunCommand): Engine { best: CliEvaluatedMove | null; equity_loss: number; skill: CliSkill; + ranking: CliEvaluatedMove[]; }; if (answer.rank === null || answer.played === null || answer.best === null) { return null; @@ -193,17 +194,10 @@ export function createWasmEngine(run: RunCommand): Engine { best: toEvaluatedMove(answer.best), equityLoss: answer.equity_loss, skill: toSkill(answer.skill), + ranking: answer.ranking.map(toEvaluatedMove), }; }, - async rankMoves(position: PositionId, cube: EngineCube, roll: Roll, ply: Ply) { - const args = ["--ply", String(ply), "moves", position, await matchId(cube)]; - const answer = (await run([...args, diceText(roll)])) as { - moves: CliEvaluatedMove[]; - }; - return answer.moves.map(toEvaluatedMove); - }, - async analyseCube( position: PositionId, cube: EngineCube, diff --git a/app/src/ui/advice-texts.ts b/app/src/ui/advice-texts.ts index cc309de..dac6fc0 100644 --- a/app/src/ui/advice-texts.ts +++ b/app/src/ui/advice-texts.ts @@ -26,8 +26,8 @@ export const ADVICE_TEXTS = { replay: "Rejouer le coup", numbers: "Voir les chiffres du moteur", you: "(vous)", - moveNote: (ply: number) => - `Écart d'équité à ${ply} plis, par rapport au meilleur coup. Un écart de 0,03 ou moins reste un bon coup.`, + moveNote: (plies: string) => + `Écart d'équité à ${plies}, par rapport au meilleur coup. Un écart de 0,03 ou moins reste un bon coup.`, cubeNote: (ply: number) => `Équités de celui qui double, pour un videau de 1, à ${ply} plis. Écart de votre décision : `, cubeRows: { diff --git a/app/src/ui/advice-view.ts b/app/src/ui/advice-view.ts index c546657..56656e9 100644 --- a/app/src/ui/advice-view.ts +++ b/app/src/ui/advice-view.ts @@ -7,7 +7,7 @@ // Copyright (C) 2026 les auteurs d'Ustagammon. // Licence : GPLv3 ou ultérieure (voir LICENSE). -import { rankingRows } from "../domain/analysis/numbers.js"; +import { pliesText, rankingPly, rankingRows } from "../domain/analysis/numbers.js"; import { type MasterPhrases, masterWords } from "../domain/analysis/phrases.js"; import { type Advice, @@ -304,7 +304,7 @@ export function createAdviceView(options: AdviceViewOptions): AdviceView { } element(details, "p", { className: "explain-note", - text: ADVICE_TEXTS.moveNote(advice.ply), + text: ADVICE_TEXTS.moveNote(pliesText(rankingPly(advice))), }); } else { // Les lignes de votre décision, en gras : doubler mène à l'une des diff --git a/app/tests/engine-analysis.test.ts b/app/tests/engine-analysis.test.ts index ddb1241..02f76f5 100644 --- a/app/tests/engine-analysis.test.ts +++ b/app/tests/engine-analysis.test.ts @@ -35,6 +35,35 @@ test("ouverture 3-1 : 24/23 13/10 est une erreur très mauvaise, 8/5 6/5 le meil Math.abs(analysis.equityLoss - (analysis.played.equity - analysis.best.equity)) < 1e-6, ); assert.ok(analysis.equityLoss < -SKILL_THRESHOLDS.veryBad); + // Le classement de l'analyse, comme gnubg de bureau + // (tests/reference/analysis/move-opening-31-error.json) : le meilleur + // coup et le coup joué, une erreur, réévalués à 2 plis ; les suivants + // restés à 1 pli, puis à 0, écartés par les filtres. + assert.deepEqual(analysis.ranking[0], analysis.best); + assert.deepEqual(analysis.ranking[1], analysis.played); + assert.deepEqual( + analysis.ranking.map((move) => move.ply), + [2, 2, 1, 1, 1, 0, 0, 0, 0, 0], + ); +}); + +test("ouverture 3-1 jouée 8/5 6/5 : un seul coup passe le filtre, tout reste à 1 pli", async () => { + const engine = await loadNodeEngine(); + const analysis = await engine.analyseMove( + START_POSITION, + centered, + [3, 1], + "4HPwATCwZ/ABMA", + 2, + ); + assert.ok(analysis !== null); + assert.equal(analysis.rank, 1); + assert.deepEqual(analysis.ranking[0], analysis.best); + assert.deepEqual(analysis.best, analysis.played); + assert.deepEqual( + analysis.ranking.slice(0, 5).map((move) => move.ply), + [1, 1, 1, 1, 1], + ); }); test("sans coup possible, il n'y a rien à analyser", async () => { diff --git a/app/tests/game.test.ts b/app/tests/game.test.ts index 77ede5f..901657f 100644 --- a/app/tests/game.test.ts +++ b/app/tests/game.test.ts @@ -73,7 +73,6 @@ function fakeEngine(overrides: Partial = {}): Engine { }; }, analyseMove: async () => null, - rankMoves: async () => [], analyseCube: async () => ({ analysed: false, equities: null, diff --git a/app/tests/prompts.test.ts b/app/tests/prompts.test.ts index 3583492..fe7b40c 100644 --- a/app/tests/prompts.test.ts +++ b/app/tests/prompts.test.ts @@ -239,7 +239,97 @@ test("prompt d'un coup : la position, les identifiants et le top 3 du moteur, m assert.ok(shown.includes(`${row.move.notation}|${formatEquity(row.loss)}`)); } assert.ok(prompt.includes(`1. ${advice.best.notation}`), prompt); - assert.ok(prompt.includes("analyse à 2 plis"), prompt); + assert.match(prompt, /\(analyse à [12] plis?\)/); + } +}); + +/** Une équité écrite (« −0,212 ») proche d'une équité de gnubg de bureau : + * à l'arrondi près, plus la tolérance des positions de référence (0,002 ; + * tests/reference/README.md), le moteur WebAssembly ne calculant pas au + * bit près comme gnubg de bureau (ADR 0005). */ +function assertNear(text: string, value: number, message: string): void { + const written = Number(text.replace("−", "-").replace("+", "").replace(",", ".")); + assert.ok(Math.abs(written - value) <= 0.0005 + 0.002, `${text} ≠ ${value}\n${message}`); +} + +/** Le niveau du titre du classement, et chaque ligne du classement : coup, + * équité, écart, niveau propre (null : celui du titre). */ +function rankingOfPrompt(prompt: string) { + const title = /^Classement de GNU Backgammon \(analyse à (\d) plis?\)/m.exec(prompt); + assert.ok(title, prompt); + const lines = [ + ...prompt.matchAll( + /^(\d+)\. (.+?)( \(mon coup\))? : équité (\S+), écart (\S+?)(?:, évalué à (\d) plis?)?$/gm, + ), + ].map((m) => ({ + rank: Number(m[1]), + notation: m[2], + mine: m[3] !== undefined, + equity: m[4], + loss: m[5], + ply: m[6] === undefined ? null : Number(m[6]), + })); + return { ply: Number(title[1]), lines }; +} + +test("prompt d'un coup : le niveau du titre est celui des lignes, les équités celles de l'analyse", async () => { + const engine = await loadNodeEngine(); + // gnubg de bureau, analyse de partie à 2 plis (tests/reference/analysis/). + const reference = (id: string) => + JSON.parse(readFileSync(new URL(`tests/reference/analysis/${id}.json`, root), "utf8")) + .plies["2"]; + for (const [dice, move, id] of [ + // Erreur : gnubg réévalue à 2 plis le meilleur coup et le coup joué ; + // les autres restent au niveau où ses filtres les ont laissés. + [[3, 1], "24/23 13/10", "move-opening-31-error"], + // Meilleur coup : un seul coup passe le filtre de 1 pli, gnubg + // s'arrête là, tout le classement est à 1 pli. + [[3, 1], "8/5 6/5", "move-opening-31-best"], + [[6, 5], "13/8 13/7", "move-opening-65-error"], + ] as const) { + const played = await playOpening(engine, dice, move); + const advice = await adviceOn(engine, played); + assert.ok(advice.kind === "move"); + played.game.recordAdvice(advice); + const position = await positionBefore(engine, played.game.journal, played.eventId); + const prompt = decisionPrompt(templates, phrases, { + journal: played.game.journal, + advice, + position, + }); + const { ply, lines } = rankingOfPrompt(prompt); + const expected = reference(id); + + // Le titre dit le niveau du meilleur coup, celui que gnubg de bureau + // donne ; une ligne sans niveau propre est à ce niveau, une ligne + // avec un niveau propre, à un niveau inférieur. + assert.equal(ply, expected.ranking[0].ply, prompt); + assert.equal(ply, advice.best.ply); + assert.ok(lines.length >= 3, prompt); + for (const line of lines) { + const reference = expected.ranking.find( + (m: { move: string }) => m.move === line.notation, + ); + assert.ok(reference, `${line.notation} absent du classement de gnubg`); + assert.equal(line.ply ?? ply, reference.ply, `${line.notation}\n${prompt}`); + assert.ok(line.ply === null || line.ply < ply, prompt); + assertNear(line.equity, reference.equity, `${line.notation}\n${prompt}`); + } + + // Les équités et l'écart du meilleur coup et du vôtre sont ceux de + // l'analyse, que disent la feuille du maître et l'écran d'analyse. + const best = lines[0]; + const mine = lines.find((line) => line.mine); + assert.ok(best && mine, prompt); + assert.equal(best.notation, advice.best.notation); + assert.equal(best.equity, formatEquity(advice.best.equity)); + assert.equal(best.loss, formatEquity(0)); + assert.equal(mine.notation, advice.played.notation); + assert.equal(mine.rank, advice.rank); + assert.equal(mine.equity, formatEquity(advice.played.equity)); + assert.equal(mine.loss, formatEquity(advice.equityLoss)); + assertNear(mine.loss, expected.equity_loss, prompt); + assert.ok(shownRanking(advice).includes(`${mine.notation}|${mine.loss}`)); } }); diff --git a/docs/adr/0007-architecture-et-conventions-de-code.md b/docs/adr/0007-architecture-et-conventions-de-code.md index f16099b..1d7fe97 100644 --- a/docs/adr/0007-architecture-et-conventions-de-code.md +++ b/docs/adr/0007-architecture-et-conventions-de-code.md @@ -184,3 +184,21 @@ a plus besoin (`app/src/ui/review-runner.ts`). La bibliothèque gagne `saveAnalyses`, qui enregistre le journal analysé d'une partie sans en faire la partie en cours ni changer la date de sa dernière action (l'ordre de « Mes parties »). + +### 2026-11-11 — Version 2 des conseils : le classement de l'analyse + +Le classement des coups d'un conseil (`ranking`) venait d'un second appel au +moteur, comme la commande « hint » de gnubg, alors que le coup joué et le +meilleur coup venaient de l'analyse de partie. Les deux listes diffèrent : +quand le coup joué est une erreur, l'analyse réévalue le meilleur coup et le +coup joué au niveau demandé, ce que « hint » ne fait pas quand un seul coup +passe ses filtres (ouverture 3-1 jouée 24/23 13/10 : 8/5 6/5 à +0,200 à +2 plis dans l'analyse, +0,204 à 1 pli dans « hint »). Le classement vient +désormais de l'analyse même (`engine_analyse_move`, champ `ranking`), comme +le panneau d'analyse de gnubg de bureau. + +- `ADVICE_VERSION` passe à 2 (`app/src/domain/game/advice.ts`). Selon la + règle de l'amendement du 2026-11-10, un conseil de la version 1 est oublié + à la reprise et recalculé en arrière-plan ; les analyses de l'ordinateur + (`review.computer`), du même format, aussi. +- Ni les événements (version 2) ni le schéma IndexedDB ne changent. diff --git a/docs/prompts/README.md b/docs/prompts/README.md index 1758b5b..4a2a905 100644 --- a/docs/prompts/README.md +++ b/docs/prompts/README.md @@ -54,5 +54,5 @@ Repères communs à plusieurs gabarits : | `{diagramme}` | le plateau en texte : vos pions X, ceux de l'ordinateur O, dans votre numérotation | | `{score}` | « partie libre » ou le score du match | | `{videau}` | sa valeur et qui le possède | -| `{niveau_eval}` | le niveau d'évaluation de GNU Backgammon, « analyse à 2 plis » | +| `{niveau_eval}` | le niveau d'évaluation de GNU Backgammon, « analyse à 2 plis » ; pour un classement de coups, celui auquel le moteur a évalué le meilleur coup (voir [coup.md](coup.md)) | | `{langue}` | la langue de la réponse : « français » | diff --git a/docs/prompts/coup.md b/docs/prompts/coup.md index 47fc1a5..d54b028 100644 --- a/docs/prompts/coup.md +++ b/docs/prompts/coup.md @@ -12,7 +12,15 @@ Repères propres à ce gabarit : | `{coup_joue}` | votre coup, dans la notation de GNU Backgammon | | `{appreciation}` | le mot du maître : « Bon coup », « Imprécision », « Erreur » ou « Grosse erreur » | | `{rang}` | « le meilleur coup » ou « le 4e choix sur 15 coups possibles » | -| `{classement}` | le classement du moteur : les trois meilleurs coups, et le vôtre s'il n'en est pas, avec leur équité et leur écart au meilleur, comme dans « Voir les chiffres du moteur » | +| `{classement}` | le classement de l'analyse de votre coup : les trois meilleurs coups, et le vôtre s'il n'en est pas, avec leur équité et leur écart au meilleur, comme dans « Voir les chiffres du moteur » | + +Le niveau du titre (`{niveau_eval}`) est celui auquel GNU Backgammon a +évalué le meilleur coup. C'est d'ordinaire le niveau de l'analyse (2 plis), +mais le moteur s'arrête plus tôt quand un seul coup passe ses filtres : à +l'ouverture 3-1 jouée 8/5 6/5, tout le classement est à 1 pli. Les coups que +ses filtres ont écartés gardent l'évaluation d'un niveau inférieur, et leur +ligne le dit (« évalué à 1 pli »). Les équités et l'écart de votre coup sont +ceux de l'analyse, que disent la feuille du maître et l'écran d'analyse. ## Gabarit -- 2.54.0 From cb42052c8014a4d3c9f46cd6884bc69001820e3e Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Thu, 12 Nov 2026 02:47:38 +0100 Subject: [PATCH 4/6] Test navigateur : attendre l'enregistrement de la nouvelle partie MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sous Firefox, sur disque lent, l'enregistrement de la nouvelle partie attend dans la file derrière l'analyse de la partie finie ; le test lisait IndexedDB trop tôt. Il attend désormais que la partie en cours change. Test unitaire : rien n'est perdu avec un stockage lent. Co-Authored-By: Claude Opus 5.5 --- app/tests/storage.test.ts | 51 +++++++++++++++++++++++++++++++++++++++ tests/browser/play.mjs | 50 +++++++++++++++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/app/tests/storage.test.ts b/app/tests/storage.test.ts index a6bc7ec..1f4ed8d 100644 --- a/app/tests/storage.test.ts +++ b/app/tests/storage.test.ts @@ -222,6 +222,57 @@ test("les identifiants de partie sont distincts", () => { assert.match(newGameId(), /^[0-9a-z]+-[0-9a-z]{6}$/); }); +// Régression du test navigateur « mes parties » sous Firefox (disque lent) : +// la nouvelle partie n'est pas perdue quand son enregistrement attend +// derrière ceux, lents, de l'analyse de la partie finie ; seule une lecture +// directe du stockage, hors de la file, peut la précéder. +test("stockage lent : la nouvelle partie est enregistrée après l'analyse de l'ancienne", async () => { + const storage = memoryStorage(); + const raw = createKeyValueBackend(storage); + const pause = () => new Promise((resolve) => setTimeout(resolve, 30)); + const slow = { + ...raw, + write: async (...args: Parameters) => { + await pause(); + await raw.write(...args); + }, + setCurrentId: async (id: string) => { + await pause(); + await raw.setCurrentId(id); + }, + }; + const games = createGameLibrary(slow, { now: clock() }); + const engine = {} as Engine; + const session = createGameSession(games, { + start: () => Game.start(engine, { dice: fixedDice([5, 2]) }), + resume: (j) => Game.resume(engine, j), + }); + const finished = session.newGame(); + const finishedId = session.shownId as string; + session.save(finished.journal); + assert.equal((await games.current())?.summary.id, finishedId); + + // L'analyse de la partie finie s'enregistre en arrière-plan, puis + // « Rejouer » : nouvelle partie, enregistrée sans attendre. + for (let i = 0; i < 5; i++) { + void games.saveAnalyses(finishedId, finished.journal, { level: "2ply" }); + } + const next = session.newGame(); + session.save(next.journal); + const nextId = session.shownId as string; + assert.notEqual(nextId, finishedId); + + // Lue directement, tout de suite : encore l'ancienne partie. + assert.equal(await raw.currentId(), finishedId); + // Lue par la bibliothèque, après les écritures en attente : la nouvelle, + // avec son journal ; l'ancienne reste là. + const current = await games.current(); + assert.equal(current?.summary.id, nextId); + assert.deepEqual(current?.journal, next.journal); + assert.equal(await raw.currentId(), nextId); + assert.deepEqual((await games.load(finishedId))?.journal, finished.journal); +}); + test("session : « Reprendre l'ancienne » par l'identifiant, et reprise d'une branche", async () => { const games = library(); // Game.start et Game.resume n'interrogent pas le moteur. diff --git a/tests/browser/play.mjs b/tests/browser/play.mjs index 2b47443..be3b5dd 100644 --- a/tests/browser/play.mjs +++ b/tests/browser/play.mjs @@ -164,6 +164,45 @@ async function readJournal() { ); } +/** L'identifiant de la partie en cours enregistrée dans IndexedDB + * (magasin settings, clé « current »), ou null. */ +async function savedGameId() { + return page.evaluate( + () => + new Promise((resolve, reject) => { + const open = indexedDB.open("ustagammon"); + open.onerror = () => reject(open.error); + open.onsuccess = () => { + const db = open.result; + const current = db.transaction("settings").objectStore("settings").get("current"); + current.onsuccess = () => { + db.close(); + resolve(current.result?.value ?? null); + }; + }; + }), + ); +} + +/** + * Attend que la partie en cours enregistrée ne soit plus oldId, et renvoie + * le temps d'attente en ms (null au bout de 30 s). L'application enregistre + * sans attendre l'écriture, dans une file (app/src/storage/game-library.ts) : + * sous Firefox, sur un disque lent et chargé, l'enregistrement de la + * nouvelle partie peut attendre derrière ceux de l'analyse de la partie + * finie, et une lecture directe d'IndexedDB trouver encore l'ancienne. + */ +async function waitForSavedGameOtherThan(oldId) { + const start = Date.now(); + while (Date.now() - start < 30_000) { + if ((await savedGameId()) !== oldId) { + return Date.now() - start; + } + await sleep(100); + } + return null; +} + /** Le nombre de parties enregistrées dans IndexedDB. */ async function savedGameCount() { await savedJournal(); // les enregistrements en cours terminés @@ -1026,6 +1065,7 @@ async function rematch() { * immédiat, bandeau 10 s. Renvoie le journal de la partie quittée. */ async function newGameFromMenu() { const before = await savedJournal(); + const beforeId = await savedGameId(); const started = () => page.evaluate(() => JSON.parse(localStorage.getItem("ustagammon.settings") ?? "{}").gamesStarted); const startedBefore = await started(); @@ -1038,6 +1078,8 @@ async function newGameFromMenu() { `bandeau : « ${await page.textContent("#banner")} »`); check(!(await page.isVisible("#menu")), "le menu reste ouvert après « Nouvelle partie »"); await waitForPlayer(); + check((await waitForSavedGameOtherThan(beforeId)) !== null, + "« Nouvelle partie » : la partie n'a pas été enregistrée (30 s)"); const after = await savedJournal(); // (Les identifiants d'événements se suivent : une partie neuve peut avoir // la même tête que la précédente ; on compte les parties commencées.) @@ -1617,11 +1659,17 @@ step("mes parties"); // recharger la page. { const finished = await savedJournal(); + const finishedId = await savedGameId(); await rematch(); await waitForPlayer(); + // L'état observable, et non un délai : la nouvelle partie devient la + // partie en cours enregistrée. + const waited = await waitForSavedGameOtherThan(finishedId); + check(waited !== null, "la nouvelle partie n'a pas été enregistrée (30 s)"); + log(`nouvelle partie enregistrée ${waited} ms après la main au joueur`); const second = await savedJournal(); check(second.head !== finished.head || second.events.length !== finished.events.length, - "la nouvelle partie n'a pas été enregistrée"); + "la nouvelle partie enregistrée a le journal de la partie finie"); await openMenu(); await page.click("#menu-games"); await page.waitForSelector("#games-veil:not([hidden]) .games-item"); -- 2.54.0 From 282a3d688b2318d377e4fbb6260679e7b2e34a85 Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Thu, 12 Nov 2026 03:38:35 +0100 Subject: [PATCH 5/6] =?UTF-8?q?Test=20navigateur=20:=20zones=20du=20platea?= =?UTF-8?q?u=20relev=C3=A9es=20d'un=20coup,=20sans=20bo=C3=AEte=20nulle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit touchTarget mesurait chaque zone par boundingBox ; une zone remplacée entre-temps par un nouveau dessin du plateau n'avait plus de boîte (null, vu sous Firefox). Les zones sont relevées dans la page en une lecture, et le test attend qu'elles soient toutes là. Co-Authored-By: Claude Opus 5.5 --- tests/browser/play.mjs | 44 ++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/tests/browser/play.mjs b/tests/browser/play.mjs index be3b5dd..30e35be 100644 --- a/tests/browser/play.mjs +++ b/tests/browser/play.mjs @@ -626,17 +626,41 @@ async function checkTraces() { * (les dés en ont une de chaque côté du plateau), chacune un bouton * accessible d'au moins 44 px de côté. Renvoie le locator de la zone index. */ async function touchTarget(action, label, count = 1, index = 0) { - const zones = page.locator(`[data-action="${action}"]`); - check((await zones.count()) === count, - `zone ${action} : ${await zones.count()} sur le plateau, ${count} attendues`); - for (const zone of await zones.all()) { - check((await zone.getAttribute("role")) === (action === "menu" ? "link" : "button") && - (await zone.getAttribute("aria-label")) === label, `zone ${action} sans rôle accessible`); - const box = await zone.boundingBox(); - check(box.width >= 44 && box.height >= 44, - `zone ${action} trop petite : ${Math.round(box.width)} x ${Math.round(box.height)} px`); + // Les zones sont relevées d'un coup dans la page, où le plateau ne peut + // pas se redessiner entre deux lectures : mesurées une à une depuis + // Playwright (boundingBox), une zone remplacée entre-temps par un + // nouveau dessin du plateau n'avait plus de boîte (null, vu sous + // Firefox). On attend l'état observable : le nombre de zones attendu, + // toutes dessinées (5 s au plus). + const read = () => + page.evaluate( + (name) => + [...document.querySelectorAll(`[data-action="${name}"]`)].map((zone) => { + const box = zone.getBoundingClientRect(); + return { + role: zone.getAttribute("role"), + label: zone.getAttribute("aria-label"), + width: box.width, + height: box.height, + }; + }), + action, + ); + let zones = await read(); + const start = Date.now(); + while ((zones.length !== count || zones.some((z) => z.width === 0 && z.height === 0)) && + Date.now() - start < 5000) { + await sleep(100); + zones = await read(); } - return zones.nth(index); + check(zones.length === count, `zone ${action} : ${zones.length} sur le plateau, ${count} attendues`); + for (const zone of zones) { + check(zone.role === (action === "menu" ? "link" : "button") && zone.label === label, + `zone ${action} sans rôle accessible`); + check(zone.width >= 44 && zone.height >= 44, + `zone ${action} trop petite : ${Math.round(zone.width)} x ${Math.round(zone.height)} px`); + } + return page.locator(`[data-action="${action}"]`).nth(index); } /** Le nombre de dés montrés, et de dés estompés (déjà joués). */ -- 2.54.0 From ac0e924e4c98581facae2c5030a45130c6d73ca2 Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Thu, 12 Nov 2026 09:14:13 +0100 Subject: [PATCH 6/6] Test navigateur : fermer le bilan rouvert avant l'aide ; plan des corrections clos Co-Authored-By: Claude Opus 5.5 --- .../2026-11-11-corrections-plis-firefox.md | 49 ------------------- tests/browser/play.mjs | 6 +++ 2 files changed, 6 insertions(+), 49 deletions(-) delete mode 100644 docs/plans/2026-11-11-corrections-plis-firefox.md diff --git a/docs/plans/2026-11-11-corrections-plis-firefox.md b/docs/plans/2026-11-11-corrections-plis-firefox.md deleted file mode 100644 index e01af39..0000000 --- a/docs/plans/2026-11-11-corrections-plis-firefox.md +++ /dev/null @@ -1,49 +0,0 @@ -# Corrections : niveau des plis dans le prompt d'un coup, test Firefox instable - -- **Date** : 2026-11-11 -- **Étape** : 1f (suites) -- **Statut** : en cours -- **Exécutant** : sous-agent general-purpose - -## Objectif - -Le prompt d'un coup ne dit que ce que le moteur a calculé, au bon niveau, avec -les mêmes équités que l'application ; le test navigateur « mes parties » ne -dépend plus de la vitesse d'IndexedDB sous Firefox. - -## Consignes - -1. **Niveau d'évaluation dans le prompt d'un coup.** Le prompt - (`docs/prompts/coup.md`, `app/src/domain/prompts/`) dit en titre - « Classement de GNU Backgammon (analyse à 2 plis) », mais chaque ligne dit - « évalué à 1 pli » (ouverture 3-1 : « 8/5 6/5 : équité +0,204, écart 0,000, - évalué à 1 pli »). Trouver ce que vaut le champ `ply` des coups renvoyés par - le moteur (`engine.h`, `engine_best_moves`, commande `moves` d'engine-cli, - `rankMoves`) : décalage de convention, ou coups vraiment évalués à 1 pli, - auquel cas le classement n'est pas celui du conseil (`analyseMove`, 2 plis). - Vérifier que les équités du prompt sont celles de la feuille du maître et de - l'analyse (règle de 1f). Comparer au gnubg de bureau de référence. Corriger - la cause ; tester la cohérence du niveau du titre et des lignes, et - l'égalité des équités avec l'analyse. Si `engine.c` change (jamais - `engine/gnubg/`), recompiler une seule fois et ajouter une référence. -2. **Test instable sous Firefox.** `tests/browser/play.mjs` échoue parfois - sous Firefox, section « mes parties » (« la nouvelle partie n'a pas été - enregistrée », vers la ligne 1624) et passe à la relance. Déterminer s'il - s'agit d'un vrai défaut (sauvegarde perdue, course entre « nouvelle partie » - et l'enregistrement), à corriger dans l'app, ou d'un test qui lit trop tôt, - à corriger en attendant l'état observable plutôt qu'un délai. - -Limites : CSP stricte (aucun `.style.`), aucune nouvelle dépendance, `-j2` -au maximum, vérifier `docker ps` avant `make test-browser` (un autre agent -travaille en parallèle). - -## Critères de réussite - -- `make lint`, `make test`, `make test-browser` passent ; -- `SCRIPTS=play BROWSERS=firefox make test-browser` passe deux fois de suite ; -- un test vérifie la cohérence du niveau (titre et lignes) et l'égalité des - équités du prompt avec l'analyse. - -## Résultat - -Rempli à la clôture, dans la description de la merge request. diff --git a/tests/browser/play.mjs b/tests/browser/play.mjs index 30e35be..ea75380 100644 --- a/tests/browser/play.mjs +++ b/tests/browser/play.mjs @@ -1816,6 +1816,12 @@ if (offline === "roll") { await page.click("#roll"); await waitForPlayer(); } +// Si la partie reprise est finie, son bilan se rouvre au rechargement et +// couvre l'écran (c'est voulu) : on le ferme avant d'ouvrir l'aide. +if (await page.locator("#summary-veil").isVisible()) { + await page.click("#summary-close"); + await page.waitForSelector("#summary-veil", { state: "hidden" }); +} await page.click("#help-button"); await page.waitForSelector(".glossary dt", { state: "attached" }); await page.click("#help-back"); -- 2.54.0