Chess System Tal GUI - a chess board for your PC
CST-GUI
A clean 2D chess program for Windows that lets you play Chess System Tal — or any UCI engine — across the board. Drag the pieces and watch it think: live analysis with principal-variation lines, a best-move arrow, clocks and every time control from bullet to classical. Take back, swap sides, turn on pondering, load a Polyglot opening book, read and write PGN and EPD. The download bundles four Chess System Tal engines and plays the moment you unzip it — no setup.
https://whittingtonchess.com/
Released: Chess System Tal GUI
Moderator: Ras
-
chrisw
- Posts: 5100
- Joined: Tue Apr 03, 2012 4:28 pm
- Location: Digital Nomad. Anywhere but the Western Empire
- Full name: Christopher Whittington
-
Steve Maughan
- Posts: 1353
- Joined: Wed Mar 08, 2006 8:28 pm
- Location: Florida, USA
Re: Released: Chess System Tal GUI
Brilliant — thanks Chris!
http://www.chessprogramming.net - Juggernaut & Maverick Chess Engine
-
chrisw
- Posts: 5100
- Joined: Tue Apr 03, 2012 4:28 pm
- Location: Digital Nomad. Anywhere but the Western Empire
- Full name: Christopher Whittington
Re: Released: Chess System Tal GUI
New release update. 2D rendered board with animations and 3D rendered board and piece, wireframed Staunton modelling. Looks quite cool for those who like that sort of thing.
Look and feel art on website, lonk to engines and 3D GUI
Look and feel art on website, lonk to engines and 3D GUI
-
F.Huber
- Posts: 903
- Joined: Thu Mar 09, 2006 4:50 pm
- Location: Austria
- Full name: Franz Huber
Re: Released: Chess System Tal GUI
Hi Chris,
Are there any special prerequisites (e.g. Direct3D) necessary?
The previous version (2D) worked without problems ...
Regards,
Franz
this new version (with 3D) doesn't work for me - after starting abolutely nothing happens: no GUI, no error message, no CST-GUI in the TaskManager!
Are there any special prerequisites (e.g. Direct3D) necessary?
The previous version (2D) worked without problems ...
Regards,
Franz
-
chrisw
- Posts: 5100
- Joined: Tue Apr 03, 2012 4:28 pm
- Location: Digital Nomad. Anywhere but the Western Empire
- Full name: Christopher Whittington
Re: Released: Chess System Tal GUI
I’m somewhat stuck for testing at the moment, in nomad mode. The zip file should consist of the gui executable, four cstal engines and possibly some artwork (i forget if art is bundled into the exe or separate files). I guess to wait a few days to hear if it’s a universal bug or confined to your case only. Needless to say it worked when I tested it.F.Huber wrote: ↑Wed Jul 22, 2026 9:32 am Hi Chris,this new version (with 3D) doesn't work for me - after starting abolutely nothing happens: no GUI, no error message, no CST-GUI in the TaskManager!
Are there any special prerequisites (e.g. Direct3D) necessary?
The previous version (2D) worked without problems ...
Regards,
Franz
-
MMarco
- Posts: 221
- Joined: Sun Apr 12, 2020 1:09 am
- Full name: Marc-O Moisan-Plante
Re: Released: Chess System Tal GUI
I tried it yesterday and got only a black window to opon, nothing showing in it.F.Huber wrote: ↑Wed Jul 22, 2026 9:32 am Hi Chris,this new version (with 3D) doesn't work for me - after starting abolutely nothing happens: no GUI, no error message, no CST-GUI in the TaskManager!
Are there any special prerequisites (e.g. Direct3D) necessary?
The previous version (2D) worked without problems ...
Regards,
Franz
-
Dann Corbit
- Posts: 12885
- Joined: Wed Mar 08, 2006 8:57 pm
- Location: Redmond, WA USA
Re: Released: Chess System Tal GUI
Looks like a nice, clean interface, reminiscent of Winboard/Xboard.
Feel free not only to say no, but also to laugh in my face but...
I would like the ability to analyze a file of EPD positions and save the analysis to disk.
Arena does this, but the I/O is fully buffered rather than line buffered, so it loses contact with the engines, especially at fast time control. Lots of engines also use buffered output, so it's only half a solution but better than no solution.
Feel free not only to say no, but also to laugh in my face but...
I would like the ability to analyze a file of EPD positions and save the analysis to disk.
Arena does this, but the I/O is fully buffered rather than line buffered, so it loses contact with the engines, especially at fast time control. Lots of engines also use buffered output, so it's only half a solution but better than no solution.
Taking ideas is not a vice, it is a virtue. We have another word for this. It is called learning.
But sharing ideas is an even greater virtue. We have another word for this. It is called teaching.
But sharing ideas is an even greater virtue. We have another word for this. It is called teaching.
-
chrisw
- Posts: 5100
- Joined: Tue Apr 03, 2012 4:28 pm
- Location: Digital Nomad. Anywhere but the Western Empire
- Full name: Christopher Whittington
Re: Released: Chess System Tal GUI
will have some space/time to look at this in a couple of days. tested the download on a different PC here, worked. Possibly it demands avx2 minimum, but not anything else as far as I know.MMarco wrote: ↑Sat Jul 25, 2026 5:17 amI tried it yesterday and got only a black window to opon, nothing showing in it.F.Huber wrote: ↑Wed Jul 22, 2026 9:32 am Hi Chris,this new version (with 3D) doesn't work for me - after starting abolutely nothing happens: no GUI, no error message, no CST-GUI in the TaskManager!
Are there any special prerequisites (e.g. Direct3D) necessary?
The previous version (2D) worked without problems ...
Regards,
Franz
-
chrisw
- Posts: 5100
- Joined: Tue Apr 03, 2012 4:28 pm
- Location: Digital Nomad. Anywhere but the Western Empire
- Full name: Christopher Whittington
Re: Released: Chess System Tal GUI
I must have written this exact utility code ten times by now, its a trivial Python job. I don't get why a chess gui should do this, seems more of a job for a separate tool? Five minutes before I have to go out, lets see if Claude can hustle up the code by then ....Dann Corbit wrote: ↑Sat Jul 25, 2026 7:36 am Looks like a nice, clean interface, reminiscent of Winboard/Xboard.
Feel free not only to say no, but also to laugh in my face but...
I would like the ability to analyze a file of EPD positions and save the analysis to disk.
Arena does this, but the I/O is fully buffered rather than line buffered, so it loses contact with the engines, especially at fast time control. Lots of engines also use buffered output, so it's only half a solution but better than no solution.
- Here's my design (a standalone epd_analyse.py in EPD-Hack):
Drives any UCI engine via chess.engine.SimpleEngine (line-based, properly flushed — fixes the Arena GUI-side buffering problem).
Reads an EPD (or FEN) file; for each position runs engine.analyse to a configurable limit (--depth / --movetime / --nodes, any combination, at least one required).
Writes annotated EPD, appending standard opcodes: ce (eval, side-to-move POV, mate-aware), acd (depth), acs (seconds), acn (nodes), pv (SAN line). Original opcodes (id, existing bm/am test keys) are preserved verbatim; a bm is emitted only when the line has none, so nothing is clobbered.
Line-buffered output flushed per position + --resume to skip positions already written — a crash or fast-TC hiccup never loses completed work.
Code: Select all
"""
epd_analyse.py -- Batch-analyse a file of EPD (or FEN) positions with a UCI
engine and save the analysis to disk as annotated EPD.
Why this exists
---------------
Arena can analyse an EPD list, but its engine I/O is fully buffered rather than
line buffered, so it loses contact with engines -- badly at fast time control.
python-chess talks UCI over pipes with proper line-based reading and flushing,
so the GUI side of that problem goes away. (An engine that buffers its *own*
stdout is still the engine's problem to fix -- this tool can't help there.)
Output is written line-buffered and flushed after every position, and --resume
skips positions already present in the output file, so a crash or an interrupted
run never loses the work already done.
Output format -- annotated EPD
------------------------------
Each input line is written back out with its original operations preserved
verbatim (so `id`, and any existing `bm`/`am` test keys, survive), followed by
appended standard analysis opcodes:
ce centipawn evaluation, from the side-to-move point of view (mate-aware)
acd analysis count depth (search depth reached)
acs analysis count seconds (wall time spent on the position)
acn analysis count nodes (nodes searched)
pv principal variation (SAN, from the analysed position)
bm best move (SAN) -- emitted ONLY if the line has no bm/am already,
so engine picks never clobber a test suite's keys
Examples
--------
py -3.9 epd_analyse.py --engine stockfish.exe --epd suite.epd --movetime 1000
py -3.9 epd_analyse.py --engine tal.exe --epd wac.epd --out wac.out.epd \\
--depth 18 --hash 256 --threads 4 --resume
At least one of --depth / --movetime / --nodes must be given; if several are
given the engine stops at whichever limit it reaches first.
"""
import argparse
import datetime
import os
import re
import shlex
import sys
import chess
import chess.engine
# --------------------------------------------------------------------------- #
# EPD / FEN parsing
# --------------------------------------------------------------------------- #
# Matches a bm/am operation anywhere in the operations string, e.g. "bm Rxf4"
# or "am Qxg2;" -- used to decide whether to emit our own bm without clobbering.
_BM_AM_RE = re.compile(r"(?:^|;|\s)(?:bm|am)\s", re.IGNORECASE)
def parse_epd_line(line):
"""Split one EPD/FEN line into (full_fen, operations_string).
Accepts both 4-field EPD (placement stm castling ep [ops...]) and 6-field
FEN (with halfmove/fullmove counters). Returns (fen, ops) where `ops` is the
original trailing operations text with surrounding whitespace stripped, or
None if the line has too few fields to be a position.
"""
tokens = line.split()
if len(tokens) < 4:
return None
placement, stm, castling, ep = tokens[0:4]
# If tokens[4]/[5] look like the FEN move counters, consume them and start
# the operations after them; otherwise this is EPD and ops start at token 4.
halfmove, fullmove = "0", "1"
ops_index = 4
if len(tokens) >= 6 and tokens[4].isdigit() and tokens[5].isdigit():
halfmove, fullmove = tokens[4], tokens[5]
ops_index = 6
fen = " ".join([placement, stm, castling, ep, halfmove, fullmove])
# Recover the operations substring verbatim (preserve internal spacing,
# commas, semicolons and quotes exactly as written) by taking everything
# after the consumed leading fields.
ops = _tail_after_tokens(line, ops_index) if ops_index < len(tokens) else ""
return fen, ops
def _tail_after_tokens(line, n):
"""Return the substring of `line` that follows the first `n` whitespace
tokens, with leading/trailing whitespace stripped and any single trailing
';' preserved. Keeps the original operations text intact."""
pos = 0
for i in range(n):
# advance past whitespace
while pos < len(line) and line[pos].isspace():
pos += 1
# advance past the token
end = pos
while end < len(line) and not line[end].isspace():
end += 1
pos = end
return line[pos:].strip()
def board_key(fen):
"""Stable key identifying a position by its four EPD board fields."""
return " ".join(fen.split()[0:4])
# --------------------------------------------------------------------------- #
# Formatting the analysis into EPD opcodes
# --------------------------------------------------------------------------- #
def pv_to_san(board, pv):
"""Convert a list of Move objects into a space-separated SAN string,
starting from `board`. Stops early if a move is illegal (defensive)."""
b = board.copy(stack=False)
out = []
for mv in pv:
try:
out.append(b.san(mv))
b.push(mv)
except Exception:
break
return " ".join(out)
def eval_cp(pov_score, mate_score):
"""Centipawn integer from the side-to-move point of view, mate-aware.
A mate is encoded near +/-mate_score (closer mates score higher)."""
return pov_score.relative.score(mate_score=mate_score)
def build_output_line(fen, ops, info, board, mate_score, emit_bm):
"""Return the annotated EPD line for one analysed position."""
score = info.get("score")
pv = info.get("pv", []) or []
depth = info.get("depth")
nodes = info.get("nodes")
seconds = info.get("time")
parts = []
# Preserve original operations verbatim; ensure they are ';'-terminated so
# the appended opcodes remain valid EPD.
ops = ops.strip()
if ops:
if not ops.endswith(";"):
ops += ";"
parts.append(ops)
# Engine best move -> bm, only when the line carries no bm/am already.
if emit_bm and pv and not (ops and _BM_AM_RE.search(ops)):
try:
parts.append("bm {};".format(board.san(pv[0])))
except Exception:
pass
if score is not None:
parts.append("ce {};".format(eval_cp(score, mate_score)))
if depth is not None:
parts.append("acd {};".format(depth))
if seconds is not None:
parts.append("acs {:.3f};".format(seconds))
if nodes is not None:
parts.append("acn {};".format(nodes))
if pv:
san = pv_to_san(board, pv)
if san:
parts.append("pv {};".format(san))
board_fields = " ".join(fen.split()[0:4])
return board_fields + " " + " ".join(parts)
def short_status(fen, info, mate_score):
"""A one-line human-readable progress summary for stderr."""
score = info.get("score")
pv = info.get("pv", []) or []
best = ""
if pv:
try:
best = chess.Board(fen).san(pv[0])
except Exception:
best = pv[0].uci()
ce = eval_cp(score, mate_score) if score is not None else "?"
return "bm={:<7} ce={:<7} d={} n={}".format(
best, ce, info.get("depth", "?"), info.get("nodes", "?"))
# --------------------------------------------------------------------------- #
# Engine setup
# --------------------------------------------------------------------------- #
def configure_engine(engine, options):
"""Apply UCI options, skipping (with a warning) any the engine lacks."""
for name, value in options.items():
if name not in engine.options:
print(" warning: engine has no UCI option '{}' -- skipped"
.format(name), file=sys.stderr)
continue
try:
engine.configure({name: value})
except chess.engine.EngineError as exc:
print(" warning: could not set {} = {}: {}"
.format(name, value, exc), file=sys.stderr)
def open_engine(command):
engine = chess.engine.SimpleEngine.popen_uci(command)
return engine
# --------------------------------------------------------------------------- #
# Main driver
# --------------------------------------------------------------------------- #
def build_limit(args):
kwargs = {}
if args.depth is not None:
kwargs["depth"] = args.depth
if args.movetime is not None:
kwargs["time"] = args.movetime / 1000.0 # ms -> seconds
if args.nodes is not None:
kwargs["nodes"] = args.nodes
if not kwargs:
return None
return chess.engine.Limit(**kwargs)
def collect_uci_options(args):
options = {}
if args.hash is not None:
options["Hash"] = args.hash
if args.threads is not None:
options["Threads"] = args.threads
if args.multipv > 1:
options["MultiPV"] = args.multipv
for item in args.uci_option or []:
if "=" not in item:
print(" warning: ignoring malformed --uci-option '{}' "
"(expected NAME=VALUE)".format(item), file=sys.stderr)
continue
name, value = item.split("=", 1)
options[name.strip()] = value.strip()
return options
def load_done_keys(out_path):
"""Board keys already present in an existing output file (for --resume)."""
done = set()
if not os.path.exists(out_path):
return done
with open(out_path, "r", encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if not line or line.startswith("#"):
continue
if len(line.split()) >= 4:
done.add(board_key(line))
return done
def main(argv=None):
parser = argparse.ArgumentParser(
description="Analyse a file of EPD/FEN positions with a UCI engine and "
"save the analysis as annotated EPD.")
parser.add_argument("--engine", required=True,
help="Path to the UCI engine executable.")
parser.add_argument("--engine-args", default="",
help="Extra command-line arguments for the engine "
"(quoted string, split like a shell).")
parser.add_argument("--epd", required=True,
help="Input file of EPD or FEN positions (one per line).")
parser.add_argument("--out",
help="Output annotated-EPD file "
"(default: <epd>.analysed.epd).")
# Limit (configurable; at least one required, combinable).
parser.add_argument("--depth", type=int, help="Search depth per position.")
parser.add_argument("--movetime", type=int,
help="Think time per position, milliseconds.")
parser.add_argument("--nodes", type=int, help="Node budget per position.")
# Common UCI options (convenience) + arbitrary passthrough.
parser.add_argument("--hash", type=int, help="Hash table size in MB.")
parser.add_argument("--threads", type=int, help="Search threads.")
parser.add_argument("--multipv", type=int, default=1,
help="Number of PVs to request (default 1). "
"Alternatives are recorded as c0 comments.")
parser.add_argument("--uci-option", action="append", metavar="NAME=VALUE",
help="Set an arbitrary UCI option (repeatable).")
parser.add_argument("--mate-score", type=int, default=100000,
help="Centipawn magnitude used to encode mates in 'ce' "
"(default 100000).")
parser.add_argument("--no-bm", action="store_true",
help="Do not emit a 'bm' opcode even when the line has "
"none (rely on 'pv' for the engine's move).")
parser.add_argument("--resume", action="store_true",
help="Append to the output file and skip positions "
"already analysed in it.")
parser.add_argument("--limit-positions", type=int,
help="Analyse at most this many positions (for testing).")
args = parser.parse_args(argv)
limit = build_limit(args)
if limit is None:
parser.error("give at least one of --depth / --movetime / --nodes")
out_path = args.out or (args.epd + ".analysed.epd")
if not os.path.exists(args.epd):
parser.error("input file not found: {}".format(args.epd))
# Read input positions.
with open(args.epd, "r", encoding="utf-8") as fh:
raw_lines = fh.readlines()
positions = []
for lineno, raw in enumerate(raw_lines, 1):
text = raw.strip()
if not text or text.startswith("#"):
continue
parsed = parse_epd_line(text)
if parsed is None:
print(" warning: skipping unparseable line {}: {!r}"
.format(lineno, text), file=sys.stderr)
continue
positions.append((lineno, parsed[0], parsed[1]))
done = load_done_keys(out_path) if args.resume else set()
if done:
print("resume: {} position(s) already in {}".format(len(done), out_path),
file=sys.stderr)
engine_command = [args.engine] + shlex.split(args.engine_args)
options = collect_uci_options(args)
print("engine : {}".format(args.engine), file=sys.stderr)
print("input : {} ({} position(s))".format(args.epd, len(positions)),
file=sys.stderr)
print("output : {}".format(out_path), file=sys.stderr)
print("limit : {}".format(_describe_limit(args)), file=sys.stderr)
engine = open_engine(engine_command)
try:
configure_engine(engine, options)
# Line-buffered text output, flushed after every position so an
# interrupted run keeps everything analysed so far.
mode = "a" if args.resume else "w"
with open(out_path, mode, buffering=1, encoding="utf-8") as out:
if mode == "w":
out.write("# analysed {} by {} | limit {}\n".format(
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
os.path.basename(args.engine), _describe_limit(args)))
analysed = 0
skipped = 0
failed = 0
total = len(positions)
for idx, (lineno, fen, ops) in enumerate(positions, 1):
if args.limit_positions and analysed >= args.limit_positions:
break
key = board_key(fen)
if key in done:
skipped += 1
continue
try:
board = chess.Board(fen)
except ValueError as exc:
print(" [{}/{}] line {}: invalid FEN ({}) -- skipped"
.format(idx, total, lineno, exc), file=sys.stderr)
failed += 1
continue
try:
info = _analyse(engine, board, limit, args, engine_command)
except chess.engine.EngineError as exc:
print(" [{}/{}] line {}: engine error ({}) -- skipped"
.format(idx, total, lineno, exc), file=sys.stderr)
out.write(board_key(fen) +
' c0 "analysis failed: {}";\n'.format(
str(exc).replace('"', "'")))
failed += 1
# analyse() re-opens the engine on termination; keep going.
engine = _engine_holder["engine"]
continue
best_info = info[0] if isinstance(info, list) else info
line_out = build_output_line(
fen, ops, best_info, board, args.mate_score,
emit_bm=not args.no_bm)
# Extra PVs (multipv > 1) recorded as c0 comments.
if isinstance(info, list) and len(info) > 1:
line_out += _multipv_comment(info[1:], board, args.mate_score)
out.write(line_out + "\n")
analysed += 1
done.add(key)
print(" [{}/{}] {}".format(idx, total,
short_status(fen, best_info,
args.mate_score)),
file=sys.stderr)
print("done: {} analysed, {} skipped, {} failed -> {}".format(
analysed, skipped, failed, out_path), file=sys.stderr)
return 0
except KeyboardInterrupt:
print("\ninterrupted -- partial results saved to {}".format(out_path),
file=sys.stderr)
return 130
finally:
try:
_engine_holder["engine"].quit()
except Exception:
pass
# Holder so a re-opened engine after termination is visible to the main loop.
_engine_holder = {"engine": None}
def _analyse(engine, board, limit, args, engine_command):
"""Analyse one position, re-opening the engine once if it terminated."""
_engine_holder["engine"] = engine
multipv = args.multipv if args.multipv > 1 else None
try:
return engine.analyse(board, limit, info=chess.engine.INFO_ALL,
multipv=multipv)
except chess.engine.EngineTerminatedError:
# Restart once and retry; if it fails again the error propagates.
try:
engine.quit()
except Exception:
pass
engine = open_engine(engine_command)
configure_engine(engine, collect_uci_options(args))
_engine_holder["engine"] = engine
return engine.analyse(board, limit, info=chess.engine.INFO_ALL,
multipv=multipv)
def _multipv_comment(extra_infos, board, mate_score):
"""Render alternative PVs (multipv>1) as a trailing c0 comment."""
bits = []
for rank, info in enumerate(extra_infos, start=2):
pv = info.get("pv", []) or []
if not pv:
continue
san = pv_to_san(board, pv)
score = info.get("score")
ce = eval_cp(score, mate_score) if score is not None else "?"
bits.append("{}. ce {} pv {}".format(rank, ce, san))
if not bits:
return ""
return ' c0 "{}";'.format(" | ".join(bits).replace('"', "'"))
def _describe_limit(args):
bits = []
if args.depth is not None:
bits.append("depth {}".format(args.depth))
if args.movetime is not None:
bits.append("movetime {}ms".format(args.movetime))
if args.nodes is not None:
bits.append("nodes {}".format(args.nodes))
return ", ".join(bits)
if __name__ == "__main__":
sys.exit(main())
-
F.Huber
- Posts: 903
- Joined: Thu Mar 09, 2006 4:50 pm
- Location: Austria
- Full name: Franz Huber
Re: Released: Chess System Tal GUI
No, avx2 is definitely not the problem, because I've many avx2 chess engines running here without problems, so it must be something else.