Compare commits

...

23 Commits

Author SHA1 Message Date
6c2fd6566e Make minmax slighly less ugly
Can't vouch for correctness yet tho
2025-03-20 21:17:02 +01:00
9f7fbc8ef9 Implement minmax that is ugly as fuck 2025-03-06 21:45:15 +01:00
f4ac92ff89 Implement basic move evaluation
It ain't much but it's honest work
2025-03-05 21:40:20 +01:00
d797dc545e Add forgotten Knight parsing 2025-02-22 15:54:28 +01:00
e798480b72 For pawn caputre rays do same as knight rays 2025-02-22 15:47:13 +01:00
eda2ec6c93 Implement Knight 2025-02-22 15:39:49 +01:00
7cc7fa3e70 Refactor Position delta
Now it's a bit easier to work with
2025-02-22 15:38:43 +01:00
15202df4f4 Move some methods around 2025-02-20 21:24:59 +01:00
797c54e83b Purge ALL_VALUES from rank/file enums 2025-02-20 21:07:55 +01:00
01ccb65b64 Swap file and rank in Position
Because the chess positions read as "file-rank"
2025-02-20 21:00:53 +01:00
5108b4a6e2 Add uv config to adapter 2025-02-19 21:41:56 +01:00
98645f01d0 Make grid axes dumb enums 2025-02-18 21:27:12 +01:00
c7f82769e3 Add reset button 2022-01-05 22:10:04 +01:00
0e8eb7b767 Simplify the get_legal_moves method
It's cheap (and gets rid of boilerplate) to check for the field
occupancy inside the get_moves and get_captures funcitons.
2021-12-27 20:29:39 +01:00
5916a054ad Add basic autoplay features to frontend
Rudimentary play for white against engine is now possible.
2021-12-27 20:16:38 +01:00
c92a9e42d7 Add array reply parsing to the adapter 2021-12-27 20:15:21 +01:00
0f7de146b7 Implement dummy chess solver
It chooses a random available move ¯\_(ツ)_/¯
2021-12-22 22:03:17 +01:00
95c0ccfbd2 Move some functionality into the new Engine mod
This will help with separating the "thinking" logic from the move rules
and stuff.
2021-12-21 21:07:19 +01:00
802b069269 Add command for manual state setting at engine 2021-12-20 21:46:21 +01:00
ef5719f2f5 Reorganize board code in engine
Just a bit
2021-12-20 21:17:14 +01:00
d80cc79f57 Use destructuring for arg splitting in engine ui
This make the arg handling code less awkward.
2021-12-20 21:13:34 +01:00
a7d3bb3752 Use cargo run in the adapter
Makes it easier to start the app eliminating the danger of forgetting to
build.
2021-12-17 21:26:06 +01:00
6ee41a112c Introduce movement Rays
These reflect the fact that a piece can't jump over another piece
(unless it's a knight).

Issue #5
2021-12-17 21:25:51 +01:00
10 changed files with 869 additions and 191 deletions

View File

@@ -4,8 +4,10 @@ import subprocess
import flask import flask
from flask_cors import CORS from flask_cors import CORS
PIECE_TYPE = [
PIECE_TYPE = [("P", "pawn")] ("P", "pawn"),
("N", "knight"),
]
COLOR = [ COLOR = [
("W", "white"), ("W", "white"),
("B", "black"), ("B", "black"),
@@ -15,12 +17,17 @@ HERE = os.path.abspath(os.path.dirname(__file__))
app = flask.Flask(__name__) app = flask.Flask(__name__)
CORS(app) CORS(app)
engine = subprocess.Popen( engine = subprocess.Popen(
[os.path.join(HERE, "../rs/target/debug/schach")], ["cargo", "run"],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
cwd=os.path.join(HERE, "../rs"),
) )
def reverse(zipped):
return ((b, a) for a, b in zipped)
def make_piece(piece_str): def make_piece(piece_str):
piece_type, color = piece_str piece_type, color = piece_str
return { return {
@@ -33,7 +40,7 @@ def ask_engine(command):
engine.stdin.write(f"{command}\n".encode("ascii")) engine.stdin.write(f"{command}\n".encode("ascii"))
engine.stdin.flush() engine.stdin.flush()
reply = engine.stdout.readline().decode("ascii").strip() reply = engine.stdout.readline().decode("ascii").strip()
status, result = reply.split(",") status, *result = reply.split(",")
if status != "ok": if status != "ok":
flask.abort(400) flask.abort(400)
return result return result
@@ -48,14 +55,14 @@ def parse_state(state_str):
@app.route("/get_state/") @app.route("/get_state/")
def get_state(): def get_state():
state_str = ask_engine("get_state") (state_str,) = ask_engine("get_state")
return flask.jsonify(parse_state(state_str)) return flask.jsonify(parse_state(state_str))
@app.route("/get_moves/", methods=["POST"]) @app.route("/get_moves/", methods=["POST"])
def get_moves(): def get_moves():
position_str = flask.request.json position_str = flask.request.json
moves_str = ask_engine(f"get_moves,{position_str}") (moves_str,) = ask_engine(f"get_moves,{position_str}")
moves = [moves_str[i : i + 2] for i in range(0, len(moves_str), 2)] moves = [moves_str[i : i + 2] for i in range(0, len(moves_str), 2)]
return flask.jsonify(moves) return flask.jsonify(moves)
@@ -63,7 +70,20 @@ def get_moves():
@app.route("/make_move/", methods=["POST"]) @app.route("/make_move/", methods=["POST"])
def make_move(): def make_move():
source, target = flask.request.json source, target = flask.request.json
state_str = ask_engine(f"make_move,{source},{target}") (state_str,) = ask_engine(f"make_move,{source},{target}")
return flask.jsonify(parse_state(state_str))
@app.route("/choose_move/", methods=["POST"])
def choose_move():
color = dict(reverse(COLOR))[flask.request.json]
move = ask_engine(f"choose_move,{color}")
return flask.jsonify(move)
@app.route("/reset/", methods=["POST"])
def reset():
(state_str,) = ask_engine("reset")
return flask.jsonify(parse_state(state_str)) return flask.jsonify(parse_state(state_str))

9
adapter/pyproject.toml Normal file
View File

@@ -0,0 +1,9 @@
[project]
name = "adapter"
version = "0.1.0"
description = "Basic HTTP adapter for chess engine"
requires-python = ">=3.12"
dependencies = [
"flask>=3.1.0",
"flask-cors>=5.0.0",
]

146
adapter/uv.lock generated Normal file
View File

@@ -0,0 +1,146 @@
version = 1
requires-python = ">=3.12"
[[package]]
name = "adapter"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "flask" },
{ name = "flask-cors" },
]
[package.metadata]
requires-dist = [
{ name = "flask", specifier = ">=3.1.0" },
{ name = "flask-cors", specifier = ">=5.0.0" },
]
[[package]]
name = "blinker"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 },
]
[[package]]
name = "click"
version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "flask"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
{ name = "click" },
{ name = "itsdangerous" },
{ name = "jinja2" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/89/50/dff6380f1c7f84135484e176e0cac8690af72fa90e932ad2a0a60e28c69b/flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", size = 680824 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/af/47/93213ee66ef8fae3b93b3e29206f6b251e65c97bd91d8e1c5596ef15af0a/flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136", size = 102979 },
]
[[package]]
name = "flask-cors"
version = "5.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "flask" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4f/d0/d9e52b154e603b0faccc0b7c2ad36a764d8755ef4036acbf1582a67fb86b/flask_cors-5.0.0.tar.gz", hash = "sha256:5aadb4b950c4e93745034594d9f3ea6591f734bb3662e16e255ffbf5e89c88ef", size = 30954 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/07/1afa0514c876282bebc1c9aee83c6bb98fe6415cf57b88d9b06e7e29bf9c/Flask_Cors-5.0.0-py2.py3-none-any.whl", hash = "sha256:b9e307d082a9261c100d8fb0ba909eec6a228ed1b60a8315fd85f783d61910bc", size = 14463 },
]
[[package]]
name = "itsdangerous"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 },
]
[[package]]
name = "jinja2"
version = "3.1.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 },
]
[[package]]
name = "markupsafe"
version = "3.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 },
{ url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 },
{ url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 },
{ url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 },
{ url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 },
{ url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 },
{ url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 },
{ url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 },
{ url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 },
{ url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 },
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 },
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 },
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 },
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 },
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 },
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 },
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 },
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 },
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 },
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 },
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 },
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 },
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 },
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 },
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 },
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 },
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 },
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 },
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 },
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
]
[[package]]
name = "werkzeug"
version = "3.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 },
]

