{"id":189,"date":"2026-05-03T14:13:15","date_gmt":"2026-05-03T12:13:15","guid":{"rendered":"https:\/\/dzo.ovh\/?page_id=189"},"modified":"2026-05-04T00:29:25","modified_gmt":"2026-05-03T22:29:25","slug":"le-compte-est-bon","status":"publish","type":"page","link":"https:\/\/dzo.ovh\/?page_id=189","title":{"rendered":"Le Compte est bon"},"content":{"rendered":"\n<p>Jouer et r\u00e9soudre<\/p>\n\n\n\n<p>Ecrit au d\u00e9part en ABAP sous SAP, Migr\u00e9 \u00e0 l&#8217;aide de Claude en Python puis HTML5<\/p>\n\n\n\n<p><\/p>\n\n\n\n<div class=\"wp-block-file\"><a id=\"wp-block-file--media-3dff224b-ec2e-4dfa-bde3-d87d5aff5840\" href=\"http:\/\/dzo.ovh\/wp-content\/uploads\/2026\/05\/compte_est_bon.html\">compte_est_bon<\/a><a href=\"http:\/\/dzo.ovh\/wp-content\/uploads\/2026\/05\/compte_est_bon.html\" class=\"wp-block-file__button wp-element-button\" download aria-describedby=\"wp-block-file--media-3dff224b-ec2e-4dfa-bde3-d87d5aff5840\">Download<\/a><\/div>\n\n\n\n<p>Test 2 sa m\u00e8re !<\/p>\n\n\n\n<div class=\"wp-block-file\"><a href=\"https:\/\/dzo.ovh\/wp-content\/uploads\/2026\/05\/compte_est_bon.html\">compte_est_bon.html<\/a><a href=\"https:\/\/dzo.ovh\/wp-content\/uploads\/2026\/05\/compte_est_bon.html\" class=\"wp-block-file__button wp-element-button\" download>T\u00e9l\u00e9charger<\/a><\/div>\n\n\n\n<p><\/p>\n\n\n\n<p><\/p>\n\n\n<p>&lt;!DOCTYPE html&gt;<\/p>\n<p>&lt;html lang=&#8221;fr&#8221;&gt;<br \/>\n&lt;head&gt;<br \/>\n&lt;meta charset=&#8221;UTF-8&#8243;&gt;<br \/>\n&lt;meta name=&#8221;viewport&#8221; content=&#8221;width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no&#8221;&gt;<br \/>\n&lt;title&gt;Le Compte Est Bon&lt;\/title&gt;<br \/>\n&lt;meta name=&#8221;theme-color&#8221; content=&#8221;#1e3a52&#8243;&gt;<br \/>\n&lt;meta name=&#8221;apple-mobile-web-app-capable&#8221; content=&#8221;yes&#8221;&gt;<br \/>\n&lt;meta name=&#8221;apple-mobile-web-app-status-bar-style&#8221; content=&#8221;black-translucent&#8221;&gt;<br \/>\n&lt;meta name=&#8221;apple-mobile-web-app-title&#8221; content=&#8221;CEB&#8221;&gt;<br \/>\n&lt;link rel=&#8221;manifest&#8221; id=&#8221;pwa-manifest&#8221;&gt;<br \/>\n&lt;style&gt;<br \/>\n:root {<br \/>\n  &#8211;bg:       #3a6186;<br \/>\n  &#8211;bg-card:  #4a7399;<br \/>\n  &#8211;bg-hdr:   #1e3a52;<br \/>\n  &#8211;bg-sub:   #2e5070;<br \/>\n  &#8211;accent:   #e05c6e;<br \/>\n  &#8211;accent2:  #f5a623;<br \/>\n  &#8211;green:    #2ecc71;<br \/>\n  &#8211;blue:     #5dade2;<br \/>\n  &#8211;grey:     #5a7a8a;<br \/>\n  &#8211;text:     #ffffff;<br \/>\n  &#8211;text-med: #c8dde8;<br \/>\n  &#8211;text-dim: #8ab0c4;<br \/>\n  &#8211;border:   #2e5070;<br \/>\n  &#8211;yellow:   #f5c518;<br \/>\n  &#8211;btn-num:  #1e3a52;<br \/>\n  &#8211;btn-res:  #2ecc71;<br \/>\n  &#8211;btn-used: #2e5070;<br \/>\n}<br \/>\n* { box-sizing: border-box; margin: 0; padding: 0; }<br \/>\nbutton, input, select { touch-action: manipulation; }<br \/>\nbody { font-family: &#8216;Segoe UI&#8217;, system-ui, sans-serif; background: var(&#8211;bg); color: var(&#8211;text); min-height: 100vh; }<\/p>\n<p>\/* HEADER *\/<br \/>\nheader { background: var(\u2013bg-hdr); display: flex; align-items: center; justify-content: space-between; padding: 10px 20px; }<br \/>\nheader h1 { font-size: 1.2rem; font-weight: 700; letter-spacing: 2px; }<br \/>\nheader .hdr-right { display: flex; gap: 8px; }<\/p>\n<p>\/* INFO BAR *\/<br \/>\n.info-bar { background: var(\u2013bg-sub); display: flex; align-items: center; gap: 20px; padding: 6px 20px; border-top: 1px solid var(\u2013border); border-bottom: 1px solid var(\u2013border); font-size: 0.85rem; }<br \/>\n.info-bar .timer { font-size: 1.1rem; font-weight: 700; color: var(\u2013accent2); min-width: 60px; }<br \/>\n.info-bar .score { margin-left: auto; font-weight: 700; }<\/p>\n<p>\/* MAIN *\/<br \/>\nmain { max-width: 680px; margin: 0 auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; }<\/p>\n<p>\/* CIBLE *\/<br \/>\n.cible-wrap { text-align: center; }<br \/>\n.cible-label { font-size: 0.85rem; color: var(\u2013text-med); }<br \/>\n.cible-val { font-size: 3rem; font-weight: 700; color: var(\u2013accent2); line-height: 1.1; }<\/p>\n<p>\/* PLAQUES *\/<br \/>\n.plq-section { }<br \/>\n.plq-title { font-size: 0.8rem; color: var(\u2013text-dim); margin-bottom: 6px; }<br \/>\n.plq-grid {<br \/>\ndisplay: grid;<br \/>\ngrid-template-columns: repeat(auto-fill, minmax(60px, 1fr));<br \/>\ngap: 8px;<br \/>\n}<br \/>\n.plq-btn-ghost {<br \/>\nbackground: transparent;<br \/>\nborder: 2px dashed var(\u2013border);<br \/>\nborder-radius: 8px;<br \/>\nheight: 52px;<br \/>\nopacity: .4;<br \/>\npointer-events: none;<br \/>\n}<br \/>\n.plq-btn {<br \/>\nfont-family: inherit; font-size: 1.1rem; font-weight: 700;<br \/>\nbackground: var(\u2013btn-num); color: var(\u2013text);<br \/>\nborder: none; border-radius: 8px;<br \/>\nheight: 52px; width: 100%; cursor: pointer;<br \/>\ntransition: transform .1s, background .15s;<br \/>\n}<br \/>\n.plq-btn:hover:not(:disabled) { background: #2a5070; transform: scale(1.04); }<br \/>\n.plq-btn.selected { outline: 3px solid var(\u2013yellow); }<br \/>\n.plq-btn.used { background: var(\u2013btn-used); color: var(\u2013text-dim); cursor: default; }<br \/>\n.plq-btn.result { background: var(\u2013btn-res); color: #1a1a2e; }<br \/>\n.plq-btn:disabled { opacity: .5; cursor: default; }<\/p>\n<p>\/* CALCUL EN COURS *\/<br \/>\n.cur-op { background: var(\u2013bg-card); border-radius: 8px; padding: 10px 16px; min-height: 44px; font-size: 1.1rem; font-weight: 700; color: var(\u2013accent2); text-align: center; }<\/p>\n<p>\/* OPERATEURS *\/<br \/>\n.op-row { display: flex; justify-content: center; gap: 10px; }<br \/>\n.op-btn {<br \/>\nfont-family: inherit; font-size: 1.2rem; font-weight: 700;<br \/>\nbackground: var(\u2013accent); color: #fff;<br \/>\nborder: none; border-radius: 8px;<br \/>\nwidth: 52px; height: 44px; cursor: pointer;<br \/>\ntransition: transform .1s;<br \/>\n}<br \/>\n.op-btn:hover { transform: scale(1.06); }<br \/>\n.op-btn.active { outline: 3px solid var(\u2013yellow); }<\/p>\n<p>\/* HISTORIQUE *\/<br \/>\n.hist { background: var(\u2013bg-card); border-radius: 8px; padding: 10px 16px; }<br \/>\n.hist-title { font-size: 0.8rem; color: var(\u2013text-med); margin-bottom: 4px; }<br \/>\n.hist-line { font-family: \u2018Courier New\u2019, monospace; font-size: 1.15rem; font-weight: 700; color: var(\u2013accent2); min-height: 24px; padding: 2px 0; }<\/p>\n<p>\/* ACTIONS *\/<br \/>\n.actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }<br \/>\n.act-btn {<br \/>\nfont-family: inherit; font-size: 0.9rem; font-weight: 700;<br \/>\nborder: none; border-radius: 8px; padding: 10px 18px; cursor: pointer;<br \/>\ntransition: transform .1s, opacity .15s;<br \/>\n}<br \/>\n.act-btn:hover { transform: scale(1.03); }<br \/>\n.act-btn.green { background: var(\u2013green); color: #1a1a2e; }<br \/>\n.act-btn.blue  { background: var(\u2013blue);  color: #1a1a2e; }<br \/>\n.act-btn.red   { background: var(\u2013accent); color: #fff; }<br \/>\n.act-btn.grey  { background: var(\u2013grey);  color: #fff; }<br \/>\n.act-btn:disabled { opacity: .4; cursor: default; transform: none; }<\/p>\n<p>\/* SOLUTION AUTO *\/<br \/>\n.sol-box { background: var(\u2013bg-card); border-radius: 8px; padding: 10px 16px; }<br \/>\n.sol-title { font-size: 0.8rem; color: var(\u2013text-med); margin-bottom: 4px; }<br \/>\n.sol-line { font-family: \u2018Courier New\u2019, monospace; font-size: 0.9rem; color: var(\u2013text-med); min-height: 18px; }<br \/>\n.sol-line.exact { color: var(\u2013green); }<\/p>\n<p>\/* STATUS *\/<br \/>\n.status { text-align: center; font-size: 0.85rem; color: var(\u2013text-dim); padding: 4px 0; min-height: 20px; }<\/p>\n<p>\/* MODAL *\/<br \/>\n.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.55); z-index: 100; align-items: center; justify-content: center; }<br \/>\n.modal-overlay.open { display: flex; }<br \/>\n.modal { background: var(\u2013bg-card); border-radius: 12px; padding: 24px; min-width: 300px; max-width: 480px; width: 90%; }<br \/>\n.modal h2 { font-size: 1rem; margin-bottom: 16px; color: var(\u2013accent2); }<br \/>\n.modal .row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; font-size: 0.9rem; }<br \/>\n.modal label { width: 180px; color: var(\u2013text-med); flex-shrink: 0; }<br \/>\n.modal input[type=number] { width: 80px; background: var(\u2013bg-hdr); color: var(\u2013text); border: 1px solid var(\u2013border); border-radius: 6px; padding: 4px 8px; font-size: 0.9rem; }<br \/>\n.modal select { background: var(\u2013bg-hdr); color: var(\u2013text); border: 1px solid var(\u2013border); border-radius: 6px; padding: 4px 8px; font-size: 0.9rem; }<br \/>\n.modal .modal-actions { display: flex; gap: 8px; margin-top: 18px; justify-content: flex-end; }<\/p>\n<p>\/* DETAIL MODAL *\/<br \/>\n.detail-tabs { display: flex; gap: 0; margin-bottom: 0; }<br \/>\n.tab-btn { background: var(\u2013bg-sub); color: var(\u2013text-med); border: none; padding: 8px 16px; cursor: pointer; font-size: 0.85rem; font-family: inherit; }<br \/>\n.tab-btn.active { background: var(\u2013bg-card); color: var(\u2013accent); font-weight: 700; }<br \/>\n.tab-content { display: none; }<br \/>\n.tab-content.active { display: block; }<br \/>\n.reach-grid { display: grid; grid-template-columns: repeat(10, 1fr); gap: 2px; max-height: 340px; overflow-y: auto; margin-top: 8px; }<br \/>\n.reach-cell { font-family: \u2018Courier New\u2019, monospace; font-size: 0.75rem; text-align: center; padding: 3px 2px; border-radius: 3px; background: var(\u2013bg-sub); color: var(\u2013text); }<br \/>\n.reach-cell.is-target { background: var(\u2013accent2); color: #1a1a2e; font-weight: 700; }<br \/>\n.reach-cell.is-firstgap { background: #2d5f80; color: var(\u2013blue); }<br \/>\n.sol-item { background: var(\u2013bg-sub); border-radius: 6px; padding: 8px 12px; margin-bottom: 6px; font-family: \u2018Courier New\u2019, monospace; font-size: 0.82rem; cursor: pointer; }<br \/>\n.sol-item:hover { background: #3a6a90; }<br \/>\n.sol-item .sol-preview { color: var(\u2013text-med); font-size: 0.78rem; }<br \/>\n.sol-steps { display: none; margin-top: 6px; }<br \/>\n.sol-steps.open { display: block; }<br \/>\n.sol-steps span { display: block; color: var(\u2013green); }<br \/>\n&lt;\/style&gt;<\/p>\n<p>&lt;\/head&gt;<br \/>\n&lt;body&gt;<\/p>\n<p>&lt;header&gt;<br \/>\n  &lt;h1&gt;LE COMPTE EST BON &lt;span style=&#8221;font-size:.6rem;font-weight:400;opacity:.7;letter-spacing:1px;&#8221;&gt;by J.Dimijian + Claude&lt;\/span&gt;&lt;\/h1&gt;<br \/>\n  &lt;div class=&#8221;hdr-right&#8221;&gt;<br \/>\n    &lt;button class=&#8221;act-btn grey&#8221; onclick=&#8221;openStats()&#8221; style=&#8221;padding:8px 12px;&#8221;&gt;\ud83d\udcca&lt;\/button&gt;<br \/>\n    &lt;button class=&#8221;act-btn grey&#8221; onclick=&#8221;openOptions()&#8221;&gt;Options&lt;\/button&gt;<br \/>\n  &lt;\/div&gt;<br \/>\n&lt;\/header&gt;<\/p>\n<p>&lt;div class=&#8221;info-bar&#8221;&gt;<br \/>\n  &lt;span id=&#8221;lbl-round&#8221; style=&#8221;color:var(&#8211;text-dim)&#8221;&gt;\u2014&lt;\/span&gt;<br \/>\n  &lt;span id=&#8221;lbl-mode&#8221; style=&#8221;color:var(&#8211;text-dim)&#8221;&gt;CEB24&lt;\/span&gt;<br \/>\n  &lt;span class=&#8221;timer&#8221; id=&#8221;lbl-timer&#8221;&gt;\u2014&lt;\/span&gt;<br \/>\n  &lt;span class=&#8221;score&#8221;&gt;Score : &lt;span id=&#8221;lbl-score&#8221;&gt;0&lt;\/span&gt;&lt;\/span&gt;<br \/>\n&lt;\/div&gt;<\/p>\n<p>&lt;main&gt;<br \/>\n  &lt;div class=&#8221;cible-wrap&#8221; style=&#8221;display:flex;align-items:center;gap:12px;&#8221;&gt;<br \/>\n    &lt;button class=&#8221;act-btn red&#8221; id=&#8221;btn-nouveau&#8221; onclick=&#8221;newGame()&#8221; style=&#8221;padding:10px 16px;flex-shrink:0;&#8221;&gt;Nouveau&lt;\/button&gt;<br \/>\n    &lt;div style=&#8221;flex:1;text-align:center;&#8221;&gt;<br \/>\n      &lt;div class=&#8221;cible-label&#8221;&gt;Cible&lt;\/div&gt;<br \/>\n      &lt;div class=&#8221;cible-val&#8221; id=&#8221;lbl-cible&#8221;&gt;\u2014&lt;\/div&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n  &lt;\/div&gt;<\/p>\n<p>  &lt;div class=&#8221;plq-section&#8221;&gt;<br \/>\n    &lt;div class=&#8221;plq-title&#8221; id=&#8221;plq-title&#8221;&gt;Plaques disponibles&lt;\/div&gt;<br \/>\n    &lt;div class=&#8221;plq-grid&#8221; id=&#8221;plq-grid&#8221;&gt;&lt;\/div&gt;<br \/>\n    &lt;!&#8211; Mode entra\u00eenement : saisie libre &#8211;&gt;<br \/>\n    &lt;div id=&#8221;entrainement-zone&#8221; style=&#8221;display:none; margin-top:8px;&#8221;&gt;<br \/>\n      &lt;div style=&#8221;font-size:0.8rem;color:var(&#8211;text-dim);margin-bottom:6px;&#8221;&gt;Saisis les plaques et la cible, puis clique sur Jouer&lt;\/div&gt;<br \/>\n      &lt;div style=&#8221;display:flex;flex-wrap:wrap;gap:6px;align-items:center;&#8221; id=&#8221;entry-plq-row&#8221;&gt;&lt;\/div&gt;<br \/>\n      &lt;div style=&#8221;display:flex;align-items:center;gap:10px;margin-top:8px;&#8221;&gt;<br \/>\n        &lt;span style=&#8221;font-size:0.85rem;color:var(&#8211;text-med);&#8221;&gt;Cible :&lt;\/span&gt;<br \/>\n        &lt;input type=&#8221;number&#8221; id=&#8221;entry-cible&#8221; min=&#8221;1&#8243; max=&#8221;9999&#8243;<br \/>\n               style=&#8221;width:80px;background:var(&#8211;bg-hdr);color:var(&#8211;accent2);border:1px solid var(&#8211;border);<br \/>\n                      border-radius:6px;padding:6px 8px;font-size:1.1rem;font-weight:700;text-align:center;&#8221;&gt;<br \/>\n        &lt;button class=&#8221;act-btn green&#8221; onclick=&#8221;jouerEntrainement()&#8221; style=&#8221;padding:8px 14px;&#8221;&gt;Jouer !&lt;\/button&gt;<br \/>\n      &lt;\/div&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n  &lt;\/div&gt;<\/p>\n<p>  &lt;div class=&#8221;cur-op&#8221; id=&#8221;cur-op&#8221;&gt;&lt;\/div&gt;<\/p>\n<p>  &lt;div class=&#8221;op-row&#8221; id=&#8221;op-row&#8221; style=&#8221;display:flex;align-items:center;&#8221;&gt;<br \/>\n    &lt;div style=&#8221;flex:1;display:flex;justify-content:center;gap:16px;&#8221;&gt;<br \/>\n      &lt;button class=&#8221;op-btn&#8221; onclick=&#8221;onOperator(&#8216;+&#8217;, this)&#8221;&gt;+&lt;\/button&gt;<br \/>\n      &lt;button class=&#8221;op-btn&#8221; onclick=&#8221;onOperator(&#8216;-&#8216;, this)&#8221;&gt;\u2212&lt;\/button&gt;<br \/>\n      &lt;button class=&#8221;op-btn&#8221; onclick=&#8221;onOperator(&#8216;X&#8217;, this)&#8221;&gt;\u00d7&lt;\/button&gt;<br \/>\n      &lt;button class=&#8221;op-btn&#8221; onclick=&#8221;onOperator(&#8216;\/&#8217;, this)&#8221;&gt;\u00f7&lt;\/button&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n    &lt;button class=&#8221;op-btn&#8221; onclick=&#8221;onEffacer()&#8221; style=&#8221;background:var(&#8211;grey);font-size:1rem;flex-shrink:0;&#8221;&gt;\u232b&lt;\/button&gt;<br \/>\n  &lt;\/div&gt;<\/p>\n<p>  &lt;div class=&#8221;hist&#8221;&gt;<br \/>\n    &lt;div class=&#8221;hist-title&#8221;&gt;Calculs :&lt;\/div&gt;<br \/>\n    &lt;div id=&#8221;hist-lines&#8221;&gt;&lt;\/div&gt;<br \/>\n  &lt;\/div&gt;<\/p>\n<p>  &lt;div class=&#8221;actions&#8221;&gt;<br \/>\n    &lt;button class=&#8221;act-btn green&#8221; onclick=&#8221;onValider()&#8221;&gt;Valider&lt;\/button&gt;<br \/>\n    &lt;button class=&#8221;act-btn blue&#8221;  onclick=&#8221;onResoudre()&#8221; id=&#8221;btn-resoudre&#8221; disabled&gt;R\u00e9soudre&lt;\/button&gt;<br \/>\n    &lt;button class=&#8221;act-btn grey&#8221;  onclick=&#8221;openDetail()&#8221; id=&#8221;btn-detail&#8221; disabled&gt;D\u00e9tail\u2026&lt;\/button&gt;<br \/>\n  &lt;\/div&gt;<\/p>\n<p>  &lt;div class=&#8221;sol-box&#8221; id=&#8221;sol-box&#8221; style=&#8221;display:none&#8221;&gt;<br \/>\n    &lt;div class=&#8221;sol-title&#8221;&gt;Meilleure solution :&lt;\/div&gt;<br \/>\n    &lt;div id=&#8221;sol-lines&#8221;&gt;&lt;\/div&gt;<br \/>\n  &lt;\/div&gt;<\/p>\n<p>  &lt;div class=&#8221;status&#8221; id=&#8221;lbl-status&#8221;&gt;Clique sur Nouveau pour commencer&lt;\/div&gt;<br \/>\n&lt;\/main&gt;<\/p>\n<p>&lt;!&#8211; OPTIONS MODAL &#8211;&gt;<\/p>\n<p>&lt;div class=&#8221;modal-overlay&#8221; id=&#8221;modal-options&#8221;&gt;<br \/>\n  &lt;div class=&#8221;modal&#8221;&gt;<br \/>\n    &lt;h2&gt;Options du jeu&lt;\/h2&gt;<br \/>\n    &lt;div style=&#8221;font-size:.8rem;color:var(&#8211;accent2);margin-bottom:6px;font-weight:700;&#8221;&gt;\u2014 Partie \u2014&lt;\/div&gt;<br \/>\n    &lt;div class=&#8221;row&#8221;&gt;<br \/>\n      &lt;label&gt;N\u00b0 partie \/ graine (0=al\u00e9atoire)&lt;\/label&gt;<br \/>\n      &lt;input type=&#8221;number&#8221; id=&#8221;opt-partie&#8221; value=&#8221;0&#8243; min=&#8221;0&#8243; style=&#8221;width:80px;&#8221;&gt;<br \/>\n      &lt;span style=&#8221;font-size:.75rem;color:var(&#8211;text-dim)&#8221;&gt;\u26a0 reset score&lt;\/span&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n    &lt;div class=&#8221;row&#8221;&gt;&lt;label&gt;Nombre de tirages par partie&lt;\/label&gt;&lt;input type=&#8221;number&#8221; id=&#8221;opt-nbtirages&#8221; min=&#8221;1&#8243; max=&#8221;999&#8243; value=&#8221;50&#8243; style=&#8221;width:80px;&#8221;&gt;&lt;\/div&gt;<br \/>\n    &lt;div style=&#8221;font-size:.8rem;color:var(&#8211;accent2);margin:8px 0 6px;font-weight:700;&#8221;&gt;\u2014 Tirage \u2014&lt;\/div&gt;<br \/>\n    &lt;div class=&#8221;row&#8221;&gt;&lt;label&gt;Mode de tirage&lt;\/label&gt;<br \/>\n      &lt;select id=&#8221;opt-tirage&#8221;&gt;<br \/>\n        &lt;option value=&#8221;ceb24&#8243;&gt;CEB 24 (classique)&lt;\/option&gt;<br \/>\n        &lt;option value=&#8221;ceb28&#8243;&gt;CEB 28 (grands \u00d72)&lt;\/option&gt;<br \/>\n        &lt;option value=&#8221;alea20&#8243;&gt;Al\u00e9atoire 20&lt;\/option&gt;<br \/>\n      &lt;\/select&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n    &lt;div class=&#8221;row&#8221;&gt;&lt;label&gt;Nombre de plaques&lt;\/label&gt;&lt;input type=&#8221;number&#8221; id=&#8221;opt-nbplq&#8221; min=&#8221;2&#8243; max=&#8221;8&#8243; value=&#8221;6&#8243;&gt;&lt;\/div&gt;<br \/>\n    &lt;div class=&#8221;row&#8221;&gt;&lt;label&gt;Cible minimum&lt;\/label&gt;&lt;input type=&#8221;number&#8221; id=&#8221;opt-tmin&#8221; value=&#8221;101&#8243;&gt;&lt;\/div&gt;<br \/>\n    &lt;div class=&#8221;row&#8221;&gt;&lt;label&gt;Cible maximum&lt;\/label&gt;&lt;input type=&#8221;number&#8221; id=&#8221;opt-tmax&#8221; value=&#8221;999&#8243;&gt;&lt;\/div&gt;<br \/>\n    &lt;div class=&#8221;row&#8221;&gt;&lt;label&gt;Cible forc\u00e9ment atteignable&lt;\/label&gt;<br \/>\n      &lt;input type=&#8221;checkbox&#8221; id=&#8221;opt-exact&#8221; checked style=&#8221;width:20px;height:20px;accent-color:var(&#8211;green);cursor:pointer;&#8221;&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n    &lt;div style=&#8221;font-size:.8rem;color:var(&#8211;accent2);margin:8px 0 6px;font-weight:700;&#8221;&gt;\u2014 Jeu \u2014&lt;\/div&gt;<br \/>\n    &lt;div class=&#8221;row&#8221;&gt;&lt;label&gt;Temps limite (s)&lt;\/label&gt;&lt;input type=&#8221;number&#8221; id=&#8221;opt-time&#8221; value=&#8221;45&#8243;&gt;&lt;\/div&gt;<br \/>\n    &lt;div class=&#8221;row&#8221;&gt;&lt;label&gt;Reprise auto r\u00e9sultat pr\u00e9c\u00e9dent&lt;\/label&gt;<br \/>\n      &lt;input type=&#8221;checkbox&#8221; id=&#8221;opt-reprise&#8221; style=&#8221;width:20px;height:20px;accent-color:var(&#8211;green);cursor:pointer;&#8221;&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n    &lt;div class=&#8221;row&#8221;&gt;&lt;label&gt;Mode entra\u00eenement (plaques + cible libres)&lt;\/label&gt;<br \/>\n      &lt;input type=&#8221;checkbox&#8221; id=&#8221;opt-entrainement&#8221; style=&#8221;width:20px;height:20px;accent-color:var(&#8211;green);cursor:pointer;&#8221;&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n    &lt;div style=&#8221;font-size:.8rem;color:var(&#8211;accent2);margin:8px 0 6px;font-weight:700;&#8221;&gt;\u2014 Solveur \u2014&lt;\/div&gt;<br \/>\n    &lt;div class=&#8221;row&#8221;&gt;&lt;label&gt;Mode r\u00e9solution&lt;\/label&gt;<br \/>\n      &lt;select id=&#8221;opt-solve&#8221;&gt;<br \/>\n        &lt;option value=&#8221;complete&#8221;&gt;Meilleure solution&lt;\/option&gt;<br \/>\n        &lt;option value=&#8221;stop&#8221;&gt;Premi\u00e8re exacte&lt;\/option&gt;<br \/>\n        &lt;option value=&#8221;all&#8221;&gt;Toutes (jusqu&#8217;\u00e0 r\u00e9cur 3)&lt;\/option&gt;<br \/>\n      &lt;\/select&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n    &lt;div class=&#8221;modal-actions&#8221;&gt;<br \/>\n      &lt;button class=&#8221;act-btn green&#8221; onclick=&#8221;saveOptions()&#8221;&gt;Appliquer&lt;\/button&gt;<br \/>\n      &lt;button class=&#8221;act-btn red&#8221;   onclick=&#8221;closeModal(&#8216;modal-options&#8217;)&#8221;&gt;Annuler&lt;\/button&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n  &lt;\/div&gt;<br \/>\n&lt;\/div&gt;<\/p>\n<p>&lt;!&#8211; DETAIL MODAL &#8211;&gt;<\/p>\n<p>&lt;div class=&#8221;modal-overlay&#8221; id=&#8221;modal-detail&#8221;&gt;<br \/>\n  &lt;div class=&#8221;modal&#8221; style=&#8221;max-width:640px; width:95%; max-height:90vh; overflow:hidden; display:flex; flex-direction:column;&#8221;&gt;<br \/>\n    &lt;h2 id=&#8221;detail-title&#8221; style=&#8221;flex-shrink:0;&#8221;&gt;D\u00e9tail&lt;\/h2&gt;<br \/>\n    &lt;div class=&#8221;detail-tabs&#8221; style=&#8221;flex-shrink:0;&#8221;&gt;<br \/>\n      &lt;button class=&#8221;tab-btn active&#8221; onclick=&#8221;showTab(&#8216;tab-reach&#8217;)&#8221;&gt;R\u00e9sultats&lt;\/button&gt;<br \/>\n      &lt;button class=&#8221;tab-btn&#8221;        onclick=&#8221;showTab(&#8216;tab-sols&#8217;)&#8221;&gt;Solutions exactes&lt;\/button&gt;<br \/>\n      &lt;button class=&#8221;tab-btn&#8221;        onclick=&#8221;showTab(&#8216;tab-best&#8217;)&#8221;&gt;Meilleure&lt;\/button&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n    &lt;div style=&#8221;flex:1; overflow-y:auto; min-height:0;&#8221;&gt;<br \/>\n    &lt;div id=&#8221;tab-reach&#8221; class=&#8221;tab-content active&#8221;&gt;<br \/>\n      &lt;div id=&#8221;reach-info&#8221; style=&#8221;font-size:.85rem;color:var(&#8211;text-med);padding:6px 0&#8243;&gt;&lt;\/div&gt;<br \/>\n      &lt;div id=&#8221;reach-canvas-wrap&#8221; style=&#8221;overflow-y:auto;max-height:340px;margin-top:4px;&#8221;&gt;&lt;\/div&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n    &lt;div id=&#8221;tab-sols&#8221; class=&#8221;tab-content&#8221;&gt;<br \/>\n      &lt;div id=&#8221;sols-list&#8221; style=&#8221;overflow-y:auto;max-height:420px;&#8221;&gt;&lt;\/div&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n    &lt;div id=&#8221;tab-best&#8221; class=&#8221;tab-content&#8221;&gt;<br \/>\n      &lt;div id=&#8221;best-content&#8221; style=&#8221;padding:12px 0; font-family:&#8217;Courier New&#8217;,monospace; font-size:1rem;&#8221;&gt;&lt;\/div&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n    &lt;div style=&#8221;flex-shrink:0; border-top:1px solid var(&#8211;border); padding-top:10px; margin-top:8px; text-align:right;&#8221;&gt;<br \/>\n      &lt;button class=&#8221;act-btn red&#8221; onclick=&#8221;closeModal(&#8216;modal-detail&#8217;)&#8221;&gt;Fermer&lt;\/button&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n  &lt;\/div&gt;<br \/>\n&lt;\/div&gt;<\/p>\n<p>&lt;!&#8211; FIN DE PARTIE MODAL &#8211;&gt;<\/p>\n<p>&lt;div class=&#8221;modal-overlay&#8221; id=&#8221;modal-fin&#8221;&gt;<br \/>\n  &lt;div class=&#8221;modal&#8221; style=&#8221;max-width:480px;width:95%;text-align:center;&#8221;&gt;<br \/>\n    &lt;h2 style=&#8221;font-size:1.4rem;margin-bottom:4px;&#8221;&gt;\ud83c\udfc1 Fin de partie !&lt;\/h2&gt;<br \/>\n    &lt;div id=&#8221;fin-graine&#8221; style=&#8221;font-size:.8rem;color:var(&#8211;text-dim);margin-bottom:16px;&#8221;&gt;&lt;\/div&gt;<br \/>\n    &lt;div id=&#8221;fin-score&#8221; style=&#8221;font-size:2.5rem;font-weight:700;color:var(&#8211;accent2);margin-bottom:4px;&#8221;&gt;&lt;\/div&gt;<br \/>\n    &lt;div id=&#8221;fin-moyenne&#8221; style=&#8221;font-size:.9rem;color:var(&#8211;text-med);margin-bottom:16px;&#8221;&gt;&lt;\/div&gt;<br \/>\n    &lt;div id=&#8221;fin-hist&#8221; style=&#8221;text-align:left;max-height:260px;overflow-y:auto;margin-bottom:16px;&#8221;&gt;&lt;\/div&gt;<br \/>\n    &lt;div class=&#8221;modal-actions&#8221; style=&#8221;justify-content:center;gap:12px;&#8221;&gt;<br \/>\n      &lt;button class=&#8221;act-btn green&#8221; onclick=&#8221;nouvellePartie()&#8221;&gt;Nouvelle partie&lt;\/button&gt;<br \/>\n      &lt;button class=&#8221;act-btn grey&#8221;  onclick=&#8221;closeModal(&#8216;modal-fin&#8217;)&#8221;&gt;Fermer&lt;\/button&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n  &lt;\/div&gt;<br \/>\n&lt;\/div&gt;<\/p>\n<p>&lt;!&#8211; STATS MODAL &#8211;&gt;<\/p>\n<p>&lt;div class=&#8221;modal-overlay&#8221; id=&#8221;modal-stats&#8221;&gt;<br \/>\n  &lt;div class=&#8221;modal&#8221; style=&#8221;max-width:580px;width:95%;max-height:90vh;overflow:hidden;display:flex;flex-direction:column;&#8221;&gt;<br \/>\n    &lt;h2 style=&#8221;flex-shrink:0;&#8221;&gt;\ud83d\udcca Statistiques&lt;\/h2&gt;<br \/>\n    &lt;div class=&#8221;detail-tabs&#8221; style=&#8221;flex-shrink:0;&#8221;&gt;<br \/>\n      &lt;button class=&#8221;tab-btn active&#8221; onclick=&#8221;showStatsTab(&#8216;st-global&#8217;)&#8221;&gt;Globales&lt;\/button&gt;<br \/>\n      &lt;button class=&#8221;tab-btn&#8221;        onclick=&#8221;showStatsTab(&#8216;st-parties&#8217;)&#8221;&gt;Parties&lt;\/button&gt;<br \/>\n      &lt;button class=&#8221;tab-btn&#8221;        onclick=&#8221;showStatsTab(&#8216;st-modes&#8217;)&#8221;&gt;Modes&lt;\/button&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n    &lt;div style=&#8221;flex:1;overflow-y:auto;min-height:0;padding:8px 0;&#8221;&gt;<\/p>\n<p>&#8220;`<br \/>\n  &lt;!&#8211; Globales &#8211;&gt;<br \/>\n  &lt;div id=&#8221;st-global&#8221; class=&#8221;tab-content active&#8221;&gt;&lt;\/div&gt;<\/p>\n<p>  &lt;!&#8211; Parties &#8211;&gt;<br \/>\n  &lt;div id=&#8221;st-parties&#8221; class=&#8221;tab-content&#8221;&gt;<br \/>\n    &lt;div id=&#8221;st-parties-list&#8221; style=&#8221;font-size:.82rem;&#8221;&gt;&lt;\/div&gt;<br \/>\n  &lt;\/div&gt;<\/p>\n<p>  &lt;!&#8211; Modes &#8211;&gt;<br \/>\n  &lt;div id=&#8221;st-modes&#8221; class=&#8221;tab-content&#8221;&gt;&lt;\/div&gt;<\/p>\n<p>&lt;\/div&gt;<br \/>\n&lt;div style=&#8221;flex-shrink:0;border-top:1px solid var(&#8211;border);padding-top:10px;margin-top:8px;display:flex;justify-content:space-between;&#8221;&gt;<br \/>\n  &lt;button class=&#8221;act-btn red&#8221; onclick=&#8221;resetStats()&#8221; style=&#8221;font-size:.8rem;padding:8px 12px;&#8221;&gt;\ud83d\uddd1 Reset stats&lt;\/button&gt;<br \/>\n  &lt;button class=&#8221;act-btn grey&#8221; onclick=&#8221;closeModal(&#8216;modal-stats&#8217;)&#8221;&gt;Fermer&lt;\/button&gt;<br \/>\n&lt;\/div&gt;<br \/>\n&#8220;`<\/p>\n<p>  &lt;\/div&gt;<br \/>\n&lt;\/div&gt;<\/p>\n<p>&lt;script&gt;<br \/>\n&#8220;use strict&#8221;;<\/p>\n<p>\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\n\/\/  HP11C RANDOM \u2014 BigInt pour \u00e9viter toute perte d&#8217;arrondi<br \/>\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\nconst KINCR  = 1017980433n;<br \/>\nconst KBASE  = 1574352261n;<br \/>\nconst K10E10 = 10000000000n;<\/p>\n<p>class HP11CRandom {<br \/>\n  constructor(partieNum = 0) {<br \/>\n    this._next = partieNum &gt; 0<br \/>\n      ? BigInt(partieNum) * 10000n<br \/>\n      : BigInt(Math.floor(Math.random() * 99999999));<br \/>\n  }<br \/>\n  rand(limHaute) {<br \/>\n    this._next = (this._next * KBASE + KINCR) % K10E10;<br \/>\n    return Number(this._next * BigInt(limHaute + 1) \/ K10E10);<br \/>\n  }<br \/>\n}<\/p>\n<p>\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\n\/\/  TIRAGE DES PLAQUES<br \/>\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\nfunction extract(pool, n, rng) {<br \/>\n  pool = [&#8230;pool];<br \/>\n  const result = [];<br \/>\n  for (let i = 0; i &lt; n; i++) {<br \/>\n    const remaining = pool.length &#8211; 1;<br \/>\n    const idx = rng.rand(remaining);<br \/>\n    result.push(pool.splice(idx, 1)[0]);<br \/>\n  }<br \/>\n  return result;<br \/>\n}<\/p>\n<p>function ceb24(n, rng) {<br \/>\n  const pool = [&#8230;Array.from({length:10},(_,i)=&gt;i+1), &#8230;Array.from({length:10},(_,i)=&gt;i+1), 25,50,75,100];<br \/>\n  return extract(pool, n, rng);<br \/>\n}<br \/>\nfunction ceb28(n, rng) {<br \/>\n  const base = [&#8230;Array.from({length:10},(_,i)=&gt;i+1), 25,50,75,100];<br \/>\n  return extract([&#8230;base,&#8230;base], n, rng);<br \/>\n}<br \/>\nfunction alea20(n, rng) {<br \/>\n  const pool = [&#8230;Array.from({length:10},(_,i)=&gt;i+1), &#8230;Array.from({length:10},(_,i)=&gt;i+1)];<br \/>\n  for (let i = 0; i &lt; 8; i++) pool.push(rng.rand(88) + 11);<br \/>\n  return extract(pool, n, rng);<br \/>\n}<br \/>\nfunction tireCible(min, max, rng) {<br \/>\n  return rng.rand(max &#8211; min) + min;<br \/>\n}<\/p>\n<p>\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\n\/\/  SOLVEUR R\u00c9CURSIF<br \/>\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\nfunction calc4(a, b) {<br \/>\n  if (a &lt; b) { let t = a; a = b; b = t; }<br \/>\n  const som = a + b;<br \/>\n  const dif = (a !== b) ? a &#8211; b : 0;<br \/>\n  const mul = (b !== 1) ? a * b : 0;<br \/>\n  let div = 0;<br \/>\n  if (b !== 1 &amp;&amp; b !== 0 &amp;&amp; a % b === 0) div = a \/ b;<br \/>\n  return [som, dif, mul, div];<br \/>\n}<\/p>\n<p>class CEBSolver {<br \/>\n  constructor(target, mode = &#8216;complete&#8217;) {<br \/>\n    this.target    = target;<br \/>\n    this.mode      = mode;<br \/>\n    this.best      = null;<br \/>\n    this.allSols   = [];<br \/>\n    this.reachable = new Set();<br \/>\n    this._seen     = new Set();<br \/>\n    this._ops      = 0;<br \/>\n  }<\/p>\n<p>  _comboKey(plaques) {<br \/>\n    return [&#8230;plaques].sort((a,b)=&gt;a-b).join(&#8216;\/&#8217;);<br \/>\n  }<\/p>\n<p>  _recurse(plaques, steps) {<br \/>\n    const n = plaques.length;<br \/>\n    if (n &lt; 2) return;<\/p>\n<p>    const key   = this._comboKey(plaques);<br \/>\n    const depth = this._depthTotal &#8211; n + 1;<\/p>\n<p>    if (this._seen.has(key)) {<br \/>\n      if (this.mode === &#8216;all&#8217; &amp;&amp; depth &lt;= 3) { \/* continue *\/ }<br \/>\n      else return;<br \/>\n    }<br \/>\n    this._seen.add(key);<\/p>\n<p>    if (this.mode === &#8216;stop&#8217; &amp;&amp; this.best &amp;&amp; this.best.diff === 0) return;<\/p>\n<p>    for (let i = 0; i &lt; n &#8211; 1; i++) {<br \/>\n      for (let j = i + 1; j &lt; n; j++) {<br \/>\n        this._ops++;<br \/>\n        const a = plaques[i], b = plaques[j];<br \/>\n        const [som, dif, mul, div] = calc4(a, b);<br \/>\n        const big = Math.max(a,b), small = Math.min(a,b);<\/p>\n<p>        const ops = [<br \/>\n          [som, `${big} + ${small} = ${som}`],<br \/>\n          dif ? [dif, `${big} &#8211; ${small} = ${dif}`] : null,<br \/>\n          mul ? [mul, `${big} \u00d7 ${small} = ${mul}`] : null,<br \/>\n          div ? [div, `${big} \/ ${small} = ${div}`] : null,<br \/>\n        ];<\/p>\n<p>        for (const entry of ops) {<br \/>\n          if (!entry) continue;<br \/>\n          const [result, label] = entry;<br \/>\n          this.reachable.add(result);<br \/>\n          const diff = Math.abs(result &#8211; this.target);<br \/>\n          const curSteps = [&#8230;steps, label];<\/p>\n<p>          const bestDiff = this.best ? this.best.diff : this.target;<br \/>\n          let isBetter = false;<br \/>\n          if (!this.best)                                          isBetter = true;<br \/>\n          else if (diff &lt; this.best.diff)                          isBetter = true;<br \/>\n          else if (diff === this.best.diff &amp;&amp; curSteps.length &lt; this.best.depth) isBetter = true;<\/p>\n<p>          if (diff &lt;= bestDiff) {<br \/>\n            const sol = { result, diff, depth: curSteps.length, steps: curSteps };<br \/>\n            if (diff === 0) this.allSols.push(sol);<br \/>\n            if (isBetter)   this.best = sol;<br \/>\n          }<br \/>\n        }<\/p>\n<p>        const rest = plaques.filter((_,k) =&gt; k !== i &amp;&amp; k !== j);<br \/>\n        for (const entry of [<br \/>\n          mul ? [mul, `${big} \u00d7 ${small} = ${mul}`] : null,<br \/>\n          div ? [div, `${big} \/ ${small} = ${div}`] : null,<br \/>\n          [som, `${big} + ${small} = ${som}`],<br \/>\n          dif ? [dif, `${big} &#8211; ${small} = ${dif}`] : null,<br \/>\n        ]) {<br \/>\n          if (!entry) continue;<br \/>\n          const [val, label] = entry;<br \/>\n          this._recurse([&#8230;rest, val], [&#8230;steps, label]);<br \/>\n        }<br \/>\n      }<br \/>\n    }<br \/>\n  }<\/p>\n<p>  solve(plaques) {<br \/>\n    this._seen.clear();<br \/>\n    this._ops = 0;<br \/>\n    this._depthTotal = plaques.length;<\/p>\n<p>    if (plaques.includes(this.target)) {<br \/>\n      this.best = { result: this.target, diff: 0, depth: 0,<br \/>\n                    steps: [`La cible ${this.target} est directement une des plaques !`] };<br \/>\n      this.allSols = [this.best];<br \/>\n      return this.best;<br \/>\n    }<\/p>\n<p>    const bestPlaque = plaques.reduce((b,x) =&gt; Math.abs(x-this.target) &lt; Math.abs(b-this.target) ? x : b);<br \/>\n    this.best = { result: bestPlaque, diff: Math.abs(bestPlaque-this.target), depth: 0,<br \/>\n                  steps: [`(meilleure plaque seule : ${bestPlaque})`] };<\/p>\n<p>    this._recurse(plaques, []);<br \/>\n    return this.best;<br \/>\n  }<br \/>\n}<\/p>\n<p>\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\n\/\/  MOTEUR DE JEU<br \/>\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\nconst cfg = {<br \/>\n  tirageMode: &#8216;ceb24&#8217;, solveMode: &#8216;complete&#8217;,<br \/>\n  nbPlaques: 6, targetMin: 101, targetMax: 999,<br \/>\n  timeLimit: 45, partieNum: 0, nbTirages: 50,<br \/>\n  exactOnly:    true,<br \/>\n  repriseAuto:  true,   \/\/ reprise auto r\u00e9sultat pr\u00e9c\u00e9dent (d\u00e9faut coch\u00e9)<br \/>\n  entrainement: false,<br \/>\n  scoreInTime:  [10,  7, -5],<br \/>\n  scoreOutTime: [ 5,  2,-10],<br \/>\n};<\/p>\n<p>let state = {<br \/>\n  plaques: [], target: 0,<br \/>\n  best: null, allSols: [], reachable: new Set(),<br \/>\n  startTime: 0, score: 0, roundNum: 0,<br \/>\n  validated: true,<br \/>\n  history: [],   \/\/ [{tirage, target, diff, pts, inTime}]<br \/>\n};<\/p>\n<p>let rng = null;<br \/>\nfunction getRng() {<br \/>\n  if (!rng) rng = new HP11CRandom(cfg.partieNum);<br \/>\n  return rng;<br \/>\n}<\/p>\n<p>function doTirage() {<br \/>\n  const r = getRng();<br \/>\n  let plaques;<br \/>\n  if      (cfg.tirageMode === &#8216;ceb24&#8217;)  plaques = ceb24(cfg.nbPlaques, r);<br \/>\n  else if (cfg.tirageMode === &#8216;ceb28&#8217;)  plaques = ceb28(cfg.nbPlaques, r);<br \/>\n  else                                  plaques = alea20(cfg.nbPlaques, r);<\/p>\n<p>  let target;<br \/>\n  if (cfg.exactOnly) {<br \/>\n    \/\/ Calcule d&#8217;abord toutes les valeurs atteignables, puis tire parmi elles<br \/>\n    const scanner = new CEBSolver(0, &#8216;complete&#8217;);<br \/>\n    scanner._depthTotal = plaques.length;<br \/>\n    scanner._recurse(plaques, []);<br \/>\n    const candidates = [&#8230;scanner.reachable]<br \/>\n      .filter(v =&gt; v &gt;= cfg.targetMin &amp;&amp; v &lt;= cfg.targetMax)<br \/>\n      .sort((a, b) =&gt; a &#8211; b);<br \/>\n    if (candidates.length &gt; 0) {<br \/>\n      const idx = r.rand(candidates.length &#8211; 1);<br \/>\n      target = candidates[idx];<br \/>\n    } else {<br \/>\n      target = tireCible(cfg.targetMin, cfg.targetMax, r); \/\/ fallback<br \/>\n    }<br \/>\n  } else {<br \/>\n    target = tireCible(cfg.targetMin, cfg.targetMax, r);<br \/>\n  }<\/p>\n<p>  state = { &#8230;state, plaques, target, best: null, allSols: [], reachable: new Set(),<br \/>\n            startTime: Date.now(), roundNum: state.roundNum + 1, validated: false };<br \/>\n  return state;<br \/>\n}<\/p>\n<p>function doSolve() {<br \/>\n  const solver = new CEBSolver(state.target, cfg.solveMode);<br \/>\n  const t0 = performance.now();<br \/>\n  const sol = solver.solve(state.plaques);<br \/>\n  const ms  = performance.now() &#8211; t0;<br \/>\n  state.best    = sol;<br \/>\n  state.allSols = solver.allSols;<br \/>\n  state.reachable = solver.reachable;<br \/>\n  return { sol, solver, ms };<br \/>\n}<\/p>\n<p>function evaluate(playerResult) {<br \/>\n  if (state.validated) return { error: &#8216;D\u00e9j\u00e0 \u00e9valu\u00e9.&#8217; };<br \/>\n  const elapsed = (Date.now() &#8211; state.startTime) \/ 1000;<br \/>\n  const inTime  = elapsed &lt;= cfg.timeLimit;<br \/>\n  const diff    = Math.abs(playerResult &#8211; state.target);<br \/>\n  const tbl     = inTime ? cfg.scoreInTime : cfg.scoreOutTime;<br \/>\n  const pts     = diff === 0 ? tbl[0] : diff &lt;= 3 ? tbl[1] : tbl[2];<br \/>\n  state.score    += pts;<br \/>\n  state.validated = true;<br \/>\n  state.history.push({<br \/>\n    num: state.roundNum, plaques: [&#8230;state.plaques],<br \/>\n    target: state.target, result: playerResult,<br \/>\n    diff, pts, inTime, elapsed: +elapsed.toFixed(1),<br \/>\n  });<br \/>\n  if (!cfg.entrainement &amp;&amp; state.roundNum &gt;= cfg.nbTirages) {<br \/>\n    setTimeout(() =&gt; openFinPartie(), 400);<br \/>\n  }<br \/>\n  saveSession();<br \/>\n  return { diff, inTime, elapsed: elapsed.toFixed(1), points: pts,<br \/>\n           total: state.score,<br \/>\n           msg: `${inTime ? &#8216;\u2713 In time&#8217; : &#8216;\u2717 Out of time&#8217;} (${elapsed.toFixed(1)}s) | \u00e9cart=${diff} | ${pts &gt;= 0 ? &#8216;+&#8217; : &#8221;}${pts} pts` };<br \/>\n}<\/p>\n<p>\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\n\/\/  UI STATE<br \/>\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\nlet phase = 1;  \/\/ 1=op1, 2=oper, 3=op2<br \/>\nlet currentOp1   = null;  \/\/ {idx, val}<br \/>\nlet currentOper  = null;<br \/>\nlet calcSteps    = [];    \/\/ [{op1,oper,op2,res}]<br \/>\nlet plaqueBtns   = [];<br \/>\nlet plaqueActive = [];<br \/>\nlet plaqueVals   = [];<br \/>\nlet timerId      = null;<br \/>\nlet elapsed      = 0;<br \/>\nlet lastSolver   = null;<\/p>\n<p>\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\n\/\/  TIMER<br \/>\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\nfunction startTimer() {<br \/>\n  elapsed = 0;<br \/>\n  stopTimer();<br \/>\n  tick();<br \/>\n}<br \/>\nfunction tick() {<br \/>\n  const rest = cfg.timeLimit &#8211; elapsed;<br \/>\n  const t = document.getElementById(&#8216;lbl-timer&#8217;);<br \/>\n  t.textContent = rest &gt;= 0 ? `${rest}s` : &#8216;Temps !&#8217;;<br \/>\n  t.style.color = rest &gt; 15 ? &#8216;var(&#8211;green)&#8217; : rest &gt; 5 ? &#8216;var(&#8211;accent2)&#8217; : &#8216;var(&#8211;accent)&#8217;;<br \/>\n  elapsed++;<br \/>\n  timerId = setTimeout(tick, 1000);<br \/>\n}<br \/>\nfunction stopTimer() {<br \/>\n  if (timerId) { clearTimeout(timerId); timerId = null; }<br \/>\n}<\/p>\n<p>\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\n\/\/  AFFICHAGE PLAQUES<br \/>\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\nfunction renderPlaques() {<br \/>\n  const grid = document.getElementById(&#8216;plq-grid&#8217;);<br \/>\n  const n    = state.plaques.length;<br \/>\n  grid.innerHTML = &#8221;;<br \/>\n  plaqueBtns = [];<\/p>\n<p>  \/\/ Rang\u00e9e 1 \u2014 plaques initiales<br \/>\n  for (let i = 0; i &lt; n; i++) {<br \/>\n    const btn = _makePlaqueBtn(i, plaqueVals[i], plaqueActive[i], false);<br \/>\n    grid.appendChild(btn);<br \/>\n    plaqueBtns.push(btn);<br \/>\n  }<\/p>\n<p>  \/\/ Rang\u00e9e 2 \u2014 emplacements r\u00e9sultats (n slots fant\u00f4mes)<br \/>\n  for (let i = 0; i &lt; n; i++) {<br \/>\n    const ghost = document.createElement(&#8216;div&#8217;);<br \/>\n    ghost.className = &#8216;plq-btn-ghost&#8217;;<br \/>\n    ghost.dataset.slot = i;<br \/>\n    grid.appendChild(ghost);<br \/>\n  }<br \/>\n}<\/p>\n<p>function _makePlaqueBtn(idx, val, active, isResult) {<br \/>\n  const btn = document.createElement(&#8216;button&#8217;);<br \/>\n  btn.className = &#8216;plq-btn&#8217; + (isResult ? &#8216; result&#8217; : &#8221;) + (!active ? &#8216; used&#8217; : &#8221;);<br \/>\n  btn.textContent = val;<br \/>\n  btn.disabled = !active;<br \/>\n  btn.onclick = () =&gt; onPlaque(idx);<br \/>\n  return btn;<br \/>\n}<\/p>\n<p>\/\/ Ajoute un r\u00e9sultat dans le prochain slot fant\u00f4me de la rang\u00e9e 2<br \/>\nfunction _addResultBtn(idx, val) {<br \/>\n  const grid    = document.getElementById(&#8216;plq-grid&#8217;);<br \/>\n  const n       = state.plaques.length;<br \/>\n  const slotIdx = idx &#8211; n;<br \/>\n  const target  = grid.querySelector(`.plq-btn-ghost[data-slot=&#8221;${slotIdx}&#8221;]`);<br \/>\n  if (!target) return;<br \/>\n  const btn = _makePlaqueBtn(idx, val, true, true);<br \/>\n  grid.replaceChild(btn, target);<br \/>\n  plaqueBtns.push(btn);<br \/>\n}<\/p>\n<p>\/\/ Supprime le dernier r\u00e9sultat et remet un ghost \u00e0 sa place<br \/>\nfunction _removeLastResultBtn() {<br \/>\n  const grid = document.getElementById(&#8216;plq-grid&#8217;);<br \/>\n  const n    = state.plaques.length;<br \/>\n  const children = [&#8230;grid.children];<br \/>\n  for (let i = children.length &#8211; 1; i &gt;= n; i&#8211;) {<br \/>\n    if (!children[i].classList.contains(&#8216;plq-btn-ghost&#8217;)) {<br \/>\n      const slotIdx = i &#8211; n;<br \/>\n      const ghost = document.createElement(&#8216;div&#8217;);<br \/>\n      ghost.className = &#8216;plq-btn-ghost&#8217;;<br \/>\n      ghost.dataset.slot = slotIdx;<br \/>\n      grid.replaceChild(ghost, children[i]);<br \/>\n      plaqueBtns.pop();<br \/>\n      return;<br \/>\n    }<br \/>\n  }<br \/>\n}<\/p>\n<p>function refreshHist() {<br \/>\n  const div = document.getElementById(&#8216;hist-lines&#8217;);<br \/>\n  div.innerHTML = &#8221;;<br \/>\n  for (let i = 0; i &lt; state.plaques.length &#8211; 1; i++) {<br \/>\n    const d = document.createElement(&#8216;div&#8217;);<br \/>\n    d.className = &#8216;hist-line&#8217;;<br \/>\n    if (i &lt; calcSteps.length) {<br \/>\n      const {op1,oper,op2,res} = calcSteps[i];<br \/>\n      d.textContent = `  ${op1} ${oper} ${op2} = ${res}`;<br \/>\n    }<br \/>\n    div.appendChild(d);<br \/>\n  }<br \/>\n}<\/p>\n<p>function setStatus(txt) { document.getElementById(&#8216;lbl-status&#8217;).textContent = txt; }<br \/>\nfunction setCurOp(txt)   { document.getElementById(&#8216;cur-op&#8217;).textContent = txt; }<\/p>\n<p>\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\n\/\/  CHECK OPERATION<br \/>\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\nfunction checkOperation(op1, oper, op2) {<br \/>\n  if (oper === &#8216;+&#8217;) return { ok: true, res: op1 + op2, err: &#8221; };<br \/>\n  if (oper === &#8216;-&#8216;) {<br \/>\n    const res = op1 &#8211; op2;<br \/>\n    if (res &lt; 1) return { ok: false, res: 0, err: `Soustraction non autoris\u00e9e (r\u00e9sultat = ${res} &lt; 1)` };<br \/>\n    return { ok: true, res, err: &#8221; };<br \/>\n  }<br \/>\n  if (oper === &#8216;X&#8217; || oper === &#8216;\u00d7&#8217;) return { ok: true, res: op1 * op2, err: &#8221; };<br \/>\n  if (oper === &#8216;\/&#8217;) {<br \/>\n    if (op2 === 0) return { ok: false, res: 0, err: &#8216;Division par z\u00e9ro&#8217; };<br \/>\n    if (op1 % op2 !== 0) return { ok: false, res: 0, err: `Division non enti\u00e8re (${op1} \u00f7 ${op2})` };<br \/>\n    return { ok: true, res: op1 \/ op2, err: &#8221; };<br \/>\n  }<br \/>\n  return { ok: false, res: 0, err: &#8216;Op\u00e9rateur inconnu&#8217; };<br \/>\n}<\/p>\n<p>\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\n\/\/  INTERACTIONS JOUEUR<br \/>\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\nfunction onPlaque(idx) {<br \/>\n  if (!plaqueActive[idx]) return;<br \/>\n  const val = plaqueVals[idx];<\/p>\n<p>  if (phase === 1 || phase === 2) {<br \/>\n    \/\/ Phase 1 : choix op1  |  Phase 2 : remplacement op1 (nouvelle plaque cliqu\u00e9e)<br \/>\n    if (phase === 2 &amp;&amp; currentOp1) {<br \/>\n      \/\/ D\u00e9s\u00e9lectionne l&#8217;ancien op1<br \/>\n      plaqueBtns[currentOp1.idx].classList.remove(&#8216;selected&#8217;);<br \/>\n    }<br \/>\n    currentOp1 = { idx, val };<br \/>\n    plaqueBtns[idx].classList.add(&#8216;selected&#8217;);<br \/>\n    phase = 2;<br \/>\n    setCurOp(`${val}  ?  &#8230;`);<br \/>\n    setStatus(`${val} s\u00e9lectionn\u00e9 \u2014 choisis un op\u00e9rateur.`);<\/p>\n<p>  } else if (phase === 3) {<br \/>\n    \/\/ Clic sur la plaque d\u00e9j\u00e0 choisie comme op1 \u2192 d\u00e9s\u00e9lection, retour phase 1<br \/>\n    if (idx === currentOp1.idx) {<br \/>\n      plaqueBtns[currentOp1.idx].classList.remove(&#8216;selected&#8217;);<br \/>\n      document.querySelectorAll(&#8216;.op-btn&#8217;).forEach(b =&gt; b.classList.remove(&#8216;active&#8217;));<br \/>\n      currentOp1 = currentOper = null;<br \/>\n      phase = 1;<br \/>\n      setCurOp(&#8221;);<br \/>\n      setStatus(&#8216;S\u00e9lectionne une plaque.&#8217;);<br \/>\n      return;<br \/>\n    }<br \/>\n    const op1 = currentOp1.val;<br \/>\n    const { ok, res, err } = checkOperation(op1, currentOper, val);<br \/>\n    if (!ok) { alert(err); return; }<\/p>\n<p>    \/\/ D\u00e9sactive les deux plaques<br \/>\n    for (const i2 of [currentOp1.idx, idx]) {<br \/>\n      plaqueActive[i2] = false;<br \/>\n      plaqueBtns[i2].disabled = true;<br \/>\n      plaqueBtns[i2].classList.remove(&#8216;selected&#8217;);<br \/>\n      plaqueBtns[i2].classList.add(&#8216;used&#8217;);<br \/>\n    }<\/p>\n<p>    calcSteps.push({ op1, oper: currentOper, op2: val, res });<br \/>\n    refreshHist();<br \/>\n    saveSession();<\/p>\n<p>    \/\/ Bouton r\u00e9sultat dans la rang\u00e9e 2<br \/>\n    const newIdx = plaqueVals.length;<br \/>\n    plaqueVals.push(res);<br \/>\n    plaqueActive.push(true);<br \/>\n    _addResultBtn(newIdx, res);<\/p>\n<p>    if (res === state.target &amp;&amp; !state.validated) {<br \/>\n      stopTimer();<br \/>\n      const r = evaluate(res);<br \/>\n      if (!r.error) {<br \/>\n        document.getElementById(&#8216;lbl-score&#8217;).textContent = r.total;<br \/>\n        setStatus(`\ud83c\udf89 COMPTE EST BON !  ${r.msg}`);<br \/>\n        alert(`Compte est bon !\\n\\n${r.msg}`);<br \/>\n        document.getElementById(&#8216;btn-resoudre&#8217;).disabled = false;<br \/>\n        updateNouveauBtn();<br \/>\n      }<br \/>\n      resetPhase();<br \/>\n      return;<br \/>\n    }<\/p>\n<p>    \/\/ Reprise auto : le r\u00e9sultat devient op1 de la prochaine \u00e9tape<br \/>\n    if (cfg.repriseAuto) {<br \/>\n      currentOp1 = { idx: newIdx, val: res };<br \/>\n      plaqueBtns[newIdx].classList.add(&#8216;selected&#8217;);<br \/>\n      phase = 2;<br \/>\n      setCurOp(`${res}  ?  &#8230;`);<br \/>\n      setStatus(`Reprise : ${res} s\u00e9lectionn\u00e9 \u2014 choisis un op\u00e9rateur.`);<br \/>\n    } else {<br \/>\n      resetPhase();<br \/>\n    }<br \/>\n  }<br \/>\n}<\/p>\n<p>function onOperator(sym, btn) {<br \/>\n  \/\/ Autoris\u00e9 en phase 2 (op1 choisi) ou phase 3 (remplacement op\u00e9rateur)<br \/>\n  if (phase !== 2 &amp;&amp; phase !== 3) return;<br \/>\n  document.querySelectorAll(&#8216;.op-btn&#8217;).forEach(b =&gt; b.classList.remove(&#8216;active&#8217;));<br \/>\n  if (btn) btn.classList.add(&#8216;active&#8217;);<br \/>\n  currentOper = sym;<br \/>\n  phase = 3;<br \/>\n  setCurOp(`${currentOp1.val}  ${sym}  &#8230;`);<br \/>\n  setStatus(`${currentOp1.val} ${sym} &#8230; \u2014 choisis la 2e plaque.`);<br \/>\n}<\/p>\n<p>function resetPhase() {<br \/>\n  currentOp1 = currentOper = null;<br \/>\n  phase = 1;<br \/>\n  setCurOp(&#8221;);<br \/>\n  document.querySelectorAll(&#8216;.op-btn&#8217;).forEach(b =&gt; b.classList.remove(&#8216;active&#8217;));<br \/>\n  setStatus(&#8216;S\u00e9lectionne une plaque.&#8217;);<br \/>\n}<\/p>\n<p>function onEffacer() {<br \/>\n  if (phase === 2) {<br \/>\n    plaqueBtns[currentOp1.idx].classList.remove(&#8216;selected&#8217;);<br \/>\n    resetPhase();<br \/>\n  } else if (phase === 3) {<br \/>\n    resetPhase();<br \/>\n  } else if (calcSteps.length &gt; 0) {<br \/>\n    const {op1, op2, res} = calcSteps.pop();<br \/>\n    \/\/ R\u00e9active les 2 plaques sources (cherche par valeur + inactive)<br \/>\n    for (const search of [op1, op2]) {<br \/>\n      for (let i = 0; i &lt; plaqueVals.length; i++) {<br \/>\n        if (plaqueVals[i] === search &amp;&amp; !plaqueActive[i] &amp;&amp; i &lt; plaqueBtns.length) {<br \/>\n          plaqueActive[i] = true;<br \/>\n          plaqueBtns[i].disabled = false;<br \/>\n          plaqueBtns[i].classList.remove(&#8216;used&#8217;);<br \/>\n          break;<br \/>\n        }<br \/>\n      }<br \/>\n    }<br \/>\n    \/\/ Supprime le bouton r\u00e9sultat et remet un ghost<br \/>\n    if (plaqueVals.length &gt; state.plaques.length) {<br \/>\n      _removeLastResultBtn();<br \/>\n      plaqueActive.pop();<br \/>\n      plaqueVals.pop();<br \/>\n    }<br \/>\n    refreshHist();<br \/>\n    resetPhase();<br \/>\n  }<br \/>\n}<\/p>\n<p>function onValider() {<br \/>\n  if (!calcSteps.length) { alert(&#8216;Effectue au moins un calcul d\\&#8217;abord.&#8217;); return; }<br \/>\n  if (state.validated)   { alert(&#8216;D\u00e9j\u00e0 valid\u00e9 \u2014 Lance un nouveau tirage !&#8217;); return; }<br \/>\n  stopTimer();<br \/>\n  const r = evaluate(calcSteps[calcSteps.length &#8211; 1].res);<br \/>\n  if (r.error) { alert(r.error); return; }<br \/>\n  document.getElementById(&#8216;lbl-score&#8217;).textContent = r.total;<br \/>\n  setStatus(r.msg);<br \/>\n  alert(r.msg);<br \/>\n  document.getElementById(&#8216;btn-resoudre&#8217;).disabled = false;<br \/>\n  updateNouveauBtn();<br \/>\n}<\/p>\n<p>function onResoudre() {<br \/>\n  setStatus(&#8216;Calcul en cours\u2026&#8217;);<br \/>\n  document.getElementById(&#8216;btn-resoudre&#8217;).disabled = true;<br \/>\n  setTimeout(() =&gt; {<br \/>\n    const { sol, solver, ms } = doSolve();<br \/>\n    lastSolver = solver;<br \/>\n    showSolution(sol, solver, ms);<br \/>\n    document.getElementById(&#8216;btn-resoudre&#8217;).disabled = false;<br \/>\n    document.getElementById(&#8216;btn-detail&#8217;).disabled = false;<br \/>\n  }, 10);<br \/>\n}<\/p>\n<p>function showSolution(sol, solver, ms) {<br \/>\n  const box = document.getElementById(&#8216;sol-box&#8217;);<br \/>\n  const div = document.getElementById(&#8216;sol-lines&#8217;);<br \/>\n  box.style.display = &#8216;block&#8217;;<br \/>\n  div.innerHTML = &#8221;;<br \/>\n  if (sol) {<br \/>\n    for (const step of sol.steps) {<br \/>\n      const d = document.createElement(&#8216;div&#8217;);<br \/>\n      d.className = &#8216;sol-line&#8217; + (sol.diff === 0 ? &#8216; exact&#8217; : &#8221;);<br \/>\n      d.textContent = `  ${step}`;<br \/>\n      div.appendChild(d);<br \/>\n    }<br \/>\n  }<br \/>\n  const diff = sol ? (sol.diff === 0 ? &#8216;exact&#8217; : `\u00e9cart=${sol.diff}`) : &#8216;?&#8217;;<br \/>\n  setStatus(`R\u00e9solu en ${ms.toFixed(0)} ms \u2014 ${diff} \u2014 ${solver.allSols.length} solution(s) exacte(s)`);<br \/>\n}<\/p>\n<p>\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\n\/\/  NOUVEAU TIRAGE<br \/>\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\nfunction newGame() {<br \/>\n  stopTimer();<br \/>\n  hideSolution();<\/p>\n<p>  if (cfg.entrainement) {<br \/>\n    \/\/ Mode entra\u00eenement : affiche la zone de saisie, pas de tirage auto<br \/>\n    document.getElementById(&#8216;entrainement-zone&#8217;).style.display = &#8216;block&#8217;;<br \/>\n    document.getElementById(&#8216;plq-title&#8217;).textContent = &#8216;Saisie libre&#8217;;<br \/>\n    document.getElementById(&#8216;plq-grid&#8217;).innerHTML = &#8221;;<br \/>\n    document.getElementById(&#8216;lbl-cible&#8217;).textContent = &#8216;\u2014&#8217;;<br \/>\n    document.getElementById(&#8216;lbl-round&#8217;).textContent = &#8216;Entra\u00eenement&#8217;;<br \/>\n    document.getElementById(&#8216;lbl-mode&#8217;).textContent  = &#8216;LIBRE&#8217;;<br \/>\n    document.getElementById(&#8216;lbl-timer&#8217;).textContent = &#8216;\u2014&#8217;;<br \/>\n    document.getElementById(&#8216;btn-detail&#8217;).disabled   = true;<br \/>\n    document.getElementById(&#8216;btn-resoudre&#8217;).disabled = true;<br \/>\n    _buildEntryPlaques(cfg.nbPlaques);<br \/>\n    calcSteps = []; plaqueVals = []; plaqueActive = []; plaqueBtns = [];<br \/>\n    refreshHist();<br \/>\n    resetPhase();<br \/>\n    setStatus(&#8216;Saisis tes plaques et ta cible, puis clique sur Jouer !&#8217;);<br \/>\n    return;<br \/>\n  }<\/p>\n<p>  document.getElementById(&#8216;entrainement-zone&#8217;).style.display = &#8216;none&#8217;;<br \/>\n  document.getElementById(&#8216;plq-title&#8217;).textContent = &#8216;Plaques disponibles&#8217;;<\/p>\n<p>  setStatus(&#8216;Tirage en cours\u2026&#8217;);<br \/>\n  \/\/ setTimeout pour laisser le DOM se mettre \u00e0 jour avant le calcul exactOnly<br \/>\n  setTimeout(() =&gt; {<br \/>\n    const s = doTirage();<br \/>\n    saveSession();<br \/>\n    document.getElementById(&#8216;lbl-cible&#8217;).textContent = s.target;<br \/>\n    document.getElementById(&#8216;lbl-round&#8217;).textContent = `Tirage ${s.roundNum} \/ ${cfg.nbTirages}`;<br \/>\n    document.getElementById(&#8216;lbl-mode&#8217;).textContent  = cfg.tirageMode.toUpperCase() + (cfg.exactOnly ? &#8216; \u2713&#8217; : &#8221;);<br \/>\n    document.getElementById(&#8216;btn-detail&#8217;).disabled   = true;<br \/>\n    document.getElementById(&#8216;btn-resoudre&#8217;).disabled = true;<br \/>\n    plaqueVals   = [&#8230;s.plaques];<br \/>\n    plaqueActive = s.plaques.map(() =&gt; true);<br \/>\n    calcSteps    = [];<br \/>\n    renderPlaques();<br \/>\n    refreshHist();<br \/>\n    resetPhase();<br \/>\n    startTimer();<br \/>\n    updateNouveauBtn();<br \/>\n    setStatus(&#8216;S\u00e9lectionne une plaque pour commencer !&#8217;);<br \/>\n  }, 10);<br \/>\n}<\/p>\n<p>function hideSolution() {<br \/>\n  document.getElementById(&#8216;sol-box&#8217;).style.display = &#8216;none&#8217;;<br \/>\n  document.getElementById(&#8216;sol-lines&#8217;).innerHTML = &#8221;;<br \/>\n  lastSolver = null;<br \/>\n}<\/p>\n<p>\/\/ \u2500\u2500 Mode entra\u00eenement \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\nfunction _buildEntryPlaques(nb) {<br \/>\n  const row = document.getElementById(&#8216;entry-plq-row&#8217;);<br \/>\n  row.innerHTML = &#8221;;<br \/>\n  for (let i = 0; i &lt; nb; i++) {<br \/>\n    const inp = document.createElement(&#8216;input&#8217;);<br \/>\n    inp.type = &#8216;number&#8217;; inp.min = &#8216;1&#8217;; inp.max = &#8216;999&#8217;;<br \/>\n    inp.placeholder = `P${i+1}`;<br \/>\n    inp.style.cssText = &#8216;width:60px;background:var(&#8211;bg-hdr);color:var(&#8211;text);&#8217; +<br \/>\n      &#8216;border:1px solid var(&#8211;border);border-radius:6px;padding:6px 4px;&#8217; +<br \/>\n      &#8216;font-size:1rem;font-weight:700;text-align:center;&#8217;;<br \/>\n    inp.dataset.idx = i;<br \/>\n    row.appendChild(inp);<br \/>\n  }<br \/>\n  document.getElementById(&#8216;entry-cible&#8217;).value = &#8221;;<br \/>\n}<\/p>\n<p>function jouerEntrainement() {<br \/>\n  const inputs = document.querySelectorAll(&#8216;#entry-plq-row input&#8217;);<br \/>\n  const plaques = [];<br \/>\n  for (const inp of inputs) {<br \/>\n    const v = parseInt(inp.value);<br \/>\n    if (!v || v &lt; 1) { alert(&#8216;Toutes les plaques doivent \u00eatre remplies (&gt;= 1)&#8217;); inp.focus(); return; }<br \/>\n    plaques.push(v);<br \/>\n  }<br \/>\n  const cible = parseInt(document.getElementById(&#8216;entry-cible&#8217;).value);<br \/>\n  if (!cible || cible &lt; 1) { alert(&#8216;Saisis une cible valide (&gt;= 1)&#8217;); return; }<\/p>\n<p>  document.getElementById(&#8216;entrainement-zone&#8217;).style.display = &#8216;none&#8217;;<br \/>\n  state = { &#8230;state, plaques, target: cible,<br \/>\n            best: null, allSols: [], reachable: new Set(),<br \/>\n            startTime: Date.now(), roundNum: state.roundNum + 1, validated: false };<\/p>\n<p>  document.getElementById(&#8216;lbl-cible&#8217;).textContent = cible;<br \/>\n  document.getElementById(&#8216;lbl-round&#8217;).textContent = `Entra\u00eenement ${state.roundNum}`;<br \/>\n  document.getElementById(&#8216;btn-detail&#8217;).disabled   = true;<br \/>\n  document.getElementById(&#8216;btn-resoudre&#8217;).disabled = true;<\/p>\n<p>  plaqueVals   = [&#8230;plaques];<br \/>\n  plaqueActive = plaques.map(() =&gt; true);<br \/>\n  calcSteps    = [];<br \/>\n  renderPlaques();<br \/>\n  refreshHist();<br \/>\n  resetPhase();<br \/>\n  \/\/ Pas de timer en mode entra\u00eenement<br \/>\n  document.getElementById(&#8216;lbl-timer&#8217;).textContent = &#8216;Entra\u00eenement&#8217;;<br \/>\n  setStatus(&#8216;C\\&#8217;est parti ! S\u00e9lectionne une plaque.&#8217;);<br \/>\n}<\/p>\n<p>\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\n\/\/  OPTIONS<br \/>\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\nfunction openOptions() {<br \/>\n  stopTimer();<br \/>\n  document.getElementById(&#8216;opt-tirage&#8217;).value         = cfg.tirageMode;<br \/>\n  document.getElementById(&#8216;opt-nbplq&#8217;).value          = cfg.nbPlaques;<br \/>\n  document.getElementById(&#8216;opt-tmin&#8217;).value           = cfg.targetMin;<br \/>\n  document.getElementById(&#8216;opt-tmax&#8217;).value           = cfg.targetMax;<br \/>\n  document.getElementById(&#8216;opt-time&#8217;).value           = cfg.timeLimit;<br \/>\n  document.getElementById(&#8216;opt-partie&#8217;).value         = cfg.partieNum;<br \/>\n  document.getElementById(&#8216;opt-nbtirages&#8217;).value      = cfg.nbTirages;<br \/>\n  document.getElementById(&#8216;opt-solve&#8217;).value          = cfg.solveMode;<br \/>\n  document.getElementById(&#8216;opt-exact&#8217;).checked        = cfg.exactOnly;<br \/>\n  document.getElementById(&#8216;opt-reprise&#8217;).checked      = cfg.repriseAuto;<br \/>\n  document.getElementById(&#8216;opt-entrainement&#8217;).checked = cfg.entrainement;<br \/>\n  document.getElementById(&#8216;modal-options&#8217;).classList.add(&#8216;open&#8217;);<br \/>\n}<\/p>\n<p>function saveOptions() {<br \/>\n  const nb  = parseInt(document.getElementById(&#8216;opt-nbplq&#8217;).value);<br \/>\n  const mn  = parseInt(document.getElementById(&#8216;opt-tmin&#8217;).value);<br \/>\n  const mx  = parseInt(document.getElementById(&#8216;opt-tmax&#8217;).value);<br \/>\n  const nbt = parseInt(document.getElementById(&#8216;opt-nbtirages&#8217;).value);<br \/>\n  if (mn &gt;= mx)   { alert(&#8216;Cible min doit \u00eatre &lt; Cible max&#8217;); return; }<br \/>\n  if (nb &lt; 2 || nb &gt; 8) { alert(&#8216;Nombre de plaques : 2 \u00e0 8&#8217;); return; }<br \/>\n  if (nbt &lt; 1)    { alert(&#8216;Nombre de tirages &gt;= 1&#8217;); return; }<\/p>\n<p>  cfg.tirageMode   = document.getElementById(&#8216;opt-tirage&#8217;).value;<br \/>\n  cfg.solveMode    = document.getElementById(&#8216;opt-solve&#8217;).value;<br \/>\n  cfg.nbPlaques    = nb;<br \/>\n  cfg.targetMin    = mn;<br \/>\n  cfg.targetMax    = mx;<br \/>\n  cfg.timeLimit    = parseInt(document.getElementById(&#8216;opt-time&#8217;).value);<br \/>\n  cfg.nbTirages    = nbt;<br \/>\n  cfg.exactOnly    = document.getElementById(&#8216;opt-exact&#8217;).checked;<br \/>\n  cfg.repriseAuto  = document.getElementById(&#8216;opt-reprise&#8217;).checked;<br \/>\n  cfg.entrainement = document.getElementById(&#8216;opt-entrainement&#8217;).checked;<\/p>\n<p>  const np = parseInt(document.getElementById(&#8216;opt-partie&#8217;).value);<br \/>\n  if (np !== cfg.partieNum) {<br \/>\n    cfg.partieNum = np;<br \/>\n    rng = null;<br \/>\n    clearSession();<br \/>\n    state.score    = 0;<br \/>\n    state.roundNum = 0;<br \/>\n    state.history  = [];<br \/>\n    document.getElementById(&#8216;lbl-score&#8217;).textContent = 0;<br \/>\n  }<br \/>\n  closeModal(&#8216;modal-options&#8217;);<br \/>\n  if (cfg.entrainement) {<br \/>\n    newGame();<br \/>\n  } else {<br \/>\n    setStatus(&#8216;Options appliqu\u00e9es \u2014 Lance un nouveau tirage !&#8217;);<br \/>\n  }<br \/>\n}<\/p>\n<p>function closeModal(id) { document.getElementById(id).classList.remove(&#8216;open&#8217;); }<\/p>\n<p>\/\/ \u2500\u2500 Verrou bouton Nouveau en mode comp\u00e9tition \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\nfunction updateNouveauBtn() {<br \/>\n  const locked = cfg.partieNum &gt; 0 &amp;&amp; !cfg.entrainement &amp;&amp; !state.validated;<br \/>\n  const btn = document.getElementById(&#8216;btn-nouveau&#8217;);<br \/>\n  btn.disabled = locked;<br \/>\n  btn.title    = locked ? &#8216;Validez votre r\u00e9sultat avant de passer au tirage suivant&#8217; : &#8221;;<br \/>\n}<\/p>\n<p>\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\n\/\/  FIN DE PARTIE<br \/>\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\nfunction openFinPartie() {<br \/>\n  stopTimer();<br \/>\n  recordPartie();   \/\/ sauvegarde dans localStorage<br \/>\n  const h    = state.history;<br \/>\n  const tot  = state.score;<br \/>\n  const moy  = h.length ? (tot \/ h.length).toFixed(1) : 0;<br \/>\n  const gagne = h.filter(e =&gt; e.diff === 0).length;<\/p>\n<p>  document.getElementById(&#8216;fin-graine&#8217;).textContent =<br \/>\n    `Partie ${cfg.partieNum || &#8216;al\u00e9atoire&#8217;} \u2014 ${h.length} tirages`;<br \/>\n  document.getElementById(&#8216;fin-score&#8217;).textContent = `${tot &gt; 0 ? &#8216;+&#8217; : &#8221;}${tot} pts`;<br \/>\n  document.getElementById(&#8216;fin-score&#8217;).style.color = tot &gt;= 0 ? &#8216;var(&#8211;accent2)&#8217; : &#8216;var(&#8211;accent)&#8217;;<br \/>\n  document.getElementById(&#8216;fin-moyenne&#8217;).textContent =<br \/>\n    `Moyenne : ${moy} pts\/tirage  |  Comptes exacts : ${gagne} \/ ${h.length}`;<\/p>\n<p>  const div = document.getElementById(&#8216;fin-hist&#8217;);<br \/>\n  div.innerHTML = &#8221;;<br \/>\n  const frag = document.createDocumentFragment();<br \/>\n  h.forEach(e =&gt; {<br \/>\n    const row = document.createElement(&#8216;div&#8217;);<br \/>\n    row.style.cssText = &#8216;display:flex;justify-content:space-between;padding:4px 8px;&#8217; +<br \/>\n      `border-radius:4px;margin-bottom:3px;font-size:.82rem;` +<br \/>\n      `background:${e.diff === 0 ? &#8216;#1a4a2a&#8217; : e.diff &lt;= 3 ? &#8216;#2a3a1a&#8217; : &#8216;#3a1a1a&#8217;};`;<br \/>\n    row.innerHTML =<br \/>\n      `&lt;span style=&#8221;color:var(&#8211;text-dim)&#8221;&gt;#${e.num}&lt;\/span&gt;` +<br \/>\n      `&lt;span&gt;${e.plaques.join(&#8216; &#8216;)}&lt;\/span&gt;` +<br \/>\n      `&lt;span style=&#8221;color:var(&#8211;accent2);font-weight:700&#8243;&gt;\u2192${e.target}&lt;\/span&gt;` +<br \/>\n      `&lt;span style=&#8221;color:${e.diff===0?&#8217;var(&#8211;green)&#8217;:e.diff&lt;=3?&#8217;var(&#8211;accent2)&#8217;:&#8217;var(&#8211;accent)&#8217;}&#8221;&gt;` +<br \/>\n        `${e.diff===0?&#8217;\u2713&#8217;:(&#8216;\u00b1&#8217;+e.diff)}&lt;\/span&gt;` +<br \/>\n      `&lt;span style=&#8221;color:${e.pts&gt;=0?&#8217;var(&#8211;green)&#8217;:&#8217;var(&#8211;accent)&#8217;}&#8221;&gt;` +<br \/>\n        `${e.pts&gt;=0?&#8217;+&#8217;:&#8221;}${e.pts}&lt;\/span&gt;`;<br \/>\n    frag.appendChild(row);<br \/>\n  });<br \/>\n  div.appendChild(frag);<br \/>\n  document.getElementById(&#8216;modal-fin&#8217;).classList.add(&#8216;open&#8217;);<br \/>\n}<\/p>\n<p>function nouvellePartie() {<br \/>\n  closeModal(&#8216;modal-fin&#8217;);<br \/>\n  clearSession();<br \/>\n  rng = null;<br \/>\n  state.score    = 0;<br \/>\n  state.roundNum = 0;<br \/>\n  state.history  = [];<br \/>\n  document.getElementById(&#8216;lbl-score&#8217;).textContent = 0;<br \/>\n  newGame();<br \/>\n}<\/p>\n<p>\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\n\/\/  DETAIL \u2014 canvas virtualis\u00e9 pour r\u00e9sultats atteignables<br \/>\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\nlet _reachCanvas = null, _reachData = null;<\/p>\n<p>function _buildReachCanvas(sorted, target) {<br \/>\n  const wrap = document.getElementById(&#8216;reach-canvas-wrap&#8217;);<br \/>\n  wrap.innerHTML = &#8221;;<\/p>\n<p>  const COL    = 10;<br \/>\n  const CW     = 58;   \/\/ cell width px<br \/>\n  const CH     = 22;   \/\/ cell height px<br \/>\n  const PAD    = 2;<br \/>\n  const nRows  = Math.ceil(sorted.length \/ COL);<br \/>\n  const totalH = nRows * (CH + PAD);<\/p>\n<p>  const canvas = document.createElement(&#8216;canvas&#8217;);<br \/>\n  canvas.style.cssText = &#8216;display:block;width:100%;cursor:default;&#8217;;<br \/>\n  wrap.appendChild(canvas);<\/p>\n<p>  \/\/ Premier gap (pour couleur)<br \/>\n  let firstGapVal = null;<br \/>\n  for (let i = 1; i &lt; sorted.length; i++) {<br \/>\n    if (sorted[i] !== sorted[i-1] + 1) { firstGapVal = sorted[i]; break; }<br \/>\n  }<\/p>\n<p>  function draw() {<br \/>\n    const W = wrap.clientWidth || 600;<br \/>\n    const cw = Math.floor(W \/ COL);<br \/>\n    canvas.width  = W;<br \/>\n    canvas.height = totalH;<br \/>\n    canvas.style.height = totalH + &#8216;px&#8217;;<br \/>\n    const ctx = canvas.getContext(&#8216;2d&#8217;);<br \/>\n    ctx.font = &#8217;11px Courier New&#8217;;<br \/>\n    ctx.textAlign = &#8216;center&#8217;;<br \/>\n    ctx.textBaseline = &#8216;middle&#8217;;<\/p>\n<p>    for (let idx = 0; idx &lt; sorted.length; idx++) {<br \/>\n      const val = sorted[idx];<br \/>\n      const row = Math.floor(idx \/ COL);<br \/>\n      const col = idx % COL;<br \/>\n      const x   = col * cw;<br \/>\n      const y   = row * (CH + PAD);<\/p>\n<p>      let bg = &#8216;#2e5070&#8217;, fg = &#8216;#ffffff&#8217;;<br \/>\n      if (val === target)        { bg = &#8216;#f5a623&#8217;; fg = &#8216;#1a1a2e&#8217;; }<br \/>\n      else if (val === firstGapVal) { bg = &#8216;#2d5f80&#8217;; fg = &#8216;#5dade2&#8217;; }<\/p>\n<p>      ctx.fillStyle = bg;<br \/>\n      ctx.fillRect(x + 1, y + 1, cw &#8211; 2, CH &#8211; 2);<br \/>\n      ctx.fillStyle = fg;<br \/>\n      ctx.fillText(val, x + cw \/ 2, y + CH \/ 2);<br \/>\n    }<br \/>\n  }<\/p>\n<p>  _reachCanvas = canvas;<br \/>\n  _reachData   = { sorted, target, draw };<br \/>\n  draw();<\/p>\n<p>  \/\/ Redessine si la fen\u00eatre change de taille<br \/>\n  const ro = new ResizeObserver(() =&gt; draw());<br \/>\n  ro.observe(wrap);<br \/>\n}<\/p>\n<p>function openDetail() {<br \/>\n  if (!lastSolver) return;<br \/>\n  const solver = lastSolver;<br \/>\n  document.getElementById(&#8216;detail-title&#8217;).textContent =<br \/>\n    `Plaques : ${state.plaques.join(&#8216;  &#8216;)}   |   Cible : ${state.target}`;<\/p>\n<p>  \/\/ \u2500\u2500 Tab r\u00e9sultats \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\n  const sorted = [&#8230;solver.reachable].sort((a, b) =&gt; a &#8211; b);<br \/>\n  const attein = solver.reachable.has(state.target);<br \/>\n  document.getElementById(&#8216;reach-info&#8217;).textContent =<br \/>\n    `${sorted.length} r\u00e9sultats  |  Cible ${state.target} : ${attein ? &#8216;\u2713 Atteignable&#8217; : &#8216;\u2717 Non atteignable&#8217;}`;<br \/>\n  document.getElementById(&#8216;reach-info&#8217;).style.color = attein ? &#8216;var(&#8211;green)&#8217; : &#8216;var(&#8211;accent)&#8217;;<br \/>\n  \/\/ Canvas rendu apr\u00e8s que la modale soit visible (dimensions correctes)<br \/>\n  requestAnimationFrame(() =&gt; _buildReachCanvas(sorted, state.target));<\/p>\n<p>  \/\/ \u2500\u2500 Tab solutions exactes (virtualis\u00e9 : 1 div par solution, pas par cellule) \u2500\u2500<br \/>\n  const solsList = document.getElementById(&#8216;sols-list&#8217;);<br \/>\n  solsList.innerHTML = &#8221;;<br \/>\n  if (!solver.allSols.length) {<br \/>\n    solsList.innerHTML = &#8216;&lt;p style=&#8221;color:var(&#8211;text-dim);padding:16px&#8221;&gt;Aucune solution exacte.&lt;\/p&gt;&#8217;;<br \/>\n  } else {<br \/>\n    const sorted2 = [&#8230;solver.allSols].sort(<br \/>\n      (a, b) =&gt; a.depth &#8211; b.depth || a.steps.join().localeCompare(b.steps.join()));<br \/>\n    \/\/ Fragment pour batch-insert<br \/>\n    const frag = document.createDocumentFragment();<br \/>\n    sorted2.forEach((sol, i) =&gt; {<br \/>\n      const div = document.createElement(&#8216;div&#8217;);<br \/>\n      div.className = &#8216;sol-item&#8217;;<br \/>\n      div.innerHTML =<br \/>\n        `&lt;span style=&#8221;color:var(&#8211;text-dim);font-size:.8rem&#8221;&gt;[${i+1}] ${sol.depth} \u00e9tape(s)&lt;\/span&gt;&lt;br&gt;` +<br \/>\n        `&lt;span class=&#8221;sol-preview&#8221;&gt;${sol.steps.join(&#8216;  \u2192  &#8216;)}&lt;\/span&gt;`;<br \/>\n      \/\/ Toggle d\u00e9tail inline<br \/>\n      div.onclick = () =&gt; {<br \/>\n        const open = div.dataset.open === &#8216;1&#8217;;<br \/>\n        div.dataset.open = open ? &#8216;0&#8217; : &#8216;1&#8217;;<br \/>\n        div.querySelector(&#8216;.sol-detail&#8217;)?.remove();<br \/>\n        if (!open) {<br \/>\n          const detail = document.createElement(&#8216;div&#8217;);<br \/>\n          detail.className = &#8216;sol-detail&#8217;;<br \/>\n          detail.style.cssText = &#8216;margin-top:6px;padding:6px;background:var(&#8211;bg-hdr);border-radius:4px;&#8217;;<br \/>\n          detail.innerHTML = sol.steps.map(s =&gt;<br \/>\n            `&lt;div style=&#8221;color:var(&#8211;green);font-size:.85rem&#8221;&gt;${s}&lt;\/div&gt;`).join(&#8221;);<br \/>\n          div.appendChild(detail);<br \/>\n        }<br \/>\n      };<br \/>\n      frag.appendChild(div);<br \/>\n    });<br \/>\n    solsList.appendChild(frag);<br \/>\n  }<\/p>\n<p>  \/\/ \u2500\u2500 Tab meilleure \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\n  const bestDiv = document.getElementById(&#8216;best-content&#8217;);<br \/>\n  bestDiv.innerHTML = &#8221;;<br \/>\n  if (solver.best) {<br \/>\n    bestDiv.innerHTML =<br \/>\n      `&lt;div style=&#8221;font-size:1.1rem;font-weight:700;margin-bottom:12px;color:${solver.best.diff === 0 ? &#8216;var(&#8211;green)&#8217; : &#8216;var(&#8211;accent2)&#8217;}&#8221;&gt;` +<br \/>\n        (solver.best.diff === 0 ? &#8216;Compte exact !&#8217; : `\u00c9cart : ${solver.best.diff}`) +<br \/>\n      `&lt;\/div&gt;` +<br \/>\n      solver.best.steps.map(s =&gt; `&lt;div style=&#8221;margin:4px 0;font-size:1.05rem&#8221;&gt;${s}&lt;\/div&gt;`).join(&#8221;) +<br \/>\n      `&lt;div style=&#8221;margin-top:14px;font-size:.8rem;color:var(&#8211;text-dim)&#8221;&gt;${solver._ops} op\u00e9rations test\u00e9es&lt;\/div&gt;`;<br \/>\n  }<\/p>\n<p>  showTab(&#8216;tab-reach&#8217;);<br \/>\n  document.getElementById(&#8216;modal-detail&#8217;).classList.add(&#8216;open&#8217;);<br \/>\n}<\/p>\n<p>function showTab(id) {<br \/>\n  document.querySelectorAll(&#8216;.tab-content&#8217;).forEach(t =&gt; t.classList.remove(&#8216;active&#8217;));<br \/>\n  document.querySelectorAll(&#8216;.tab-btn&#8217;).forEach(b =&gt; b.classList.remove(&#8216;active&#8217;));<br \/>\n  document.getElementById(id).classList.add(&#8216;active&#8217;);<br \/>\n  const idx = [&#8216;tab-reach&#8217;,&#8217;tab-sols&#8217;,&#8217;tab-best&#8217;].indexOf(id);<br \/>\n  document.querySelectorAll(&#8216;.tab-btn&#8217;)[idx].classList.add(&#8216;active&#8217;);<br \/>\n}<\/p>\n<p>\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\n\/\/  PWA \u2014 manifest inline + service worker<br \/>\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\nfunction initPWA() {<br \/>\n  \/\/ Manifest inject\u00e9 en data-URI pour rester monofichier<br \/>\n  const manifest = {<br \/>\n    name: &#8216;Le Compte Est Bon&#8217;,<br \/>\n    short_name: &#8216;CEB&#8217;,<br \/>\n    description: &#8216;Le c\u00e9l\u00e8bre jeu de calcul mental&#8217;,<br \/>\n    start_url: &#8216;.\/&#8217;,<br \/>\n    display: &#8216;standalone&#8217;,<br \/>\n    background_color: &#8216;#1e3a52&#8217;,<br \/>\n    theme_color: &#8216;#1e3a52&#8217;,<br \/>\n    icons: [{<br \/>\n      src: &#8220;data:image\/svg+xml,%3Csvg xmlns=&#8217;http:\/\/www.w3.org\/2000\/svg&#8217; viewBox=&#8217;0 0 192 192&#8217;%3E%3Crect width=&#8217;192&#8242; height=&#8217;192&#8242; rx=&#8217;32&#8217; fill=&#8217;%231e3a52&#8217;\/%3E%3Ctext x=&#8217;96&#8217; y=&#8217;130&#8242; font-size=&#8217;110&#8242; text-anchor=&#8217;middle&#8217; font-family=&#8217;serif&#8217; fill=&#8217;%23f5a623&#8217;%3E%3D%3C\/text%3E%3C\/svg%3E&#8221;,<br \/>\n      sizes: &#8216;192&#215;192&#8217;, type: &#8216;image\/svg+xml&#8217;, purpose: &#8216;any maskable&#8217;,<br \/>\n    }],<br \/>\n  };<br \/>\n  const blob = new Blob([JSON.stringify(manifest)], {type: &#8216;application\/json&#8217;});<br \/>\n  document.getElementById(&#8216;pwa-manifest&#8217;).href = URL.createObjectURL(blob);<\/p>\n<p>  \/\/ Service Worker inline (cache-first)<br \/>\n  if (&#8216;serviceWorker&#8217; in navigator) {<br \/>\n    const swCode = `<br \/>\nconst CACHE = &#8216;ceb-v1&#8217;;<br \/>\nself.addEventListener(&#8216;install&#8217;, e =&gt;<br \/>\n  e.waitUntil(caches.open(CACHE).then(c =&gt; c.add(&#8216;\/&#8217;)))<br \/>\n);<br \/>\nself.addEventListener(&#8216;fetch&#8217;, e =&gt;<br \/>\n  e.respondWith(caches.match(e.request).then(r =&gt; r || fetch(e.request)))<br \/>\n);<br \/>\n    `;<br \/>\n    const swBlob = new Blob([swCode], {type: &#8216;application\/javascript&#8217;});<br \/>\n    navigator.serviceWorker.register(URL.createObjectURL(swBlob))<br \/>\n      .catch(() =&gt; {}); \/\/ silencieux si file:\/\/ ou CSP bloque<br \/>\n  }<br \/>\n}<\/p>\n<p>\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\n\/\/  STATISTIQUES \u2014 localStorage<br \/>\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\nconst STATS_KEY = &#8216;ceb_stats_v1&#8217;;<\/p>\n<p>function _emptyStats() {<br \/>\n  return {<br \/>\n    global: {<br \/>\n      partiesJouees: 0, tiragesJoues: 0, scoreTotal: 0,<br \/>\n      exacts: 0, approches: 0, rates: 0, tempsTotal: 0,<br \/>\n    },<br \/>\n    parGraine: {},   \/\/ { &#8220;42&#8221;: {meilleurScore, parties, date} }<br \/>\n    parMode:   {},   \/\/ { &#8220;ceb24&#8221;: {tirages, exacts, approches, rates} }<br \/>\n    parNbPlq:  {},   \/\/ { &#8220;6&#8221;: {tirages, exacts} }<br \/>\n    historique: [],  \/\/ derni\u00e8res 100 parties<br \/>\n  };<br \/>\n}<\/p>\n<p>function loadStats() {<br \/>\n  try {<br \/>\n    const raw = localStorage.getItem(STATS_KEY);<br \/>\n    return raw ? { &#8230;_emptyStats(), &#8230;JSON.parse(raw) } : _emptyStats();<br \/>\n  } catch { return _emptyStats(); }<br \/>\n}<\/p>\n<p>function saveStats(st) {<br \/>\n  try { localStorage.setItem(STATS_KEY, JSON.stringify(st)); } catch {}<br \/>\n}<\/p>\n<p>function recordPartie() {<br \/>\n  const h   = state.history;<br \/>\n  if (!h.length) return;<br \/>\n  const st  = loadStats();<br \/>\n  const tot = state.score;<br \/>\n  const n   = h.length;<br \/>\n  const exacts   = h.filter(e =&gt; e.diff === 0).length;<br \/>\n  const approches= h.filter(e =&gt; e.diff &gt; 0 &amp;&amp; e.diff &lt;= 3).length;<br \/>\n  const rates    = h.filter(e =&gt; e.diff &gt; 3).length;<br \/>\n  const tTemps   = h.reduce((s, e) =&gt; s + e.elapsed, 0);<br \/>\n  const mode     = cfg.tirageMode;<br \/>\n  const nbp      = String(cfg.nbPlaques);<br \/>\n  const graine   = String(cfg.partieNum);<br \/>\n  const date     = new Date().toLocaleDateString(&#8216;fr-FR&#8217;);<\/p>\n<p>  \/\/ Global<br \/>\n  st.global.partiesJouees++;<br \/>\n  st.global.tiragesJoues  += n;<br \/>\n  st.global.scoreTotal    += tot;<br \/>\n  st.global.exacts        += exacts;<br \/>\n  st.global.approches     += approches;<br \/>\n  st.global.rates         += rates;<br \/>\n  st.global.tempsTotal    += tTemps;<\/p>\n<p>  \/\/ Par graine<br \/>\n  if (cfg.partieNum &gt; 0) {<br \/>\n    const g = st.parGraine[graine] || { meilleurScore: -Infinity, parties: 0, date };<br \/>\n    g.parties++;<br \/>\n    g.date = date;<br \/>\n    if (tot &gt; g.meilleurScore) g.meilleurScore = tot;<br \/>\n    st.parGraine[graine] = g;<br \/>\n  }<\/p>\n<p>  \/\/ Par mode<br \/>\n  const m = st.parMode[mode] || { tirages: 0, exacts: 0, approches: 0, rates: 0 };<br \/>\n  m.tirages   += n;<br \/>\n  m.exacts    += exacts;<br \/>\n  m.approches += approches;<br \/>\n  m.rates     += rates;<br \/>\n  st.parMode[mode] = m;<\/p>\n<p>  \/\/ Par nb plaques<br \/>\n  const p = st.parNbPlq[nbp] || { tirages: 0, exacts: 0 };<br \/>\n  p.tirages += n;<br \/>\n  p.exacts  += exacts;<br \/>\n  st.parNbPlq[nbp] = p;<\/p>\n<p>  \/\/ Historique (max 100 parties)<br \/>\n  st.historique.unshift({<br \/>\n    graine: cfg.partieNum, mode, nbPlq: cfg.nbPlaques,<br \/>\n    nbTirages: n, score: tot, exacts, approches, rates, date,<br \/>\n  });<br \/>\n  if (st.historique.length &gt; 100) st.historique.length = 100;<\/p>\n<p>  saveStats(st);<br \/>\n}<\/p>\n<p>function resetStats() {<br \/>\n  if (!confirm(&#8216;Effacer toutes les statistiques ?&#8217;)) return;<br \/>\n  localStorage.removeItem(STATS_KEY);<br \/>\n  renderStatsGlobal(_emptyStats());<br \/>\n  renderStatsParties(_emptyStats());<br \/>\n  renderStatsModes(_emptyStats());<br \/>\n}<\/p>\n<p>\/\/ \u2500\u2500 Rendu \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\nfunction _pct(a, b) { return b ? (a \/ b * 100).toFixed(1) + &#8216;%&#8217; : &#8216;\u2014&#8217;; }<br \/>\nfunction _avg(a, b) { return b ? (a \/ b).toFixed(1) : &#8216;\u2014&#8217;; }<\/p>\n<p>function _statRow(label, val, color) {<br \/>\n  return `&lt;div style=&#8221;display:flex;justify-content:space-between;padding:6px 12px;<br \/>\n    border-bottom:1px solid var(&#8211;border);font-size:.9rem;&#8221;&gt;<br \/>\n    &lt;span style=&#8221;color:var(&#8211;text-med)&#8221;&gt;${label}&lt;\/span&gt;<br \/>\n    &lt;span style=&#8221;font-weight:700;color:${color||&#8217;var(&#8211;text)&#8217;};&#8221;&gt;${val}&lt;\/span&gt;<br \/>\n  &lt;\/div&gt;`;<br \/>\n}<\/p>\n<p>function _sectionTitle(t) {<br \/>\n  return `&lt;div style=&#8221;font-size:.75rem;font-weight:700;color:var(&#8211;accent2);<br \/>\n    padding:10px 12px 4px;letter-spacing:1px;&#8221;&gt;${t}&lt;\/div&gt;`;<br \/>\n}<\/p>\n<p>function renderStatsGlobal(st) {<br \/>\n  const g = st.global;<br \/>\n  const div = document.getElementById(&#8216;st-global&#8217;);<br \/>\n  const moy = g.partiesJouees ? (g.scoreTotal \/ g.partiesJouees).toFixed(1) : &#8216;\u2014&#8217;;<br \/>\n  div.innerHTML =<br \/>\n    _sectionTitle(&#8216;R\u00c9SULTATS&#8217;) +<br \/>\n    _statRow(&#8216;Parties jou\u00e9es&#8217;,    g.partiesJouees) +<br \/>\n    _statRow(&#8216;Tirages jou\u00e9s&#8217;,     g.tiragesJoues) +<br \/>\n    _statRow(&#8216;Score total&#8217;,       (g.scoreTotal &gt;= 0 ? &#8216;+&#8217; : &#8221;) + g.scoreTotal,<br \/>\n             g.scoreTotal &gt;= 0 ? &#8216;var(&#8211;green)&#8217; : &#8216;var(&#8211;accent)&#8217;) +<br \/>\n    _statRow(&#8216;Score moy \/ partie&#8217;, moy) +<br \/>\n    _statRow(&#8216;Score moy \/ tirage&#8217;, _avg(g.scoreTotal, g.tiragesJoues)) +<br \/>\n    _sectionTitle(&#8216;PR\u00c9CISION&#8217;) +<br \/>\n    _statRow(&#8216;Comptes exacts&#8217;,   `${g.exacts}  (${_pct(g.exacts, g.tiragesJoues)})`,  &#8216;var(&#8211;green)&#8217;) +<br \/>\n    _statRow(&#8216;Comptes approch\u00e9s&#8217;,`${g.approches}  (${_pct(g.approches, g.tiragesJoues)})`, &#8216;var(&#8211;accent2)&#8217;) +<br \/>\n    _statRow(&#8216;Rat\u00e9s (&gt;3)&#8217;,       `${g.rates}  (${_pct(g.rates, g.tiragesJoues)})`,    &#8216;var(&#8211;accent)&#8217;) +<br \/>\n    _sectionTitle(&#8216;TEMPS&#8217;) +<br \/>\n    _statRow(&#8216;Temps moyen \/ tirage&#8217;, _avg(g.tempsTotal, g.tiragesJoues) + &#8216; s&#8217;) +<br \/>\n    _statRow(&#8216;Temps total jou\u00e9&#8217;, Math.round(g.tempsTotal \/ 60) + &#8216; min&#8217;);<br \/>\n}<\/p>\n<p>function renderStatsParties(st) {<br \/>\n  const div = document.getElementById(&#8216;st-parties-list&#8217;);<br \/>\n  if (!st.historique.length) {<br \/>\n    div.innerHTML = &#8216;&lt;p style=&#8221;color:var(&#8211;text-dim);padding:16px&#8221;&gt;Aucune partie enregistr\u00e9e.&lt;\/p&gt;&#8217;;<br \/>\n    return;<br \/>\n  }<br \/>\n  const frag = document.createDocumentFragment();<br \/>\n  st.historique.forEach((p, i) =&gt; {<br \/>\n    const row = document.createElement(&#8216;div&#8217;);<br \/>\n    row.style.cssText = &#8216;display:grid;grid-template-columns:24px 60px 60px 1fr 48px 48px 48px 48px;&#8217; +<br \/>\n      &#8216;gap:4px;align-items:center;padding:5px 8px;border-bottom:1px solid var(&#8211;border);font-size:.78rem;&#8217;;<br \/>\n    row.innerHTML =<br \/>\n      `&lt;span style=&#8221;color:var(&#8211;text-dim)&#8221;&gt;${i+1}&lt;\/span&gt;` +<br \/>\n      `&lt;span style=&#8221;color:var(&#8211;text-dim)&#8221;&gt;${p.date}&lt;\/span&gt;` +<br \/>\n      `&lt;span style=&#8221;color:var(&#8211;text-med)&#8221;&gt;${p.graine || &#8216;al\u00e9a&#8217;}&lt;\/span&gt;` +<br \/>\n      `&lt;span style=&#8221;color:var(&#8211;text-med)&#8221;&gt;${p.mode} ${p.nbPlq}\u265f \u00d7${p.nbTirages}&lt;\/span&gt;` +<br \/>\n      `&lt;span style=&#8221;font-weight:700;color:${p.score&gt;=0?&#8217;var(&#8211;green)&#8217;:&#8217;var(&#8211;accent)&#8217;};&#8221;&gt;${p.score&gt;=0?&#8217;+&#8217;:&#8221;}${p.score}&lt;\/span&gt;` +<br \/>\n      `&lt;span style=&#8221;color:var(&#8211;green)&#8221;&gt;\u2713${p.exacts}&lt;\/span&gt;` +<br \/>\n      `&lt;span style=&#8221;color:var(&#8211;accent2)&#8221;&gt;~${p.approches}&lt;\/span&gt;` +<br \/>\n      `&lt;span style=&#8221;color:var(&#8211;accent)&#8221;&gt;\u2717${p.rates}&lt;\/span&gt;`;<br \/>\n    frag.appendChild(row);<br \/>\n  });<br \/>\n  \/\/ En-t\u00eate<br \/>\n  const hdr = document.createElement(&#8216;div&#8217;);<br \/>\n  hdr.style.cssText = &#8216;display:grid;grid-template-columns:24px 60px 60px 1fr 48px 48px 48px 48px;&#8217; +<br \/>\n    &#8216;gap:4px;padding:4px 8px;font-size:.7rem;color:var(&#8211;text-dim);border-bottom:2px solid var(&#8211;border);&#8217;;<br \/>\n  hdr.innerHTML = &#8216;&lt;span&gt;#&lt;\/span&gt;&lt;span&gt;Date&lt;\/span&gt;&lt;span&gt;Graine&lt;\/span&gt;&lt;span&gt;Mode&lt;\/span&gt;&#8217; +<br \/>\n    &#8216;&lt;span&gt;Score&lt;\/span&gt;&lt;span&gt;\u2713&lt;\/span&gt;&lt;span&gt;~&lt;\/span&gt;&lt;span&gt;\u2717&lt;\/span&gt;&#8217;;<br \/>\n  div.innerHTML = &#8221;;<br \/>\n  div.appendChild(hdr);<br \/>\n  div.appendChild(frag);<br \/>\n}<\/p>\n<p>function renderStatsModes(st) {<br \/>\n  const div = document.getElementById(&#8216;st-modes&#8217;);<br \/>\n  let html = _sectionTitle(&#8216;PAR MODE DE TIRAGE&#8217;);<br \/>\n  const modes = { ceb24: &#8216;CEB 24&#8217;, ceb28: &#8216;CEB 28&#8217;, alea20: &#8216;Al\u00e9atoire 20&#8217; };<br \/>\n  for (const [key, label] of Object.entries(modes)) {<br \/>\n    const m = st.parMode[key];<br \/>\n    if (!m || !m.tirages) continue;<br \/>\n    html += `&lt;div style=&#8221;padding:4px 12px 2px;font-size:.85rem;color:var(&#8211;text);font-weight:700&#8243;&gt;${label}&lt;\/div&gt;`;<br \/>\n    html += _statRow(&#8216;Tirages&#8217;, m.tirages);<br \/>\n    html += _statRow(&#8216;Exacts&#8217;,  `${m.exacts} (${_pct(m.exacts, m.tirages)})`, &#8216;var(&#8211;green)&#8217;);<br \/>\n    html += _statRow(&#8216;Approch\u00e9s&#8217;, `${m.approches} (${_pct(m.approches, m.tirages)})`, &#8216;var(&#8211;accent2)&#8217;);<br \/>\n  }<br \/>\n  html += _sectionTitle(&#8216;PAR NOMBRE DE PLAQUES&#8217;);<br \/>\n  for (const [nbp, p] of Object.entries(st.parNbPlq).sort((a,b)=&gt;+a[0]-+b[0])) {<br \/>\n    html += _statRow(`${nbp} plaques`, `${p.tirages} tirages  \u2014  ${_pct(p.exacts, p.tirages)} exacts`);<br \/>\n  }<br \/>\n  html += _sectionTitle(&#8216;PAR GRAINE (top 10 meilleures)&#8217;);<br \/>\n  const graines = Object.entries(st.parGraine)<br \/>\n    .sort((a,b) =&gt; b[1].meilleurScore &#8211; a[1].meilleurScore).slice(0, 10);<br \/>\n  for (const [g, d] of graines) {<br \/>\n    html += _statRow(`Graine ${g}`,<br \/>\n      `Meilleur : ${d.meilleurScore &gt;= 0 ? &#8216;+&#8217; : &#8221;}${d.meilleurScore}  (${d.parties} partie(s))`,<br \/>\n      &#8216;var(&#8211;accent2)&#8217;);<br \/>\n  }<br \/>\n  if (!graines.length) html += &#8216;&lt;p style=&#8221;color:var(&#8211;text-dim);padding:12px&#8221;&gt;Aucune partie avec graine fixe.&lt;\/p&gt;&#8217;;<br \/>\n  div.innerHTML = html;<br \/>\n}<\/p>\n<p>function showStatsTab(id) {<br \/>\n  document.querySelectorAll(&#8216;#modal-stats .tab-content&#8217;).forEach(t =&gt; t.classList.remove(&#8216;active&#8217;));<br \/>\n  document.querySelectorAll(&#8216;#modal-stats .tab-btn&#8217;).forEach(b =&gt; b.classList.remove(&#8216;active&#8217;));<br \/>\n  document.getElementById(id).classList.add(&#8216;active&#8217;);<br \/>\n  const idx = [&#8216;st-global&#8217;,&#8217;st-parties&#8217;,&#8217;st-modes&#8217;].indexOf(id);<br \/>\n  document.querySelectorAll(&#8216;#modal-stats .tab-btn&#8217;)[idx].classList.add(&#8216;active&#8217;);<br \/>\n}<\/p>\n<p>function openStats() {<br \/>\n  const st = loadStats();<br \/>\n  renderStatsGlobal(st);<br \/>\n  renderStatsParties(st);<br \/>\n  renderStatsModes(st);<br \/>\n  showStatsTab(&#8216;st-global&#8217;);<br \/>\n  document.getElementById(&#8216;modal-stats&#8217;).classList.add(&#8216;open&#8217;);<br \/>\n}<\/p>\n<p>\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\n\/\/  PERSISTANCE SESSION \u2014 localStorage<br \/>\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550<br \/>\nconst SESSION_KEY = &#8216;ceb_session_v1&#8217;;<\/p>\n<p>function saveSession() {<br \/>\n  if (cfg.entrainement) return; \/\/ pas de persistance en mode entra\u00eenement<br \/>\n  try {<br \/>\n    const session = {<br \/>\n      cfg: { &#8230;cfg },<br \/>\n      state: {<br \/>\n        plaques:   state.plaques,<br \/>\n        target:    state.target,<br \/>\n        score:     state.score,<br \/>\n        roundNum:  state.roundNum,<br \/>\n        validated: state.validated,<br \/>\n        history:   state.history,<br \/>\n      },<br \/>\n      rngNext:     rng ? rng._next.toString() : null,<br \/>\n      plaqueVals,<br \/>\n      plaqueActive,<br \/>\n      calcSteps,<br \/>\n    };<br \/>\n    localStorage.setItem(SESSION_KEY, JSON.stringify(session));<br \/>\n  } catch {}<br \/>\n}<\/p>\n<p>function clearSession() {<br \/>\n  try { localStorage.removeItem(SESSION_KEY); } catch {}<br \/>\n}<\/p>\n<p>function restoreSession() {<br \/>\n  try {<br \/>\n    const raw = localStorage.getItem(SESSION_KEY);<br \/>\n    if (!raw) return false;<br \/>\n    const s = JSON.parse(raw);<br \/>\n    if (!s.state || !s.cfg) return false;<\/p>\n<p>    \/\/ Restaure cfg<br \/>\n    Object.assign(cfg, s.cfg);<\/p>\n<p>    \/\/ Restaure state<br \/>\n    state.plaques   = s.state.plaques   || [];<br \/>\n    state.target    = s.state.target    || 0;<br \/>\n    state.score     = s.state.score     || 0;<br \/>\n    state.roundNum  = s.state.roundNum  || 0;<br \/>\n    state.validated = s.state.validated ?? true;<br \/>\n    state.history   = s.state.history   || [];<br \/>\n    state.startTime = Date.now(); \/\/ repart du moment du refresh<\/p>\n<p>    \/\/ Restaure RNG<br \/>\n    if (s.rngNext) {<br \/>\n      rng = new HP11CRandom(0);<br \/>\n      rng._next = BigInt(s.rngNext);<br \/>\n    }<\/p>\n<p>    \/\/ Restaure UI state<br \/>\n    plaqueVals   = s.plaqueVals   || [];<br \/>\n    plaqueActive = s.plaqueActive || [];<br \/>\n    calcSteps    = s.calcSteps    || [];<\/p>\n<p>    return state.plaques.length &gt; 0;<br \/>\n  } catch { return false; }<br \/>\n}<\/p>\n<p>function _restoreUI() {<br \/>\n  \/\/ Reconstruit l&#8217;\u00e9cran depuis le state restaur\u00e9<br \/>\n  document.getElementById(&#8216;entrainement-zone&#8217;).style.display = &#8216;none&#8217;;<br \/>\n  document.getElementById(&#8216;plq-title&#8217;).textContent = &#8216;Plaques disponibles&#8217;;<br \/>\n  document.getElementById(&#8216;lbl-cible&#8217;).textContent = state.target;<br \/>\n  document.getElementById(&#8216;lbl-round&#8217;).textContent = `Tirage ${state.roundNum} \/ ${cfg.nbTirages}`;<br \/>\n  document.getElementById(&#8216;lbl-mode&#8217;).textContent  = cfg.tirageMode.toUpperCase() + (cfg.exactOnly ? &#8216; \u2713&#8217; : &#8221;);<br \/>\n  document.getElementById(&#8216;lbl-score&#8217;).textContent = state.score;<br \/>\n  document.getElementById(&#8216;btn-resoudre&#8217;).disabled = !state.validated;<br \/>\n  document.getElementById(&#8216;btn-detail&#8217;).disabled   = true;<\/p>\n<p>  renderPlaques();<\/p>\n<p>  \/\/ Remet les boutons r\u00e9sultats dans la grille<br \/>\n  const n = state.plaques.length;<br \/>\n  for (let i = n; i &lt; plaqueVals.length; i++) {<br \/>\n    _addResultBtn(i, plaqueVals[i]);<br \/>\n    \/\/ Remet l&#8217;\u00e9tat used sur les plaques sources<br \/>\n  }<br \/>\n  \/\/ Remet l&#8217;\u00e9tat used sur les boutons plaques initiales<br \/>\n  for (let i = 0; i &lt; n; i++) {<br \/>\n    if (!plaqueActive[i] &amp;&amp; plaqueBtns[i]) {<br \/>\n      plaqueBtns[i].disabled = true;<br \/>\n      plaqueBtns[i].classList.add(&#8216;used&#8217;);<br \/>\n    }<br \/>\n  }<\/p>\n<p>  refreshHist();<br \/>\n  resetPhase();<br \/>\n  updateNouveauBtn();<\/p>\n<p>  if (!state.validated) {<br \/>\n    startTimer();<br \/>\n    setStatus(&#8216;Partie restaur\u00e9e \u2014 continue !&#8217;);<br \/>\n  } else {<br \/>\n    document.getElementById(&#8216;lbl-timer&#8217;).textContent = &#8216;\u2014&#8217;;<br \/>\n    setStatus(&#8216;Partie restaur\u00e9e \u2014 valide ou lance un nouveau tirage.&#8217;);<br \/>\n  }<br \/>\n}<br \/>\nwindow.onload = () =&gt; {<br \/>\n  initPWA();<br \/>\n  if (restoreSession()) {<br \/>\n    _restoreUI();<br \/>\n  } else {<br \/>\n    newGame();<br \/>\n  }<br \/>\n};<br \/>\n&lt;\/script&gt;<\/p>\n<p>&lt;\/body&gt;<br \/>\n&lt;\/html&gt;<\/p>\n\n\n\n<p>Fin<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Jouer et r\u00e9soudre Ecrit au d\u00e9part en ABAP sous SAP, Migr\u00e9 \u00e0 l&#8217;aide de Claude en Python puis HTML5 Test 2 sa m\u00e8re ! Fin<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-189","page","type-page","status-publish","hentry"],"jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/dzo.ovh\/index.php?rest_route=\/wp\/v2\/pages\/189","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dzo.ovh\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/dzo.ovh\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/dzo.ovh\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dzo.ovh\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=189"}],"version-history":[{"count":7,"href":"https:\/\/dzo.ovh\/index.php?rest_route=\/wp\/v2\/pages\/189\/revisions"}],"predecessor-version":[{"id":201,"href":"https:\/\/dzo.ovh\/index.php?rest_route=\/wp\/v2\/pages\/189\/revisions\/201"}],"wp:attachment":[{"href":"https:\/\/dzo.ovh\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=189"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}