diff --git a/.gitattributes b/.gitattributes index 461405f4..641ba134 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,12 @@ *.dat.gz filter=lfs diff=lfs merge=lfs -text *.nnue filter=lfs diff=lfs merge=lfs -text + +lib/engine/stockfish-dev/*.nnue -filter -diff -merge -text +lib/engine/stockfish-18-small/*.nnue -filter -diff -merge -text +lib/engine/stockfish-18/*.nnue -filter -diff -merge -text +lib/engine/stockfish-17-79/*.nnue -filter -diff -merge -text +lib/engine/fairy-stockfish-14/nnue/*.nnue -filter -diff -merge -text + +# README screenshots: they belong on GitHub, not inside the installed extension -- +# keep them out of the release zip (git archive honours export-ignore). +docs/ export-ignore diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 00000000..47123612 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,49 @@ +name: Something is broken +description: A bug, a site that stopped working, an engine misbehaving — no diagnosis needed. +labels: [bug] +body: + - type: markdown + attributes: + value: | + You don't need to know what caused it. Several of the worst bugs here were silent rather + than loud — autoplay skipping a move with nothing logged, an engine that never loaded — so + "it did nothing" is a real report, not a vague one. + - type: textarea + id: what + attributes: + label: What happened? + placeholder: It stopped playing moves on lichess this morning. Panel showed a move, nothing clicked. + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Shown at the top of the extension's settings page. + placeholder: "3.1.200" + - type: input + id: where + attributes: + label: Site and browser + placeholder: chess.com, Chrome 141 on Windows 11 + - type: dropdown + id: windows-4pc + attributes: + label: Is this the Windows four-player setup? + description: > + If so, which stage failed? Native-messaging failures on Windows are silent — Chrome reports + the host as unavailable and says nothing about why — so the stage is most of the diagnosis. + options: + - "No, not four-player on Windows" + - "1. setup.sh does not produce build/tetrarch.dll" + - "2. `from tetrarch import core` raises" + - "3. `python uci.py` does not answer `go` in a terminal" + - "4. Engine works in a terminal, but the panel cannot find it" + - type: textarea + id: console + attributes: + label: Anything in the console? + description: > + Optional. Right-click the page → Inspect → Console, and for the worker, chrome://extensions + → Mephisto → "service worker". Paste whatever looks relevant. + render: text diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..8e1dd409 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,3 @@ +# Freeform issues stay enabled on purpose: "it did nothing this morning" is a useful report, and a +# required-fields form is a good way to lose it. +blank_issues_enabled: true diff --git a/.github/ISSUE_TEMPLATE/idea.yml b/.github/ISSUE_TEMPLATE/idea.yml new file mode 100644 index 00000000..f89f4fea --- /dev/null +++ b/.github/ISSUE_TEMPLATE/idea.yml @@ -0,0 +1,14 @@ +name: Idea or request +description: A feature, a site to support, an engine to add, or just a thought. +labels: [idea] +body: + - type: markdown + attributes: + value: | + Most of what's in here exists because something was annoying in a real game. Half-formed is fine. + - type: textarea + id: idea + attributes: + label: What would you like? + validations: + required: true diff --git a/.gitignore b/.gitignore index 090a1f02..5424001c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .idea .DS_Store + +__pycache__/ +*.pyc diff --git a/LICENSING.md b/LICENSING.md new file mode 100644 index 00000000..829f52e7 --- /dev/null +++ b/LICENSING.md @@ -0,0 +1,56 @@ +# Licensing + +This project bundles third-party engines, neural networks, and libraries under several +licenses. Because some of those components are copyleft, the licensing of the project as a +**whole distribution** is not simply the MIT license in [`LICENSE`](LICENSE). Please read this +file before redistributing. + +## Short version + +- The **original Mephisto source code** (by Alexandru Petrusca) and this fork's own source-code + changes are made available under the **MIT License** — see [`LICENSE`](LICENSE). +- However, the project **bundles and distributes** components licensed under **GPL-3.0** + (Stockfish, Fairy-Stockfish, and their NNUE nets; the Maia-1 nets and the Maia 2200 net) and + under **AGPL-3.0** (the Maia-3 model, and this project's Maia-3 encoder which is derived from + the Maia-3 source). +- Because AGPL-3.0 is the strongest copyleft here and it is one-way compatible with GPL-3.0, the + **combined work as distributed (the released .zip / the installed extension) is governed by the + GNU Affero General Public License, version 3** — see [`licenses/AGPL-3.0.txt`](licenses/AGPL-3.0.txt). + The GPL-3.0 text is included at [`licenses/GPL-3.0.txt`](licenses/GPL-3.0.txt). + +MIT-licensed code stays MIT-licensed (MIT is compatible with, and may be included in, an +AGPL/GPL work); the copyleft obligations attach to the combined distribution. + +## What that means if you redistribute + +If you distribute this project (or a modified version), the AGPL-3.0 / GPL-3.0 terms require you to: + +1. **Keep the license texts and notices.** Ship `LICENSE`, `LICENSING.md`, `THIRD-PARTY-NOTICES.md`, + and `licenses/` intact, and preserve the copyright/permission headers inside the bundled files. +2. **License your combined work under AGPL-3.0** and make **complete corresponding source** available + to the people you distribute to (this repository is that source for the JavaScript; see + `THIRD-PARTY-NOTICES.md` for where to obtain the source of the compiled engine binaries and of the + neural-network models). +3. **Do not add further restrictions** beyond those the licenses allow. + +## Corresponding source + +- This Git repository **is** the corresponding source for all of the project's own JavaScript/HTML/CSS. +- The **Stockfish** and **Fairy-Stockfish** engines are shipped as pre-compiled WebAssembly. Their + corresponding C++ source, and the source used to produce the WebAssembly builds, are available from + the upstream projects listed in `THIRD-PARTY-NOTICES.md`. +- The **Maia** neural networks (Maia-1, Maia 2200, Maia-3) are trained models. Their training code, + and where applicable the model weights in their original form, are available from the upstream + projects listed in `THIRD-PARTY-NOTICES.md`. + +## Note on AGPL-3.0 §13 (network use) + +The Maia-3 component is AGPL-3.0, whose §13 requires that users **interacting with a modified version +over a network** be offered its source. In this project Maia-3 runs **locally in the user's own +browser** (an offscreen document); it is not offered as a network service. §13 is therefore not +expected to be triggered by ordinary use of this extension. The ordinary copyleft obligations above +(source-on-distribution, license text, no added restrictions) still apply because the model and its +derived code are distributed. + +*This file is a good-faith summary, not legal advice. If in doubt, consult a lawyer or the individual +upstream licenses.* diff --git a/README.md b/README.md index f3ed6f45..53380074 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,771 @@ - + -Mephisto is a browser extension that provides next-best-move analysis and can automate gameplay on Chess.com and Lichess. +**Real-time chess analysis and automated play on Chess.com, Lichess, BlitzTactics, TakeTakeTake and ChessBase +Tactics.** Mephisto reads the position straight off the page, runs **Stockfish** (NNUE), **Fairy-Stockfish** or +**Maia** entirely in your browser — no server, no account — and draws the best move on the board, or plays it for you +with timing and move choices tuned to look human. -
-
-
Mephisto crushing some Puzzle Rush in real-time
- +Click the toolbar icon to toggle a floating panel over the board. Unlike a normal extension popup it stays open while +you click and play, so analysis and autoplay keep running for the whole game. -## Getting Started + - -
-
-
-
-
-
-
+
+*Maia-3's live rating slider · Atomic analysed by Fairy-Stockfish*
+
+Illegal scraped positions (missing king, wrong side in check, back-rank pawns) are blocked before they can crash the
+engine, and a crashed engine auto-restarts, capped at 3 attempts.
+
+**Strength cap** — limit any Stockfish/Fairy engine to a target Elo with an engine-aware slider whose stops follow
+that engine's real `UCI_Elo` range. Both ends mean full strength.
+
+### Variants
+
+**Chess960** works on every mainline Stockfish via `UCI_Chess960`, including every castling case. Fairy-Stockfish
+adds all of Lichess's variants (Crazyhouse, King of the Hill, Three-Check, Antichess, Atomic, Horde, Racing Kings)
+plus Chess.com's **Duck, Minihouse, Seirawan and Chaturanga**. The ↻ button beside the variant selector detects the
+variant and switches engine for you.
+
+Duck, Minihouse, Seirawan and Chaturanga have nets but the bundled chess.js can't replay them — the panel says so
+instead of analysing the wrong position.
+
+---
+
+## Features
+
+### Analysis
+
+- **Multiple lines** — top 1–5 candidates (MultiPV), each drawn with its evaluation.
+- **Eval bar** — vertical bar beside the board, from your perspective, plus an **eval history graph** shaped like
+ Lichess's, marking where the opening, middlegame and endgame begin (ported from scalachess's `Divider`).
+- **Threat analysis** — the opponent's strongest reply, so you see what they're threatening.
+- **Move confidence** — how much better the best move is than the second: `clearly best (+3.7)`, `+0.35 over #2`,
+ `several equal`, `only move`. Read off the MultiPV lines already on screen, so it costs no extra search.
+- **Explain moves** — names the tactic behind the choice (fork, promotion, winning capture, mate). Deliberately
+ conservative: pins, skewers and discovered attacks can't be established from the position alone, so it stays quiet
+ rather than guessing.
+- **Opening Explorer** — how humans played this opening (Lichess database): the name, the most-played replies with
+ their win/draw/loss split, and coloured arrows. Masters, all Lichess, or a club band. Lichess requires a personal
+ API token on this endpoint now — see the setting of that name; Game Review names openings without it, from its own
+ bundled table.
+- **Read a position off the screen** — the camera button captures the tab, finds the board and loads it. Any site: a
+ video, a diagram, an image. Nothing is uploaded. **Follow screen** re-reads twice a second so a board playing
+ elsewhere keeps the panel in step.
+- **Playable panel board** — click or drag to walk a line, with underpromotion. Every move is kept as a line you can
+ click back into.
+
+
+
+*Three candidate lines, each its own arrow · the explorer's book moves on the board*
+
+
+
+*Reading a position straight off a YouTube video — a board reading is a guess and says so, naming its least-confident
+squares (`least sure: e4 pawn 62%`).*
+
+### Game review
+
+Analyse finished games on the extension's own page — **Settings → Game Review**. Paste a PGN, load a
+`.pgn`, or fetch a player's recent games from Chess.com's public archive. Nothing is uploaded: the text
+stays in the tab and the search runs in the extension's own engine.
+
+- **Any engine, at your budget** — the bundled WASM Stockfishes, or a native host at full power. A
+ **depth** is reproducible (the same depth is the same answer on any machine) and is the default at 16;
+ a **time per move** defaults to 1s. Native hosts take either. 1–10 candidate lines, your own thread and
+ hash counts.
+- **What you actually gave up** — every position is searched once, so the score before a move and the
+ score after it come from the same search at the same budget, and the played move's rank in the engine's
+ own list is exact.
+- **Accuracy and move quality** — Lichess's win% and accuracy formulas, and the same 30/20/10 bands the
+ panel judges live moves by, so a review agrees with what the panel said at the time. Best, Excellent,
+ Good, Book, Forced, Inaccuracy, Mistake, Blunder.
+- **Eval graph** with the **opening / middlegame / endgame** boundaries marked on Lichess's own divider.
+ Click anywhere on it to jump the board there; blunders and mistakes are dotted.
+- **Openings named offline** from a bundled copy of [lichess-org/chess-openings](https://github.com/lichess-org/chess-openings)
+ (CC0) — 3,810 lines keyed by *position*, so a transposition is named correctly and nothing is fetched.
+- **Think time** read from the `[%clk ...]` comments Chess.com and Lichess both write, and **titles and
+ ratings** from the PGN's own tags: `GM Carlsen (2839)`.
+- **Human model (optional)** — a second pass with Maia, which predicts what a *human* of a chosen rating
+ plays rather than what is best. Maia 1 across its bands (1100–2200) or Maia 3 on a rating dial. It
+ reports where your move sat in Maia's **own ranking**, not a yes/no.
+- **Human likeness (optional, off)** — the whole game read by that second judge instead: how expected each
+ move was rather than how good, and the moves the engine ranked first that the human model never saw
+ coming.
+- **Across games** — switch **Review every game** on and the whole file is analysed against one engine
+ load, with each player's numbers pooled over all of it. One game cannot answer a fair-play question; a
+ season of them starts to.
+- **Fair-play indicators** — an **overall estimate** per player, then the lines it is drawn from: the
+ engine-match rate over the moves that were a real choice (book, forced and recapture moves excluded),
+ the rate in sharp positions and per phase, the longest unbroken engine streak, how uniform the accuracy
+ is, whether the longer thinks went to the harder positions, and how far the human model was from the
+ played move. Four levels with a key that says what each means. **Measurements, never a verdict** — the
+ page says so, and the estimate says what it is worth as well as what it says.
+- **Export** — the report exactly as it looks on the page: same markup, same stylesheets inlined, board
+ and pieces embedded, the full move table and the PGN. One file, no scripts, nothing to fetch, opens
+ anywhere.
+
+
+
+### Automated play
+
+- **Autoplay** — plays the engine's move for you. **Help Mode** draws the arrows instead and overrides it.
+- **Safe Premove** — while the opponent thinks, certifies a reply to their *predicted* move: the same move at
+ depth 13, depth 14 and the latest depth. An exact match fires instantly; anything else searches normally, so a
+ wrong guess costs nothing.
+ Forced moves and true recaptures queue as a real site premove, and an illegal one auto-cancels. On Chess.com, a
+ line forced *two* moves deep queues both replies at once.
+- **Pondering** — searches the opponent's whole think at full threads over their top 5 replies. Off, their turn is
+ capped at two threads (not one: premove certification needs depth 14).
+- **Play Book Moves** — plays the opening from the Explorer, weighted-random among popular replies. Needs 20+ games
+ and within 40cp of the engine's best, so variety never costs you a worse move.
+- **Endgame tablebase** — at 7 pieces or fewer the position is *solved*, so it asks Lichess's Syzygy tables for the
+ perfect move and outranks both engine and book. Off by default: it sends the position to a third party.
+- **Manual Mode** — thinks indefinitely and plays nothing until you press the play key.
+- **Background Play** (off by default) — moves fire only while the tab is focused and visible; a move that comes due
+ while you're away is deferred and re-issued when you return.
+
+### Humanize
+
+
+
+Seven shares set how often it plays the **top move**, a **2nd/3rd/4th line**, an **inaccuracy**, a **mistake** or a
+**blunder**; separate thresholds set how far each may stray in centipawns, with a live [Lichess
+accuracy](https://lichess.org/page/accuracy) estimate of the win-chance drop. Defaults sit on Lichess's own
+boundaries — 110cp inaccuracy, 230cp mistake, 377cp blunder. Nothing past the blunder threshold is played, and
+blunders never fire in a decided game.
+
+Timing follows: quick on obvious moves and openings, long thinks in critical positions, and an instant reflex *only*
+for true recaptures and forced moves — snapping off a piece that merely moved in to attack looked suspiciously fast.
+A countdown shows what kind of move is coming.
+
+**Clock Mode** budgets each move off the page clock (~time/30 + 60% of the increment); **Mirror Time** paces to the
+opponent's last spend −10%. Both size the search to the time they'll spend, so the wait becomes a deeper move.
+
+**Pace to Clock** is separate and off by default. Clock Mode paces the *search*; this paces the *simulated* delay —
+the think pause and the cursor travel — which is what actually costs you time in a scramble. With clock to spare
+your settings are used exactly as they are: it only ever makes a move shorter, never slower, and never below the
+point where the click stops looking like a hand moved it.
+
+> **Priority** — *Time:* Mirror ▸ Clock ▸ Humanize ▸ Search Time. *Move:* Book ▸ Humanize ▸ engine best.
+
+### Puzzles
+
+
+
+*3999 is the ceiling — there is no higher number Lichess will show you · every action rebindable*
+
+
+
+*Hardest (+600) puzzles back to back, from the database rather than searched.
+[The full clip](docs/puzzle-database.mp4) runs a minute and a half at higher quality.*
+
+**Puzzle Mode** optimises for solving speed — every move is one it actually searched, and the opponent's scripted
+reply is never analysed. A puzzle page ships no move list, so the position is rebuilt from the pieces alone: en
+passant is recovered from the last-move highlight and castling rights from the king and rook still at home, because
+without them an ep capture is illegal and nobody can castle in *any* puzzle.
+
+**Puzzle database** — a searched move is not always the puzzle's answer; a puzzle has one line that scores, and an
+objectively stronger move still fails it. Import Lichess's database and the panel looks the position up instead: on a
+hit the whole solution is known, so it plays it with **no search at all**. Works on Training, Storm and Racer.
+
+Lichess only for now, and it doesn't even ask elsewhere — that file is built from Lichess games, so a Chess.com
+position would be a guaranteed miss.
+
+**Chess.com puzzles.** The reader shipped in v3.1.207 — the same settings page, the same import button, and the
+format is detected from the file, so there is nothing extra to choose. Importing both databases gives you both: they
+key on the position, so neither overwrites the other. A database of **620,000+ Chess.com puzzles with their
+solutions** will be published once [the upstream pull request](https://github.com/AlexPetrusca/Mephisto/pull/37) is
+merged; it covers rated tactics and the daily archive.
+
+=n.outWidth||Math.floor(T)!==T)){var E=l*p-1-a.get(f,N,T,m)===w*p+S?1:0;0!==E&&(b+=e.get(f,N,T,m)*E)}}}h.set(b,f,g,y,m)}return h.toTensor()},e.prototype.avgPoolBackprop=function(e,t,r){this.assertNotComplex([e,t],"avgPoolBackprop");for(var n=r.strideHeight,a=r.strideWidth,o=r.filterHeight,i=r.filterWidth,s=r.dilationHeight,u=r.dilationWidth,l=r.effectiveFilterHeight,p=r.effectiveFilterWidth,c=p-1-r.padInfo.left,d=l-1-r.padInfo.top,h=buffer(t.shape,"float32"),f=1/(o*i),m=0;m =r.outWidth||Math.floor(E)!==E||(w+=e.get(m,S,E,g))}}h.set(w*f,m,y,v,g)}return h.toTensor()},e.prototype.cast=function(e,t){return castTensor(e,t,this)},e.prototype.reshape=function(e,t){return reshapeTensor(e,t)},e.prototype.avgPool=function(e,t){return this.assertNotComplex(e,"avgPool"),this.pool(e,t,"avg").toFloat()},e.prototype.resizeBilinear=function(e,t,r,n){this.assertNotComplex(e,"resizeBilinear");for(var a=e.shape,o=a[0],i=a[1],s=a[2],u=a[3],l=e.dataSync(),p=new Float32Array(sizeFromShape([o,t,r,u])),c=[n&&t>1?i-1:i,n&&r>1?s-1:s],d=[n&&t>1?t-1:t,n&&r>1?r-1:r],h=0,f=c[0]/d[0],m=c[1]/d[1],g=0;g 1?S*(l-1)+k*C:.5*(S+E)*(l-1))<0||H>l-1)for(D=0;D =e.size/s)throw new Error("Invalid indices: "+h+" does not index into "+e.shape);for(var y=0;y=n/a)throw new Error("Invalid indices: "+m+" does not index into "+r);for(var x=0;x0,"scheme must not be an empty string.");var n=e.getInstance();assert(null==n.managers[t],"A model store manager is already registered for scheme '"+t+"'."),n.managers[t]=r},e.getManager=function(e){var t=this.getInstance().managers[e];if(null==t)throw new Error("Cannot find model manager for scheme '"+e+"'");return t},e.getSchemes=function(){return Object.keys(this.getInstance().managers)},e}();function parseURL(e){if(-1===e.indexOf(URL_SCHEME_SUFFIX))throw new Error("The url string provided does not contain a scheme. Supported schemes are: "+ModelStoreManagerRegistry.getSchemes().join(","));return{scheme:e.split(URL_SCHEME_SUFFIX)[0],path:e.split(URL_SCHEME_SUFFIX)[1]}}function cloneModelInternal(e,t,r){return void 0===r&&(r=!1),__awaiter(this,void 0,void 0,function(){var n,a,o,i,s,u,l,p,c;return __generator(this,function(d){switch(d.label){case 0:return assert(e!==t,"Old path and new path are the same: '"+e+"'"),assert((n=IORouterRegistry.getLoadHandlers(e)).length>0,"Copying failed because no load handler is found for source URL "+e+"."),assert(n.length<2,"Copying failed because more than one ("+n.length+") load handlers for source URL "+e+"."),a=n[0],assert((o=IORouterRegistry.getSaveHandlers(t)).length>0,"Copying failed because no save handler is found for destination URL "+t+"."),assert(o.length<2,"Copying failed because more than one ("+n.length+") save handlers for destination URL "+t+"."),i=o[0],s=parseURL(e).scheme,u=parseURL(e).path,l=s===parseURL(e).scheme,[4,a.load()];case 1:return p=d.sent(),r&&l?[4,ModelStoreManagerRegistry.getManager(s).removeModel(u)]:[3,3];case 2:d.sent(),d.label=3;case 3:return[4,i.save(p)];case 4:return c=d.sent(),!r||l?[3,6]:[4,ModelStoreManagerRegistry.getManager(s).removeModel(u)];case 5:d.sent(),d.label=6;case 6:return[2,c.modelArtifactsInfo]}})})}function listModels(){return __awaiter(this,void 0,void 0,function(){var e,t,r,n,a,o,i;return __generator(this,function(s){switch(s.label){case 0:e=ModelStoreManagerRegistry.getSchemes(),t={},r=0,n=e,s.label=1;case 1:return r