View File

@@ -25,15 +25,41 @@ class Backend {
makeMove(source, target) { makeMove(source, target) {
return post_json("http://localhost:3000/make_move/", [source, target]); return post_json("http://localhost:3000/make_move/", [source, target]);
} }
chooseMove(color) {
return post_json("http://localhost:3000/choose_move/", color);
}
getAvailableMoves(position) { getAvailableMoves(position) {
return post_json("http://localhost:3000/get_moves/", position); return post_json("http://localhost:3000/get_moves/", position);
} }
reset() {
return post_json("http://localhost:3000/reset/");
}
} }
class Chess { class Chess {
constructor() { constructor() {
this.backend = new Backend(); this.backend = new Backend();
this.canvas = new visuals.Canvas(); this.canvas = new visuals.Canvas();
this.autoplay = false;
this.button = document.createElement("button");
this.button.innerHTML = "Autoplay Off";
this.button.onclick = () => {
this.autoplay = !this.autoplay;
this.button.innerHTML = ["Autoplay Off", "Autoplay On"][
Number(this.autoplay)
];
};
document.body.appendChild(this.button);
this.resetButton = document.createElement("button");
this.resetButton.innerHTML = "Reset";
this.resetButton.onclick = () => {
this.backend.reset();
this.updateState();
};
document.body.appendChild(this.resetButton);
this.configVis = new visuals.ConfigVis(this.backend.getConfig()); this.configVis = new visuals.ConfigVis(this.backend.getConfig());
this.configVis.draw(this.canvas); this.configVis.draw(this.canvas);
this.activeSquares = new visuals.ActiveSquares(); this.activeSquares = new visuals.ActiveSquares();
@@ -46,6 +72,12 @@ class Chess {
this.configVis.configuration = config; this.configVis.configuration = config;
this.configVis.draw(this.canvas); this.configVis.draw(this.canvas);
} }
chooseMove() {
let move = this.backend.chooseMove("black");
if (move === null) return;
this.backend.makeMove(...move);
this.updateState();
}
initializeMove(source) { initializeMove(source) {
if (source === null) return; if (source === null) return;
let validMoves = this.backend.getAvailableMoves(source); let validMoves = this.backend.getAvailableMoves(source);
@@ -55,13 +87,19 @@ class Chess {
this.moveSource = source; this.moveSource = source;
} }
finalizeMove(target) { finalizeMove(target) {
let success;
if (target !== null) { if (target !== null) {
this.backend.makeMove(this.moveSource, target); success = this.backend.makeMove(this.moveSource, target) !== null;
} else {
success = false;
} }
this.activeSquares.unselectSquare(this.canvas); this.activeSquares.unselectSquare(this.canvas);
this.activeSquares.unsetMoveSquares(this.canvas); this.activeSquares.unsetMoveSquares(this.canvas);
this.updateState(); this.updateState();
this.moveSource = null; this.moveSource = null;
if (this.autoplay && success) {
this.chooseMove();
}
} }
click(ev) { click(ev) {
let rc = this.canvas.screenCoordsToRowCol(ev.clientX, ev.clientY); let rc = this.canvas.screenCoordsToRowCol(ev.clientX, ev.clientY);

78
rs/Cargo.lock generated
View File

@@ -2,6 +2,84 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "getrandom"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "libc"
version = "0.2.112"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125"
[[package]]
name = "ppv-lite86"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
[[package]]
name = "rand"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_hc",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom",
]
[[package]]
name = "rand_hc"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
dependencies = [
"rand_core",
]
[[package]] [[package]]
name = "schach" name = "schach"
version = "0.1.0" version = "0.1.0"
dependencies = [
"rand",
]
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"

View File

@@ -6,3 +6,4 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
rand = "0.8.4"

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Eq, PartialEq, Clone)] #[derive(Eq, PartialEq, Clone, Debug)]
pub enum Color { pub enum Color {
Black, Black,
White, White,
@@ -19,43 +19,58 @@ impl Color {
Color::White => Rank::_2, Color::White => Rank::_2,
} }
} }
fn get_direction(&self) -> i8 { pub fn other(&self) -> Color {
match self { match self {
Color::Black => -1, Color::Black => Color::White,
Color::White => 1, Color::White => Color::Black,
} }
} }
} }
#[derive(PartialEq, Eq, Hash, Clone)] #[derive(Debug)]
pub struct Move_ {
pub source: Position,
pub target: Position,
}
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
pub struct Position { pub struct Position {
pub rank: Rank,
pub file: File, pub file: File,
pub rank: Rank,
}
enum Dir {
Left,
Right,
Up,
Down,
} }
impl Position { impl Position {
pub fn new(rank: Rank, file: File) -> Position { pub fn new(file: File, rank: Rank) -> Position {
Position { Position {
rank,
file, file,
rank,
} }
} }
fn delta(&self, delta_rank_file: (i8, i8)) -> Result<Position, ()> { fn chain(&self, dirs: &[&Dir]) -> Option<Position> {
let (delta_rank, delta_file) = delta_rank_file; dirs.iter().try_fold(self.clone(), |pos, dir| pos.delta(dir))
Ok(Position::new(
self.rank.delta(delta_rank)?,
self.file.delta(delta_file)?,
))
} }
fn deltas_to_valid_positions(&self, deltas: &[(i8, i8)]) -> Vec<Position> { fn delta(&self, direction: &Dir) -> Option<Position> {
deltas let (file, rank) = match direction {
.iter() Dir::Left => (self.file.decr()?, self.rank),
.filter_map(|&delta_rank_file| self.delta(delta_rank_file).ok()) Dir::Right => (self.file.incr()?, self.rank),
.collect() Dir::Up => (self.file, self.rank.incr()?),
Dir::Down => (self.file, self.rank.decr()?),
};
Some(Position {
file,
rank,
})
} }
} }
#[derive(PartialEq, Eq, Hash, Clone, Copy)] #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub enum Rank { pub enum Rank {
_1, _1,
_2, _2,
@@ -67,7 +82,7 @@ pub enum Rank {
_8, _8,
} }
#[derive(PartialEq, Eq, Hash, Clone, Copy)] #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub enum File { pub enum File {
A, A,
B, B,
@@ -84,137 +99,234 @@ where
Self: Sized, Self: Sized,
Self: Clone, Self: Clone,
{ {
const ALL_VALUES: [Self; 8]; fn incr(&self) -> Option<Self>;
fn new(index: u8) -> Result<Self, ()> { fn decr(&self) -> Option<Self>;
Ok(Self::ALL_VALUES[Self::validate_index(index)? as usize].clone())
}
fn validate_index(index: u8) -> Result<u8, ()> {
if index as usize >= Self::ALL_VALUES.len() {
Err(())
} else {
Ok(index)
}
}
fn get_index(&self) -> u8;
fn delta(&self, delta: i8) -> Result<Self, ()> {
let highest = Self::ALL_VALUES.len() as i8 - 1;
if delta > highest
|| delta < -highest
|| (delta < 0 && self.get_index() < delta.abs() as u8)
|| (delta > 0 && self.get_index() + delta as u8 > highest as u8)
{
Err(())
} else {
Self::new(((self.get_index() as i8) + delta) as u8)
}
}
} }
impl GridAxis for Rank { impl GridAxis for Rank {
const ALL_VALUES: [Rank; 8] = [ fn incr(&self) -> Option<Self> {
Rank::_1, match self {
Rank::_2, Rank::_1 => Some(Rank::_2),
Rank::_3, Rank::_2 => Some(Rank::_3),
Rank::_4, Rank::_3 => Some(Rank::_4),
Rank::_5, Rank::_4 => Some(Rank::_5),
Rank::_6, Rank::_5 => Some(Rank::_6),
Rank::_7, Rank::_6 => Some(Rank::_7),
Rank::_8, Rank::_7 => Some(Rank::_8),
]; Rank::_8 => None,
fn get_index(&self) -> u8 { }
*self as u8 }
fn decr(&self) -> Option<Self> {
match self {
Rank::_1 => None,
Rank::_2 => Some(Rank::_1),
Rank::_3 => Some(Rank::_2),
Rank::_4 => Some(Rank::_3),
Rank::_5 => Some(Rank::_4),
Rank::_6 => Some(Rank::_5),
Rank::_7 => Some(Rank::_6),
Rank::_8 => Some(Rank::_7),
}
} }
} }
impl GridAxis for File { impl GridAxis for File {
const ALL_VALUES: [File; 8] = [ fn incr(&self) -> Option<Self> {
File::A, match self {
File::B, File::A => Some(File::B),
File::C, File::B => Some(File::C),
File::D, File::C => Some(File::D),
File::E, File::D => Some(File::E),
File::F, File::E => Some(File::F),
File::G, File::F => Some(File::G),
File::H, File::G => Some(File::H),
]; File::H => None,
fn get_index(&self) -> u8 { }
*self as u8 }
fn decr(&self) -> Option<Self> {
match self {
File::A => None,
File::B => Some(File::A),
File::C => Some(File::B),
File::D => Some(File::C),
File::E => Some(File::D),
File::F => Some(File::E),
File::G => Some(File::F),
File::H => Some(File::G),
}
} }
} }
#[derive(Clone)] #[derive(Clone)]
pub enum PieceType { pub enum PieceType {
Pawn, Pawn,
Knight,
} }
impl PieceType { impl PieceType {
fn initial_setup(&self) -> Vec<(Piece, Position)> { // Pawn
match &self { fn new_pawn(color: Color, file: File) -> (Piece, Position) {
PieceType::Pawn => [Color::Black, Color::White] let pos = Position::new(file, color.get_pawn_rank());
.iter()
.flat_map(|color| {
File::ALL_VALUES.iter().map(|&file| {
( (
Piece { Piece {
piece_type: self.clone(), piece_type: PieceType::Pawn,
color: color.clone(), color,
}, },
Position::new(color.get_pawn_rank(), file), pos,
) )
}
fn pawn_move_rays(position: &Position, color: &Color) -> Vec<Ray> {
let direction = match color {
Color::Black => Dir::Down,
Color::White => Dir::Up,
};
let go = |p: &Position| p.delta(&direction);
if position.rank == color.get_pawn_rank() {
let one = go(position).unwrap();
let two = go(&one).unwrap();
vec![Ray {
positions: vec![one, two],
}]
} else {
if let Some(p) = go(position) {
vec![Ray {
positions: vec![p],
}]
} else {
vec![]
}
}
}
fn pawn_capture_rays(position: &Position, color: &Color) -> Vec<Ray> {
let direction = match color {
Color::Black => Dir::Down,
Color::White => Dir::Up,
};
[&[&direction, &Dir::Left], &[&direction, &Dir::Right]]
.into_iter()
.filter_map(|dirs| position.chain(dirs))
.map(|p| Ray {
positions: vec![p],
}) })
.collect()
}
// Knight
fn new_knight(color: Color, file: File) -> (Piece, Position) {
let pos = Position::new(file, color.get_home_rank());
(
Piece {
piece_type: PieceType::Knight,
color,
},
pos,
)
}
fn knight_rays(position: &Position) -> Vec<Ray> {
[
&[&Dir::Up, &Dir::Up, &Dir::Left],
&[&Dir::Up, &Dir::Up, &Dir::Right],
&[&Dir::Right, &Dir::Right, &Dir::Up],
&[&Dir::Right, &Dir::Right, &Dir::Down],
&[&Dir::Down, &Dir::Down, &Dir::Right],
&[&Dir::Down, &Dir::Down, &Dir::Left],
&[&Dir::Left, &Dir::Left, &Dir::Down],
&[&Dir::Left, &Dir::Left, &Dir::Up],
]
.into_iter()
.filter_map(|dirs| position.chain(dirs))
.map(|p| Ray {
positions: vec![p],
}) })
.collect(), .collect()
}
fn initial_setup(&self) -> Vec<(Piece, Position)> {
match &self {
PieceType::Pawn => vec![
PieceType::new_pawn(Color::White, File::A),
PieceType::new_pawn(Color::White, File::B),
PieceType::new_pawn(Color::White, File::C),
PieceType::new_pawn(Color::White, File::D),
PieceType::new_pawn(Color::White, File::E),
PieceType::new_pawn(Color::White, File::F),
PieceType::new_pawn(Color::White, File::G),
PieceType::new_pawn(Color::White, File::H),
PieceType::new_pawn(Color::Black, File::A),
PieceType::new_pawn(Color::Black, File::B),
PieceType::new_pawn(Color::Black, File::C),
PieceType::new_pawn(Color::Black, File::D),
PieceType::new_pawn(Color::Black, File::E),
PieceType::new_pawn(Color::Black, File::F),
PieceType::new_pawn(Color::Black, File::G),
PieceType::new_pawn(Color::Black, File::H),
],
PieceType::Knight => vec![
PieceType::new_knight(Color::White, File::B),
PieceType::new_knight(Color::White, File::G),
PieceType::new_knight(Color::Black, File::B),
PieceType::new_knight(Color::Black, File::G),
],
} }
} }
} }
#[derive(Clone)]
pub struct Piece { pub struct Piece {
pub color: Color, pub color: Color,
pub piece_type: PieceType, pub piece_type: PieceType,
} }
struct Ray {
positions: Vec<Position>,
}
impl Piece { impl Piece {
fn get_moves(&self, position: &Position) -> Vec<Position> { fn get_move_rays(&self, position: &Position) -> Vec<Ray> {
let deltas = match self.piece_type { match self.piece_type {
PieceType::Pawn => self._pawn_get_move_deltas(position), PieceType::Pawn => {
}; PieceType::pawn_move_rays(position, &self.color)
position.deltas_to_valid_positions(&deltas)
} }
fn get_captures(&self, position: &Position) -> Vec<Position> { PieceType::Knight => PieceType::knight_rays(position),
let deltas = match self.piece_type {
PieceType::Pawn => self._pawn_get_capture_deltas(),
};
position.deltas_to_valid_positions(&deltas)
}
fn _pawn_get_move_deltas(&self, position: &Position) -> Vec<(i8, i8)> {
let direction = self.color.get_direction();
if position.rank == self.color.get_pawn_rank() {
vec![(direction, 0), (2 * direction, 0)]
} else {
vec![(direction, 0)]
} }
} }
fn _pawn_get_capture_deltas(&self) -> Vec<(i8, i8)> { fn get_capture_rays(&self, position: &Position) -> Vec<Ray> {
let direction = self.color.get_direction(); match self.piece_type {
vec![(direction, 1), (direction, -1)] PieceType::Pawn => {
PieceType::pawn_capture_rays(position, &self.color)
}
PieceType::Knight => PieceType::knight_rays(position),
}
} }
} }
#[derive(Clone)]
pub struct Board { pub struct Board {
state: HashMap<Position, Piece>, state: HashMap<Position, Piece>,
} }
impl Board { impl Board {
fn get_at(&self, position: &Position) -> Option<&Piece> { pub fn new() -> Board {
Board {
state: Default::default(),
}
}
pub fn occupied(&self, position: &Position) -> bool {
self.state.contains_key(position)
}
pub fn get_at(&self, position: &Position) -> Option<&Piece> {
self.state.get(position) self.state.get(position)
} }
fn set_at(&mut self, position: Position, piece_or_empty: Option<Piece>) { pub fn set_at(
&mut self,
position: Position,
piece_or_empty: Option<Piece>,
) {
match piece_or_empty { match piece_or_empty {
Some(piece) => self.state.insert(position, piece), Some(piece) => self.state.insert(position, piece),
None => self.state.remove(&position), None => self.state.remove(&position),
}; };
} }
fn move_from_to( pub fn relocate(
&mut self, &mut self,
source: &Position, source: &Position,
target: Position, target: Position,
@@ -223,15 +335,9 @@ impl Board {
self.set_at(target, Some(piece)); self.set_at(target, Some(piece));
Ok(()) Ok(())
} }
pub fn new() -> Board {
Board {
state: Default::default(),
}
}
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.state.clear(); self.state.clear();
for piece_type in &[PieceType::Pawn] { for piece_type in &[PieceType::Pawn, PieceType::Knight] {
for (piece, position) in piece_type.initial_setup() { for (piece, position) in piece_type.initial_setup() {
self.set_at(position, Some(piece)); self.set_at(position, Some(piece));
} }
@@ -240,37 +346,72 @@ impl Board {
pub fn iter(&self) -> impl Iterator<Item = (&Position, &Piece)> { pub fn iter(&self) -> impl Iterator<Item = (&Position, &Piece)> {
self.state.iter() self.state.iter()
} }
fn cut_move_ray(&self, ray: Ray) -> impl Iterator<Item = Position> + '_ {
ray.positions
.into_iter()
.take_while(|position| self.get_at(position).is_none())
}
fn find_capture_along_ray(
&self,
ray: Ray,
captured_color: &Color,
) -> Option<Position> {
// I'm not expecting to call this if there's no piece in the position
ray.positions.into_iter().find(|p| match self.get_at(p) {
None => false,
Some(piece) => &piece.color == captured_color,
})
}
pub fn find_moves(
&self,
position: &Position,
) -> Result<impl Iterator<Item = Position> + '_, ()> {
if let Some(piece) = self.get_at(position) {
Ok(piece
.get_move_rays(position)
.into_iter()
.flat_map(|ray| self.cut_move_ray(ray)))
} else {
Err(())
}
}
pub fn find_captures(
&self,
position: &Position,
) -> Result<impl Iterator<Item = Position> + '_, ()> {
if let Some(piece) = self.get_at(position) {
Ok(piece.get_capture_rays(position).into_iter().filter_map(
|ray| self.find_capture_along_ray(ray, &piece.color.other()),
))
} else {
Err(())
}
}
pub fn get_legal_moves( pub fn get_legal_moves(
&self, &self,
position: &Position, position: &Position,
) -> Option<Vec<Position>> { ) -> Result<impl Iterator<Item = Position> + '_, ()> {
let piece = match self.get_at(position) { Ok(self.find_moves(position)?.chain(self.find_captures(position)?))
None => return None,
Some(piece) => piece,
};
let valid_moves = piece
.get_moves(position)
.into_iter()
.filter(|pos| self.get_at(pos).is_none());
let valid_captures =
piece.get_captures(position).into_iter().filter(|pos| match self
.get_at(pos)
{
None => false,
Some(other_piece) => (other_piece.color != piece.color),
});
Some(valid_moves.chain(valid_captures).collect())
} }
pub fn make_move( pub fn all_moves_for_color(
&mut self, &self,
source: &Position, color: Color,
target: Position, ) -> impl Iterator<Item = Move_> + '_ {
) -> Result<(), ()> { self.iter()
if !self.get_legal_moves(source).ok_or(())?.contains(&target) { .filter_map(move |(source, piece)| {
Err(()) if piece.color == color {
if let Ok(targets) = self.get_legal_moves(source) {
Some(targets.map(|target| Move_ {
source: source.clone(),
target,
}))
} else { } else {
// We checked that there is a piece at source in get_legal_moves None
self.move_from_to(source, target)
} }
} else {
None
}
})
.flatten()
} }
} }

