Plugin directory / UI & Skins / dsh-ai-battle
dsh-ai-battle
Verified · install-tested on dsh Gxk96
What it does
Turn AI task waiting time into mini-game duels with difficulty scaling and record book.
Works — verified, early-stage project
Turn AI task waiting time into mini-game duels with difficulty scaling and record book. It installs cleanly and boots without issues in our testing. It's early-stage but functional.
“Verified” means our automated CI actually ran dsh plugin add in a clean profile and it booted — nothing more. Feature descriptions and version compatibility are the author’s claims. This is not a security audit and not an endorsement of third-party code.
README
dsh-ai-battle
English | 中文
A DeepSeek Harness plugin that turns waiting time into a mini-game duel:
While the AI grinds a troublesome task, a random enabled mini-game challenges you (never the same game twice in a row while more than one is enabled), with its difficulty scaled to the task. Reach the game's goal before the AI finishes the task and you win — otherwise the AI wins. You can quit any battle (non-training quits count as a loss), and the full record book is kept and browsable any time.
Screenshots
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
What is this?
In one sentence: it turns "waiting for the AI to run a task" into "racing the AI through a mini-game".
When you hand DeepSeek Harness a long-running job, this plugin turns the idle wait into a game with a real winner — and it's a multi-game platform: Minesweeper ships built-in, and adding more games is a small, documented extension task.
- Task battles (automatic) — after a troublesome task starts, the plugin scores its complexity, picks a random game from your enabled pool (rotating, never the previous one), maps the score onto that game's difficulty presets, and pops a dialog stating the game-specific win condition. Battles are never time-limited — the only win anchor is "did you finish before the AI finished its task": finish first and you win, task first and the AI wins.
- Training mode (manual) — tap the 💣 floating button → "🏋️ 训练模式" → pick a game, then a difficulty for an untimed training round. Results are never persisted; finishing counts as a win and you can quit anytime.
- Game multi-select — a dedicated Games tab (after Records) lets you tick/untick which games appear in auto-challenges (all enabled by default). Untick ones you dislike; at least one game must stay selected (the last one can't be turned off).
- Record book — every task-battle outcome (game, difficulty, duration, triggering task) is persisted (up to 200 entries); win rate and tallies are visible and clearable. Quitting a non-training battle counts as a loss.
Built-in games (all untimed — the only win anchor is finishing before the AI task does):
| Game | Difficulties | Goal |
|---|---|---|
| 💣 Minesweeper | 简单 9×9·10 / 中等 16×16·40 / 困难 16×30·99 / 地狱 20×24·130 | Clear the field |
| 🧩 Sudoku | easy 32 holes / medium 44 / hard 52 / inferno 54 | Fill the whole 9×9 grid |
| 🪜 Klotski (15-puzzle) | easy 12 shuffles / medium 28 / hard 48 / inferno 80 | Arrange 1–15 in order |
Sudoku: tap an empty cell, then a digit below to fill it (mistakes turn red and can be corrected). Klotski: tap a tile to slide it into the gap, arrow keys work too. Both games get the platform's score→difficulty mapping, random rotation, training mode, and records for free.
Games that fit this "race the AI" frame (for extension)
Prefer untimed games whose only completion anchor is "finishing" — the outcome must hinge on "did you reach the goal before the AI finished its task", not on a built-in clock. Quick-fire games that end in seconds (e.g. memory matching) have no sense of contest against a task that runs for a while. Good candidates: 2048 (difficulty = target tile 128→2048, full example below), Tetris (clear N lines), Snake (reach a high target score), etc.
How it works
When does a battle start? — text score and AI trajectory
The auto-start decision uses two independent opinions, combined into one score:
- Text score (
lib/complexity.js, evaluated the moment your message lands): keyword weights (CN + EN), brief length, numbered requirements, separators, code fences / file paths, polish words, and an imperative-opener bonus. A score ≥ 0.25 pops the challenge immediately — short explicit instructions ("实现一个…") still trigger instantly. - AI trajectory score (
lib/trajectory.js, evaluated while the AI is running): the host watches the session event stream and accumulates real work signals — elapsed time since the task started, tool calls (fs writes, bash runs, …), model steps, and output tokens (from each step'susage). When the trajectory score crosses 0.25 and no challenge/battle is pending for this turn, the challenge is created mid-task — so a task that looks trivial in text but turns into a long AI grind still gets a battle. The difficulty uses whichever score is higher, and such challenges are tagged "⚡ 轨迹判定(AI 运行中)" in the dialog.
Rules that keep it sane: one offer per turn (declining once means no re-offer for that task), per-project opt-out and the enabled-game pool apply to both paths, the watcher stops the moment a battle starts or the task ends, and a 30-minute safety cap prevents any leak. Battles remain untimed — the only win anchor is "did you finish before the AI finished its task".
Platform layer (lib/platform.js)
The game-agnostic state machine: task lifecycle (turn/start / user/message / turn/end), challenge creation with random game picking (pickGame, excludes the previous game), score→difficulty mapping (presetForScore, clamps to the game's preset count), accept/skip/quit, untimed training mode (never recorded), and records. Games plug in as adapters.
Game adapter (lib/games/minesweeper.js)
Each game implements a small contract and is fully hosted by the platform:
id/name/icon/rules(win-condition clauses shown in the challenge dialog)presets: ascending difficulty presets{key, label, limitMs, desc}+ game-private fieldscreateGame(battleId, preset, at)→ private game stateapplyMove(gameState, move)→{ state, win?, exploded? }restart(gameState, at)andview(gameState, ended)(client render data)recordMeta(gameState)(extra fields persisted with each record)
Host half (lib/index.js)
- Watches the
session/eventfirehose;user/message(sourcekind:'user') → text score → pick game + preset → create challenge; feeds the trajectory tracker fromstep/start/tool/call/assistant/message(usage) and offers a challenge mid-task when the trajectory score clears the threshold;turn/endsettles open battles as AI wins (the only loss anchor — no timers anywhere). - HTTP surface
/ms/api(same-origin, loopback):GET /ms/api/state,GET /ms/api/records,POST /ms/api/cmd—accept/decline/move/restart/forfeit/startSolo/setPreference(per-project challenge opt-out) /setGames(game multi-select) /clearRecords. - Persists to
$DSH_HOME/storages/dsh-ai-battle.json(atomic writes, capped at 200; legacydsh-minesweeper.jsonrecords migrate automatically on first boot). Preferences: per-project opt-out (disabledCwds) + global enabled-game set (disabledGames).
Client half (lib/client.js)
A hand-written __ModuleLoader__ bundle (no build step). Registered into the root-scoped shell.overlay layer — a draggable floating "battle一下" button (position in localStorage), independent of any session/project/header. The popover has:
- 对局 (Battle) — live game (generic head: game/difficulty/timer/quit + a per-game renderer), result overlays, in-panel challenge fallback; idle view offers the training entry and a last-result banner.
- 战绩 (Records) — tallies, win rate, the record list (game, difficulty, duration, task excerpt), clearable.
- 游戏 (Games) — the auto-challenge game multi-select (all on by default, at least one must stay selected).
The challenge dialog states the game-specific rules (1. rules[0] → you win 🏆; 2. rules[1] → AI wins 🤖), labels your prompt with "💬 你的提问", shows the auto difficulty, and offers accept/skip plus the per-project opt-out.
State is polled from the host every 2s (cheap localhost JSON).
Adding a game (extension)
- Write an adapter in
lib/games/mygame.jsimplementing the contract above (seeminesweeper.js). - Register it in
lib/index.js:GAME_LIST = [minesweeperGame, myGame]. - Write a renderer in
lib/client.js'sGAME_RENDERERSmap (mygame: MyGameView, consumingbattle.viewand callingonMove(move)). - Run
node test/smoke.mjsandnode scripts/sync-to-profile.mjs, then restartdsh web.
The platform gives new games random rotation, score→difficulty mapping, training mode, records, and quit handling for free.
How difficulties work — 2048 as the worked example
The platform's only rule: each game's presets array must be sorted ascending by difficulty, and the task score is mapped automatically (presetForScore):
| Task score | Auto-selected preset |
|---|---|
| 0.25 – 0.55 | presets[0] (easiest) |
| 0.55 – 0.70 | presets[1] |
| 0.70 – 0.85 | presets[2] |
| ≥ 0.85 | presets[3] (games with 2/3 presets clamp to the last one) |
2048's natural difficulty axis is the target tile (reaching it = finishing; untimed, completion-anchored — exactly this plugin's win model):
| key | Label | Goal | desc |
|---|---|---|---|
| easy | 简单 | reach 128 | 4×4 · 目标 128 |
| medium | 中等 | reach 512 | 4×4 · 目标 512 |
| hard | 困难 | reach 1024 | 4×4 · 目标 1024 |
| inferno | 地狱 | reach 2048 | 4×4 · 目标 2048 |
Adapter skeleton (lib/games/2048.js):
export const game2048 = {
id: '2048',
name: '2048',
icon: '🔢',
rules: ['在 AI 完成任务前合成到目标方块', '任务先完成(或你放弃)'],
presets: [
{ key: 'easy', label: '简单', target: 128, desc: '4×4 · 目标 128' },
{ key: 'medium', label: '中等', target: 512, desc: '4×4 · 目标 512' },
{ key: 'hard', label: '困难', target: 1024, desc: '4×4 · 目标 1024' },
{ key: 'inferno', label: '地狱', target: 2048, desc: '4×4 · 目标 2048' },
],
createGame(battleId, preset) {
// 4×4 grid seeded with two 2s; the goal tile comes from preset.target
// (an adapter-private field the platform never touches).
return { grid: new Array(16).fill(0), target: preset.target, over: false };
},
applyMove(gameState, move) {
// move: { type: 'swipe', dir: 'up' | 'down' | 'left' | 'right' }
// Merge on swipe; reaching the target tile → { state, win: true }.
return { state: gameState };
},
view(gameState) {
return { size: 4, cells: gameState.grid, target: gameState.target };
},
recordMeta(gameState) {
return { target: gameState.target, maxTile: Math.max(...gameState.grid) };
},
};
Notes:
presetsfieldskey/label/descare for the platform and UI; private fields liketargetbelong to the adapter.- Preset count is not forced to 4 — 2 or 3 presets clamp fine (see
presetForScore). - A win is triggered solely by
applyMovereturning{ win: true }; if the AI task ends (turn/end) before that, the AI wins automatically — the game itself must not carry a clock. - Training mode reuses the same presets: pick a game, then one of its difficulty tiers (shown via
desc).
Installing into the web profile
The package ships in profiles/node_modules/dsh-ai-battle (a copy — resync after edits with node scripts/sync-to-profile.mjs), referenced by:
C:\Users\gxkor\.dsh\profiles\web\package.json→"dsh-ai-battle": "file:D:/WorkSpeace/dsh/dsh-ai-battle"C:\Users\gxkor\.dsh\profiles\web\cordis.patch.yml→ one dual-face row:
- insert:
- id: ai-battle
name: 'dsh-ai-battle'
Restart required. The new host row and boot-manifest entry take effect when
dsh webrestarts. The old plugin row (dsh-minesweeper) has been removed from the profile; records migrate automatically.
Development
node test/smoke.mjs # platform + minesweeper adapter + scoring (29 checks)
node test/client-load.mjs # loads lib/client.js like the browser does
node test/e2e-driver.mjs # task-battle flow (:3090 test instance)
node test/e2e-solo.mjs # training + game multi-select + scorer
node test/e2e-pref.mjs # per-project challenge opt-out
node scripts/sync-to-profile.mjs
Zero runtime dependencies: host uses only node:fs/node:path, client only the platform seed modules (react + slot/locale services).
Files
dsh-ai-battle/
├── package.json # exports: "." (host), "./client" (bundle); dsh.client declaration
├── README.md / README.zh.md
├── lib/
│ ├── index.js # Cordis host plugin: events, /ms/api HTTP, persistence & migration
│ ├── platform.js # multi-game platform state machine
│ ├── complexity.js # task → difficulty score (text, at task start)
│ ├── trajectory.js # AI-run-time score (elapsed, tool calls, steps, tokens)
│ ├── games/
│ │ ├── minesweeper.js # built-in Minesweeper game adapter
│ │ ├── sudoku.js # built-in Sudoku game adapter
│ │ └── klotski.js # built-in Klotski (15-puzzle) game adapter
│ └── client.js # browser bundle: floating button + challenge dialog + game renderers + training + records
├── scripts/sync-to-profile.mjs
└── test/ # smoke, client-load, host-trajectory, e2e-driver, e2e-solo, e2e-pref
Install
Install the catalog once, then DeepSeek Harness can find and install any plugin from this site automatically:
dsh plugin add dshbase-catalog Then say "install dsh-ai-battle for me" — your agent finds it in the directory and installs it. Docs: dshbase-catalog · verified packs.
This plugin is GitHub source (not published to npm) — install it straight from the repo:
Web profile:
dsh plugin --profile web add github:Gxk96/dsh-ai-battle Headless (CLI) profile:
dsh plugin --profile headless add github:Gxk96/dsh-ai-battle Test report
Verified: L1 install + L2 load + L3 runtime from GitHub source on dsh 0.1.0-rc.6.
When to use it
Change how dsh looks or how you interact with it — a theme, a skin, or a new panel that reshapes the workspace.
Who it's for
Users who spend hours in the web UI and want it to look and feel the way they work.
For developers — extending it
Skins, panels, and theme tokens are the extension points — author a new skin, add a panel, or sync tokens with an upstream palette.





