Sun Tzu Chess
A terminal program that teaches chess strategy through Sun Tzu's Art of War.
Each of the book's thirteen chapters becomes a campaign of lessons. Chapter IX
catalogs the signs an army leaves behind, so its campaign is about reading
what your opponent's last move gave away.
A lesson opens with a verse, then puts you in positions and asks for a move.
Every answer gets a reply, right or wrong, and the mistakes you repeat turn up
in a dossier. There is no free play and no computer opponent.
Six campaigns of the thirteen are written, five lessons each: Read the Board,
on chapter IX, Know Yourself, on chapter III, Be Unbeatable First, on chapter
IV, See Ahead, on chapter XIII, Do Not Dawdle, on chapter II, and Weak and
Strong, on chapter VI. The map lists the other seven and marks them as not
written.
SUN TZU CHESS the art of war on 64 squares
──────────────────────────────────────────────────────────────────────────────
› 1. Read the Board ch. IX 0/5
2. Know Yourself ch. III 0/5
3. Be Unbeatable First ch. IV 0/5
4. See Ahead ch. XIII 0/5
5. Do Not Dawdle ch. II 0/5
6. Weak and Strong ch. VI 0/5
7. Direct and Indirect ch. V not written
8. The Devious Route ch. VII not written
9. The Ground Decides ch. X not written
10. Nine Grounds ch. XI not written
11. Attack by Fire ch. XII not written
12. The Whole Plan ch. I not written
13. When Rules Break ch. VIII not written
Chapter IX is a catalog of signs: dust, birds, the tone of an envoy's
voice. Every chess move is a sign of the same kind. Learn to read the one
that was just played before you reach for one of your own.
──────────────────────────────────────────────────────────────────────────────
↑↓ choose enter open p precepts d dossier s settings q quit
A lesson puts the board beside the question, and names every marked square in
the margin:
SUN TZU CHESS What the Last Move Did 3/6
────────────────────────────────────────────────────────────────────────────────────────────
8 ♜ · ♝ ♛ ♚ ♝ ♞ ♜ Black has just played the knight from c6 to a5, attacking
7 ♟ ♟ ♟ ♟ · ♟ ♟ ♟ your bishop. Read the whole move before you answer it, then
6 · ~ · · · play your move.
5 ♞~ · ♟ ·
4 · ♗- · [♙] · · ~ c6 a5 the knight's journey
3 · · · ♘ · - c4 your bishop, and what the move wants you to look
2 ♙ ♙ ♙ ♙ ♙ ♙ ♙ at
1 ♖ ♘ ♗ ♕ ♔ · ♖
a b c d e f g h your move: enter picks up and puts down, : types it out
White to move
────────────────────────────────────────────────────────────────────────────────────────────
hjkl move enter select : type ? hint esc lessons
The square the question turns on, e5, is not marked. It appears once the move
has been played, because a label that names it answers the question.
Installing
go install github.com/sss7526/suntzuchess/cmd/suntzuchess@latest
From a clone, make build puts the binary in bin/, and go run ./cmd/suntzuchess runs it without installing anything.
Running it
suntzuchess
| flag |
effect |
-ascii |
start in the letters board style |
-no-color |
no styling; the NO_COLOR environment variable does the same |
-light |
assume a light terminal background instead of asking the terminal |
The window needs at least 64 by 18. Below 71 columns the board moves above the
text instead of beside it.
Where your progress is kept
Everything the program remembers about you goes in one file, progress.json:
which lessons you have finished, how many questions you answered, got wrong and
took a hint on, when you last played each one, the mistake tags behind the
dossier, and your board style.
| system |
file |
| Linux and BSD |
~/.config/suntzuchess/progress.json, or $XDG_CONFIG_HOME/suntzuchess/ if you set that |
| macOS |
~/Library/Application Support/suntzuchess/progress.json |
| Windows |
%AppData%\suntzuchess\progress.json |
Set SUNTZUCHESS_HOME to a directory to keep it somewhere else. The file is
plain JSON, so you can read it, copy it between machines, or delete it to start
over. Nothing leaves your computer, and the program makes no network
connections.
Keys
On the campaign map and the lesson list, arrows or hjkl move and enter
opens; on the map, g and G also jump to the first campaign and the last.
p opens the precepts index, d opens the dossier, s opens settings, q
quits.
Inside a lesson:
| key |
what it does |
hjkl, arrows |
move the cursor around the board |
enter or space |
pick up the piece under the cursor, then put it down |
enter |
once you have answered the question, go on to the next one |
: |
type the move instead: Nf3, exd5, O-O, or g1f3 |
, |
step a study back one move |
1 to 9 |
answer a multiple-choice question |
? |
hint |
f |
turn the board around |
esc |
back to the lesson list |
A question that wants a move wants the move, not the square. Select the piece
and send it to the target: to win the pawn on e5 you play the knight from f3,
you do not point at e5. Putting the cursor on a square you cannot pick up names
the pieces that could go there instead.
Board style
Terminal fonts disagree about the chess symbols. Some draw the outline set
(♔♕♖♗♘♙) and the filled set (♚♛♜♝♞♟) as clearly different shapes, some draw
them nearly alike, and some substitute a font that does neither. Press s on
the campaign map to see a board drawn in each style, and take whichever one
your terminal renders clearly. The program remembers the choice and uses it in
every lesson.
| style |
pieces |
| Diagram |
outline for White, filled for Black, both in one dark ink on tan squares |
| Contrast |
outline for White, filled for Black, kept light and dark on gray squares |
| Solid |
one filled set for both sides on gray squares, told apart by color |
| Solid Wood |
the same, on darker wooden squares |
| Letters |
uppercase for White, lowercase for Black |
Diagram and Contrast tell the sides apart by shape, so they survive
-no-color. The two Solid styles have only color to work with, and fall back
to letters when color is off.
If both sides come out the same dark shape, your terminal is drawing the filled
glyphs from a fallback font that colors them itself and ignores the foreground.
VS Code's built-in terminal does this; GNOME Terminal does not. Pick Diagram,
Contrast or Letters there.
The thirteen campaigns
They run in chess-difficulty order rather than the book's. Chapter I opens by
weighing five factors, which a beginner cannot do until the factors have been
taught, so it comes near the end. The precepts screen browses the same material
in the book's own order.
| # |
campaign |
chapter |
| 1 |
Read the Board |
IX, The Army on the March |
| 2 |
Know Yourself |
III, Attack by Stratagem |
| 3 |
Be Unbeatable First |
IV, Tactical Dispositions |
| 4 |
See Ahead |
XIII, The Use of Spies |
| 5 |
Do Not Dawdle |
II, Waging War |
| 6 |
Weak and Strong |
VI, Weak Points and Strong |
| 7 |
Direct and Indirect |
V, Energy |
| 8 |
The Devious Route |
VII, Manœuvering |
| 9 |
The Ground Decides |
X, Terrain |
| 10 |
Nine Grounds |
XI, The Nine Situations |
| 11 |
Attack by Fire |
XII, The Attack by Fire |
| 12 |
The Whole Plan |
I, Laying Plans |
| 13 |
When Rules Break |
VIII, Variation of Tactics |
The precepts index
p opens the whole of Giles' translation, a chapter at a time, with ← and
→ to move between chapters and ↑↓ to scroll. All thirteen chapters are
there in full, whether or not a campaign has been written for them, and a ›
marks each verse a lesson quotes.
The dossier
Press d for the dossier. Every wrong answer you give carries tags naming the
mistake, the tags add up across sessions, and the dossier lists the faults you
keep repeating with an explanation of each. Chapter III tells you to know
both sides. The dossier is the half about yourself.
The rest of this file is for anyone writing a lesson or working on the program.
Writing a lesson
Lessons are data. Adding one means writing YAML under corpus/, not Go.
corpus/
faults.yaml the mistake tags and what each means
book/giles.yaml the whole translation, for the precepts index
verses/giles.yaml the shorter quotations lessons show
campaigns/01-read-the-board.yaml
campaigns/02-know-yourself.yaml
campaigns/03-be-unbeatable-first.yaml
campaigns/04-see-ahead.yaml
campaigns/05-do-not-dawdle.yaml
campaigns/06-weak-and-strong.yaml
A campaign holds lessons, a lesson holds an ordered list of beats, and a beat
is one of seven kinds:
precept quotes a verse and says what it means on a board
study walks an authored line with commentary while the player watches
challenge asks for one move
gauntlet asks for several in turn, answering each with the opponent's reply
drill asks one move in each of several unrelated positions
choice asks a judgment question that no single move answers
debrief closes the lesson
A gauntlet threads one game through its steps, so only the beat sets a fen. A
drill sets one on every step, because its questions have nothing to do with
each other; that is what makes it the beat for repeating a shape until it is a
habit.
A challenge, with the shape of an answer key:
- type: challenge
fen: "r1bqkbnr/pppp1ppp/8/n3p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 4 4"
ask: What did the knight stop defending when it left?
marks:
- squares: [e5]
role: target
label: the knight on c6 was the only guard here
reveal:
- squares: [e5]
role: target
label: the knight on c6 was the only guard here
answers:
- move: Nxe5
say: A clean pawn.
mistakes:
- move: Bd5
say: You answered the threat and never asked what it cost them.
tags: [reactive-play]
otherwise: That move does not touch e5.
otherwise_tags: [reactive-play]
hint: Count the defenders of e5 now that the knight has left c6.
marks show with the question and reveal waits until it has been answered. A
label that names the loose piece, the weak square or the count answers the
question it sits beside, so it goes in reveal. Every step of a gauntlet or a
drill may reveal, because an answered question keeps the screen until the
player moves on.
otherwise_tags file a fault for a wrong move the step did not name, which is
most of them. Keep them general: this catches everything, so a specific tag
would be a guess.
A beat starts from any position. Chapter X is about pawn structure and chapter
XI about cramped and closed positions, so those campaigns want middlegames, not
openings.
mistakes names wrong moves worth answering specifically; otherwise catches
everything else. List every sound move you can find as an answer, partial: true where it is not the best one: an unlisted good move is scored wrong and
told to try again, which is the worst thing the format does. Mark roles are weak, target, attacker, path, good
and bad. Every mark also prints in the legend beside the board, so no
annotation depends on color.
Write prose in YAML folded scalars (>-). A blank line between two of them
becomes a paragraph break on screen.
Checking it
make validate # or: go run ./cmd/suntzuchess validate
This replays the whole corpus against the rules of chess and refuses anything
that would strand a player or teach them something the board does not do: an
unparseable FEN, an authored move the position does not allow, a move
that is both accepted and refuted, a gauntlet step with no opponent reply
before the next question, a precept quoting a chapter its campaign does not
teach, a mistake tag missing from faults.yaml, a lesson that does not end in
a debrief, and a good deal more. The same walk runs as a Go test, so bad
content breaks the build rather than a player's session.
With a UCI engine on PATH, make validate-engine also checks that every
accepted answer is within a pawn of the engine's choice, that no move called a
mistake comes that close, and that no partial outscores the answer above it.
It looks for stockfish, lc0 and komodo, and SUNTZUCHESS_ENGINE names one
directly.
Where a lesson has to walk the second-best move, because the position after it
is what the next question needs, the answer says teaching_line: true and
validate prints the list. Without it that pair reads as graded the wrong way
round, which is the mistake the check exists to catch.
This is an authoring tool. Lessons ship with authored answer keys and run
identically with no engine installed.
Building and testing
make on its own lists every target.
| target |
what it does |
make ci |
the whole gate: formatting, vet, tests, lint, govulncheck, the corpus check, a build, and a smoke test |
make test |
the test suite |
make cover-report |
per-package coverage as a bar chart |
make build |
the binary in bin/, with the version from git describe |
make dist |
cross-compiled archives and checksums for Linux, macOS and Windows on amd64 and arm64 |
Nothing beyond the Go toolchain needs installing. go run fetches
golangci-lint and govulncheck at pinned versions.
Layout
cmd/suntzuchess/ entry point, flags, the validate subcommand
corpus/ the lesson content, embedded into the binary
internal/game/ the chess rules library, wrapped for what lessons need
internal/board/ Render(position, overlay, options) string
internal/content/ beat types, the YAML loader, and the linter
internal/lesson/ the beat runtime: judges a move, produces the text
internal/profile/ progress and mistake tags on disk
internal/engine/ the optional UCI check
internal/ui/ the Bubble Tea front end
board.Render turns a position and an overlay into a string and touches
nothing else, and internal/lesson judges a move without drawing anything.
Neither needs a terminal to test. Tests drive internal/ui by feeding it key
messages and reading the rendered screen back.
Built on
corentings/chess for the rules,
Bubble Tea and
Lip Gloss for the terminal, and
goccy/go-yaml for the content files.
The text
The text is Sun Tzu on the Art of War, translated by Lionel Giles in 1910 and
long in the public domain. book/giles.yaml carries the treatise entire, 383
passages over thirteen chapters, keeping Giles' spelling and his verse numbers,
including the headings where he printed two verses together. His running
commentary and his footnotes are left out.
A lesson shows a shorter quotation, cut at a sentence boundary where a long
verse turns to a subject the lesson does not use. Those live in
verses/giles.yaml, and the linter checks each one against the passage it came
from, so the two cannot drift apart. It also refuses a passage that reads like
Giles' commentary rather than the treatise, because the plain-text source does
not always mark where one ends and the other begins.
License
MIT. See LICENSE.