95
rs/src/engine.rs Normal file
View File

@@ -0,0 +1,95 @@
use crate::board;
use crate::board::Move_;
impl board::PieceType {
fn value(&self) -> f32 {
match self {
board::PieceType::Pawn => 1.0,
board::PieceType::Knight => 3.0,
}
}
}
impl board::Color {
fn sign(&self) -> f32 {
match self {
board::Color::Black => -1.0,
board::Color::White => 1.0,
}
}
}
pub struct Engine {
pub board: board::Board,
}
impl Engine {
pub fn new() -> Self {
let mut board = board::Board::new();
board.reset();
Engine {
board,
}
}
pub fn reset(&mut self) {
self.board.reset()
}
pub fn get_state(&self) -> &board::Board {
&self.board
}
pub fn set_state(&mut self, board: board::Board) {
self.board = board;
}
pub fn make_move(
&mut self,
source: &board::Position,
target: board::Position,
) -> Result<(), ()> {
if !self.board.get_legal_moves(source)?.any(|pos| pos == target) {
Err(())
} else {
// We checked that there is a piece at source in get_legal_moves
self.board.relocate(source, target)
}
}
pub fn choose_move(&self, color: &board::Color) -> Option<Move_> {
Some(
self.board
.all_moves_for_color(color.clone())
.map(|m| {
let mut board = self.board.clone();
board.relocate(&m.source, m.target.clone()).unwrap();
(minmax(1, &board, color), m)
})
.max_by(|(s1, _), (s2, _)| s1.partial_cmp(s2).unwrap())?
.1,
)
}
}
pub fn evaluate_position(board: &board::Board) -> f32 {
board
.iter()
.map(|(_, piece)| piece.color.sign() * piece.piece_type.value())
.sum()
}
fn minmax(depth: i8, board: &board::Board, color: &board::Color) -> f32 {
if depth == 0 {
evaluate_position(board) * color.sign()
} else {
let best_opponent_move_score = board
.all_moves_for_color(color.other())
.map(|m| {
let mut board = board.clone();
board.relocate(&m.source, m.target).unwrap();
minmax(depth - 1, &board, &color.other())
})
.max_by(|s1, s2| s1.partial_cmp(s2).unwrap());
if let Some(s) = best_opponent_move_score {
-s
} else {
minmax(0, board, color)
}
}
}

View File

@@ -1,4 +1,6 @@
extern crate rand;
mod board; mod board;
mod engine;
mod ui; mod ui;
fn main() -> Result<(), ()> { fn main() -> Result<(), ()> {

View File

@@ -1,5 +1,6 @@
use crate::board; use crate::board;
use crate::board::GridAxis; use crate::board::GridAxis;
use crate::engine;
use std::io::BufRead; use std::io::BufRead;
impl std::fmt::Display for board::Color { impl std::fmt::Display for board::Color {
@@ -15,6 +16,7 @@ impl std::fmt::Display for board::PieceType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self { match self {
board::PieceType::Pawn => write!(f, "P"), board::PieceType::Pawn => write!(f, "P"),
board::PieceType::Knight => write!(f, "N"),
} }
} }
} }
@@ -29,30 +31,70 @@ trait GridAxisIO: std::fmt::Display
where where
Self: GridAxis, Self: GridAxis,
{ {
const ALL_CHARS: [char; 8]; fn parse(c: char) -> Result<Self, ()>;
fn parse(c: char) -> Result<Self, ()> {
let index = Self::ALL_CHARS.iter().position(|p| p == &c).ok_or(())?;
Ok(Self::ALL_VALUES[index].clone())
}
} }
impl GridAxisIO for board::Rank { impl GridAxisIO for board::Rank {
const ALL_CHARS: [char; 8] = ['1', '2', '3', '4', '5', '6', '7', '8']; fn parse(c: char) -> Result<Self, ()> {
match c {
'1' => Ok(board::Rank::_1),
'2' => Ok(board::Rank::_2),
'3' => Ok(board::Rank::_3),
'4' => Ok(board::Rank::_4),
'5' => Ok(board::Rank::_5),
'6' => Ok(board::Rank::_6),
'7' => Ok(board::Rank::_7),
'8' => Ok(board::Rank::_8),
_ => Err(()),
}
}
} }
impl std::fmt::Display for board::Rank { impl std::fmt::Display for board::Rank {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", Self::ALL_CHARS[self.get_index() as usize]) let c = match self {
board::Rank::_1 => '1',
board::Rank::_2 => '2',
board::Rank::_3 => '3',
board::Rank::_4 => '4',
board::Rank::_5 => '5',
board::Rank::_6 => '6',
board::Rank::_7 => '7',
board::Rank::_8 => '8',
};
write!(f, "{}", c)
} }
} }
impl GridAxisIO for board::File { impl GridAxisIO for board::File {
const ALL_CHARS: [char; 8] = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; fn parse(c: char) -> Result<Self, ()> {
match c {
'a' => Ok(board::File::A),
'b' => Ok(board::File::B),
'c' => Ok(board::File::C),
'd' => Ok(board::File::D),
'e' => Ok(board::File::E),
'f' => Ok(board::File::F),
'g' => Ok(board::File::G),
'h' => Ok(board::File::H),
_ => Err(()),
}
}
} }
impl std::fmt::Display for board::File { impl std::fmt::Display for board::File {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", Self::ALL_CHARS[self.get_index() as usize]) let c = match self {
board::File::A => 'a',
board::File::B => 'b',
board::File::C => 'c',
board::File::D => 'd',
board::File::E => 'e',
board::File::F => 'f',
board::File::G => 'g',
board::File::H => 'h',
};
write!(f, "{}", c)
} }
} }
@@ -62,7 +104,43 @@ impl board::Position {
let file = board::File::parse(chars.next().ok_or(())?)?; let file = board::File::parse(chars.next().ok_or(())?)?;
let rank = board::Rank::parse(chars.next().ok_or(())?)?; let rank = board::Rank::parse(chars.next().ok_or(())?)?;
if chars.next().is_none() { if chars.next().is_none() {
Ok(board::Position::new(rank, file)) Ok(board::Position::new(file, rank))
} else {
Err(())
}
}
}
impl board::PieceType {
fn parse(c: char) -> Result<Self, ()> {
match c {
'P' => Ok(board::PieceType::Pawn),
'N' => Ok(board::PieceType::Knight),
_ => Err(()),
}
}
}
impl board::Color {
fn parse(c: char) -> Result<Self, ()> {
match c {
'B' => Ok(board::Color::Black),
'W' => Ok(board::Color::White),
_ => Err(()),
}
}
}
impl board::Piece {
fn parse(s: &str) -> Result<Self, ()> {
let mut chars = s.chars();
let piece_type = board::PieceType::parse(chars.next().ok_or(())?)?;
let color = board::Color::parse(chars.next().ok_or(())?)?;
if chars.next().is_none() {
Ok(board::Piece {
piece_type,
color,
})
} else { } else {
Err(()) Err(())
} }
@@ -84,55 +162,122 @@ impl std::fmt::Display for board::Board {
} }
} }
impl board::Board {
fn parse(s: &str) -> Result<Self, ()> {
if s.chars().all(|c| char::is_ascii_alphanumeric(&c)) {
let mut board = board::Board::new();
for piece_pos in s.as_bytes().chunks(4) {
let position = board::Position::parse(
std::str::from_utf8(&piece_pos[2..]).unwrap(),
)?;
let piece = board::Piece::parse(
std::str::from_utf8(&piece_pos[..2]).unwrap(),
)?;
board.set_at(position, Some(piece));
}
Ok(board)
} else {
Err(())
}
}
}
pub struct Ui { pub struct Ui {
board: board::Board, engine: engine::Engine,
} }
impl Ui { impl Ui {
pub fn new() -> Self { pub fn new() -> Self {
let mut board = board::Board::new();
board.reset();
Ui { Ui {
board, engine: engine::Engine::new(),
} }
} }
fn get_state(&self, args: &[&str]) -> Result<String, String> { fn get_state(&self, args: &[&str]) -> Result<String, String> {
if !args.is_empty() { if let [] = args {
Err("get_state takes no args".to_owned()) let result = format!("{}", self.engine.get_state());
} else {
let result = format!("{}", self.board);
Ok(result) Ok(result)
} else {
Err("get_state takes no args".to_owned())
}
}
fn set_state(&mut self, args: &[&str]) -> Result<String, String> {
if let [state_str] = args {
self.engine.set_state(
board::Board::parse(state_str).map_err(|_| {
format!("Error parsing state {}", state_str)
})?,
);
let result = format!("{}", self.engine.get_state());
Ok(result)
} else {
Err("set_state takes 1 arg".to_owned())
} }
} }
fn get_moves(&self, args: &[&str]) -> Result<String, String> { fn get_moves(&self, args: &[&str]) -> Result<String, String> {
if args.len() != 1 { if let [position_str] = args {
return Err("get_moves takes 1 arg".to_owned()); let position =
}; board::Position::parse(position_str).map_err(|_| {
let position = board::Position::parse(args[0]) format!("Error parsing position {}", position_str)
.map_err(|_| format!("Error parsing position {}", args[0]))?; })?;
match self.board.get_legal_moves(&position) { match self.engine.board.get_legal_moves(&position) {
None => { Err(_) => {
let error = format!("No moves possible from {}", position); let error = format!("No moves possible from {}", position);
Err(error) Err(error)
} }
Some(positions) => { Ok(positions) => {
Ok(positions.iter().map(|pos| format!("{}", pos)).collect()) Ok(positions.map(|pos| format!("{}", pos)).collect())
} }
} }
} else {
Err("get_moves takes 1 arg".to_owned())
}
} }
fn make_move(&mut self, args: &[&str]) -> Result<String, String> { fn make_move(&mut self, args: &[&str]) -> Result<String, String> {
if args.len() != 2 { if let [source_str, target_str] = args {
return Err("make_move takes 2 args".to_owned()); let source = board::Position::parse(source_str)
}; .map_err(|_| format!("Error parsing source {}", source_str))?;
let source = board::Position::parse(args[0]) let target = board::Position::parse(target_str)
.map_err(|_| format!("Error parsing source {}", args[0]))?; .map_err(|_| format!("Error parsing target {}", target_str))?;
let target = board::Position::parse(args[1]) match self.engine.make_move(&source, target) {
.map_err(|_| format!("Error parsing target {}", args[1]))?; Err(()) => Err(format!(
match self.board.make_move(&source, target) { "Move not possible {}-{}",
Err(()) => { source_str, target_str
Err(format!("Move not possible {}-{}", args[0], args[1])) )),
Ok(()) => Ok(format!("{}", self.engine.get_state())),
} }
Ok(()) => Ok(self.board.to_string()), } else {
Err("make_move takes 2 args".to_owned())
}
}
fn choose_move(&self, args: &[&str]) -> Result<String, String> {
if let [color_str] = args {
if let [color_char] = color_str.chars().collect::<Vec<char>>()[..]
{
match self.engine.choose_move(
&board::Color::parse(color_char).map_err(|_| {
format!("Couldn't parse color code {}", color_char)
})?,
) {
Some(move_) => {
Ok(format!("{},{}", move_.source, move_.target))
}
None => {
Err(format!("No move possible for {}", color_char))
}
}
} else {
Err(format!("Invalid color string {}", color_str))
}
} else {
Err("choose_move takes 1 arg".to_owned())
}
}
fn reset(&mut self, args: &[&str]) -> Result<String, String> {
if let [] = args {
self.engine.reset();
Ok(format!("{}", self.engine.get_state()))
} else {
Err("reset doesn't take args".to_owned())
} }
} }
fn handle_command(&mut self, s: &str) -> Result<String, String> { fn handle_command(&mut self, s: &str) -> Result<String, String> {
@@ -144,6 +289,9 @@ impl Ui {
"get_state" => self.get_state(&args), "get_state" => self.get_state(&args),
"get_moves" => self.get_moves(&args), "get_moves" => self.get_moves(&args),
"make_move" => self.make_move(&args), "make_move" => self.make_move(&args),
"set_state" => self.set_state(&args),
"choose_move" => self.choose_move(&args),
"reset" => self.reset(&args),
"" => Err("No command given".to_owned()), "" => Err("No command given".to_owned()),
_ => Err("Invalid command".to_owned()), _ => Err("Invalid command".to_owned()),
} }