Compare commits

...

59 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
6bbcadddfd Fix flickering on board redraw
Turns out == operator can't be used to compare two JS objects. Implement
a custom method for determining piece equality.
2021-12-14 21:57:26 +01:00
a1207f2404 Implement moving on engine
Thus make all state management the engine's responsibility, and remove
the state maintaining code from frontend!
2021-12-14 21:56:27 +01:00
c49bedf0ad Split state parsing on adapter into separate func 2021-12-14 21:55:23 +01:00
4ae73410bd Implement make_move function on engine 2021-12-14 21:12:59 +01:00
27dce5f7a3 Implement move squares highlighting
Issue #3
2021-12-13 20:46:53 +01:00
9bb6a60ded Implement adapter to the get_moves function
Issue #3
2021-12-13 20:22:52 +01:00
8d93acf950 Implement selected piece highlighting
Issue #3
2021-12-13 20:14:53 +01:00
f9212a8291 Refactor move handling in frontend
Make the move handlers better encapsulated and abstract away the click.

Issue #3
2021-12-13 20:11:37 +01:00
941a5c072a Refactor square drawing
Make square drawing modular and a member function of Canvas.

Issue #3
2021-12-13 19:38:54 +01:00
d36a948936 Use separate function to talk to engine in adapter 2021-12-13 18:21:24 +01:00
b9f39fa0b4 Implement get_moves command on rs
Issue #1
2021-12-13 18:03:42 +01:00
aa899740c6 Implement basic cmdline UI framework
Issue #1
2021-12-12 12:50:02 +01:00
6d433a6d01 Create a separate Ui struct 2021-12-12 11:49:06 +01:00
1f5e58d981 Implement get_legal_moves function 2021-12-11 19:00:09 +01:00
7788aea805 Address eslint 2021-12-11 18:08:02 +01:00
9e8b633447 Fix clippy suggesting to do something weird 2021-12-11 18:03:49 +01:00
f8c7323b47 Implement std::fmt::Display for board 2021-12-11 17:59:22 +01:00
7143427dd2 Fix misc clippy suggestions 2021-12-11 17:28:06 +01:00
7dabb49cd4 Do another refactoring of frontend 2021-12-11 17:15:50 +01:00
b5c252bd4d Implement basic frontend-engine interaction
Use a Flask-based Python server as an adapter to the engine's
stdin-stdout inferface.
2021-12-11 14:58:16 +01:00
57931b29de Add serialization to String 2021-12-05 18:14:16 +01:00
4f3469b3e3 Change Board state to HashMap 2021-12-05 15:43:40 +01:00
81626aefec Remove position from Piece struct 2021-12-05 12:08:45 +01:00
02e7d7e984 Split board-related stuff into board.rs 2021-12-05 11:54:15 +01:00
458f87fd61 Rename row/column to rank/file
This is how they're "officially" called
2021-12-04 18:54:09 +01:00
e45fd3981a Implement board setup 2021-11-23 22:47:56 +01:00
72a691de90 Implement initial Pawn setup 2021-11-22 22:42:21 +01:00
cc58260943 Refactor Piece 2021-11-17 22:15:00 +01:00
799c91fea0 Prettier index.html 2021-11-14 14:43:32 +01:00
51e5228145 Implement basic Piece infrastructure
And the Pawn class
2021-11-14 14:42:53 +01:00
6279e076ac Initialize rs schach engine 2021-11-10 20:53:13 +01:00
7edf459485 Revert "Start backend with rocket.rs"
This reverts commit 28011b8f93.
2021-11-09 21:41:29 +01:00
b246ba83d1 Make prettier 2021-11-08 21:55:51 +01:00
28011b8f93 Start backend with rocket.rs 2021-08-15 12:07:02 +02:00
cb44ae4184 Fix capture
This will probably not be needed in the near future anyway
2021-08-13 17:32:33 +02:00
ab1e70e7d4 Major overhaul of the architecture
It's pretty horrible to have a commit messing up 90% of the code and
also stuff isn't really well organized, but probably it needed to be
done...
2021-08-13 17:16:56 +02:00
14 changed files with 1490 additions and 176 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
target/

91
adapter/adapter.py Normal file
View File

@@ -0,0 +1,91 @@
import os
import subprocess
import flask
from flask_cors import CORS
PIECE_TYPE = [
("P", "pawn"),
("N", "knight"),
]
COLOR = [
("W", "white"),
("B", "black"),
]
HERE = os.path.abspath(os.path.dirname(__file__))
app = flask.Flask(__name__)
CORS(app)
engine = subprocess.Popen(
["cargo", "run"],
stdin=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):
piece_type, color = piece_str
return {
"piece_type": dict(PIECE_TYPE)[piece_type],
"color": dict(COLOR)[color],
}
def ask_engine(command):
engine.stdin.write(f"{command}\n".encode("ascii"))
engine.stdin.flush()
reply = engine.stdout.readline().decode("ascii").strip()
status, *result = reply.split(",")
if status != "ok":
flask.abort(400)
return result
def parse_state(state_str):
return {
state_str[i + 2 : i + 4]: make_piece(state_str[i : i + 2])
for i in range(0, len(state_str), 4)
}
@app.route("/get_state/")
def get_state():
(state_str,) = ask_engine("get_state")
return flask.jsonify(parse_state(state_str))
@app.route("/get_moves/", methods=["POST"])
def get_moves():
position_str = flask.request.json
(moves_str,) = ask_engine(f"get_moves,{position_str}")
moves = [moves_str[i : i + 2] for i in range(0, len(moves_str), 2)]
return flask.jsonify(moves)
@app.route("/make_move/", methods=["POST"])
def make_move():
source, target = flask.request.json
(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))
if __name__ == "__main__":
app.run(debug=True, host="127.0.0.1", port=3000)

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

@@ -1,10 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chess</title>
</head>
<body>
<script src="main.js"></script>
</body>
<head>
<meta charset="UTF-8" />
<title>Chess</title>
</head>
<body>
<script type="module" src="main.js"></script>
</body>
</html>

View File

@@ -1,6 +1,6 @@
{
"compilerOptions" : {
"compilerOptions": {
"checkJs": true,
"target": "es6"
"target": "es2017"
}
}

View File

@@ -1,178 +1,120 @@
const BOX_SIDE = 80;
const INDEX_MARGIN = BOX_SIDE / 2;
const INDEX_LETTERS = 'abcdefgh';
import * as visuals from "./visuals.js";
function createCanvas() {
let canvas = document.createElement('canvas');
let boardSide = BOX_SIDE * 8 + INDEX_MARGIN;
canvas.width = boardSide;
canvas.height = boardSide;
document.body.appendChild(canvas);
return canvas
function get_json(url) {
var request = new XMLHttpRequest();
request.open("get", url, false);
request.send(null);
return JSON.parse(request.responseText);
}
function drawChessBoard(canvas) {
let ctx = canvas.getContext('2d');
function post_json(url, json) {
var request = new XMLHttpRequest();
request.open("post", url, false);
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
request.send(JSON.stringify(json));
if (request.status != 200) return null;
return JSON.parse(request.responseText);
}
// Draw squares
ctx.fillStyle = '#c0c0c0';
for (let r = 0; r < 8; r++) {
for (let c = 0; c < 8; c++) {
if ((r + c) % 2) ctx.fillRect(c*BOX_SIDE + INDEX_MARGIN,
r*BOX_SIDE + INDEX_MARGIN,
BOX_SIDE, BOX_SIDE);
class Backend {
getConfig() {
return new Map(
Object.entries(get_json("http://localhost:3000/get_state/"))
);
}
makeMove(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) {
return post_json("http://localhost:3000/get_moves/", position);
}
reset() {
return post_json("http://localhost:3000/reset/");
}
}
class Chess {
constructor() {
this.backend = new Backend();
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.draw(this.canvas);
this.activeSquares = new visuals.ActiveSquares();
this.moveSource = null;
this.canMoveTo = [];
document.onclick = (ev) => this.click(ev);
}
updateState() {
let config = this.backend.getConfig();
this.configVis.configuration = config;
this.configVis.draw(this.canvas);
}
chooseMove() {
let move = this.backend.chooseMove("black");
if (move === null) return;
this.backend.makeMove(...move);
this.updateState();
}
initializeMove(source) {
if (source === null) return;
let validMoves = this.backend.getAvailableMoves(source);
if (validMoves === null) return;
this.activeSquares.selectSquare(this.canvas, source);
this.activeSquares.setMoveSquares(this.canvas, validMoves);
this.moveSource = source;
}
finalizeMove(target) {
let success;
if (target !== null) {
success = this.backend.makeMove(this.moveSource, target) !== null;
} else {
success = false;
}
this.activeSquares.unselectSquare(this.canvas);
this.activeSquares.unsetMoveSquares(this.canvas);
this.updateState();
this.moveSource = null;
if (this.autoplay && success) {
this.chooseMove();
}
}
// Draw letters
ctx.fillStyle = 'black';
let fontsize = INDEX_MARGIN / 2;
ctx.font = `${fontsize}px Monospace`;
for (let idx = 0; idx < 8; idx++) {
ctx.fillText(INDEX_LETTERS[idx],
BOX_SIDE*idx + BOX_SIDE/2 + INDEX_MARGIN - fontsize / 4,
INDEX_MARGIN / 2);
ctx.fillText((8-idx).toString(),
INDEX_MARGIN / 2 - fontsize / 4,
BOX_SIDE*idx + BOX_SIDE/2 + INDEX_MARGIN + fontsize / 4);
}
}
function computeScreenCoords(position, canvas) {
let canvasRect = canvas.getBoundingClientRect();
let rc = boardIndexToRowCol(position);
let topPx = canvasRect.top + INDEX_MARGIN + rc[0]*BOX_SIDE + window.pageYOffset;
let leftPx = canvasRect.left + INDEX_MARGIN + rc[1]*BOX_SIDE + window.pageXOffset;
return [topPx, leftPx]
}
class Move {
constructor(kind, from, to) {
this.kind = kind;
this.from = from;
this.to = to;
}
}
class Piece {
constructor(who, color, position) {
this.who = who;
this.color = color;
this.position = position;
this._im = null;
}
icon() {
return `./icons/${this.who}_${this.color}.svg`
}
draw(canvas) {
let [topPx, leftPx] = computeScreenCoords(this.position, canvas);
if (this._im === null) {
let im = new Image(BOX_SIDE, BOX_SIDE);
im.src = this.icon();
im.onload = () => {
im.style.position = 'absolute';
im.style.top = `${topPx}px`;
im.style.left = `${leftPx}px`;
};
document.body.appendChild(im);
this._im = im;
click(ev) {
let rc = this.canvas.screenCoordsToRowCol(ev.clientX, ev.clientY);
let position;
if (rc === null) {
position = null;
} else {
position = visuals.rowColToBoardIndex(...rc);
}
else {
this._im.style.top = `${topPx}px`;
this._im.style.left = `${leftPx}px`;
}
}
undraw() {
if (this._im !== null) {
this._im.remove();
this._im = null;
if (this.moveSource === null) {
this.initializeMove(position);
} else {
this.finalizeMove(position);
}
}
}
class Configuration {
constructor(pieces) {
this.pieces = pieces
}
draw(canvas) {
for (let piece of this.pieces) {
piece.draw(canvas);
}
}
getAt(position) {
for (let piece of this.pieces) {
if (piece.position === position) return piece;
}
return null;
}
dropAt(position) {
for (let idx = 0; idx < this.pieces.length; idx++) {
if (this.pieces[idx].position === position) {
this.pieces[idx].undraw();
this.pieces.splice(idx, 1);
return;
}
}
}
makeMove(move, canvas) {
let piece = this.getAt(move.from);
if (piece === null) return;
this.dropAt(move.to);
piece.position = move.to;
piece.draw(canvas);
}
}
function setupBoard() {
let pawns = [];
for (let x of INDEX_LETTERS) {
pawns.push(new Piece('pawn', 'black', `${x}7`),
new Piece('pawn', 'white', `${x}2`));
}
return new Configuration([
new Piece('rook', 'black', 'a8'),
new Piece('rook', 'black', 'h8'),
new Piece('knight', 'black', 'b8'),
new Piece('knight', 'black', 'g8'),
new Piece('bishop', 'black', 'c8'),
new Piece('bishop', 'black', 'f8'),
new Piece('queen', 'black', 'd8'),
new Piece('king', 'black', 'e8'),
...pawns,
new Piece('rook', 'white', 'a1'),
new Piece('rook', 'white', 'h1'),
new Piece('knight', 'white', 'b1'),
new Piece('knight', 'white', 'g1'),
new Piece('bishop', 'white', 'c1'),
new Piece('bishop', 'white', 'f1'),
new Piece('queen', 'white', 'd1'),
new Piece('king', 'white', 'e1'),
])
}
function boardIndexToRowCol(boardIndex) {
let colLetter = boardIndex[0];
let rowDigit = boardIndex[1];
let row = 8 - parseInt(rowDigit);
let col = INDEX_LETTERS.indexOf(colLetter);
return [row, col];
}
async function demo() {
let canvas = createCanvas();
drawChessBoard(canvas);
let config = setupBoard();
config.draw(canvas);
await new Promise(r => setTimeout(r, 6000));
config.makeMove({'kind': 'xxx', 'from': 'e2', 'to': 'e4'}, canvas);
await new Promise(r => setTimeout(r, 2000));
config.makeMove({'kind': 'xxx', 'from': 'e7', 'to': 'e5'}, canvas);
await new Promise(r => setTimeout(r, 2000));
config.makeMove({'kind': 'xxx', 'from': 'd2', 'to': 'd4'}, canvas);
await new Promise(r => setTimeout(r, 2000));
config.makeMove({'kind': 'xxx', 'from': 'e5', 'to': 'd4'}, canvas);
}
demo();
new Chess();

196
frontend/visuals.js Normal file
View File

@@ -0,0 +1,196 @@
const BOX_SIDE = 80;
const INDEX_MARGIN = BOX_SIDE / 2;
const INDEX_LETTERS = "abcdefgh";
export class Canvas {
constructor() {
this._canvas = document.createElement("canvas");
this._ctx = this._canvas.getContext("2d");
let boardSide = BOX_SIDE * 8 + INDEX_MARGIN;
this._canvas.width = boardSide;
this._canvas.height = boardSide;
document.body.appendChild(this._canvas);
this.drawChessBoard();
}
decideColor(r, c) {
if ((r + c) % 2) return "#c0c0c0";
else return "#ffffff";
}
drawSquareColor(color, r, c) {
const crtStyle = this._ctx.fillStyle;
this._ctx.fillStyle = color;
this._ctx.fillRect(
c * BOX_SIDE + INDEX_MARGIN,
r * BOX_SIDE + INDEX_MARGIN,
BOX_SIDE,
BOX_SIDE
);
this._ctx.fillStyle = crtStyle;
}
drawChessBoard() {
// Draw squares
for (let r = 0; r < 8; r++) {
for (let c = 0; c < 8; c++) {
this.drawSquareColor(this.decideColor(r, c), r, c);
}
}
// Draw letters
this._ctx.fillStyle = "black";
let fontsize = INDEX_MARGIN / 2;
this._ctx.font = `${fontsize}px Monospace`;
for (let idx = 0; idx < 8; idx++) {
this._ctx.fillText(
INDEX_LETTERS[idx],
BOX_SIDE * idx + BOX_SIDE / 2 + INDEX_MARGIN - fontsize / 4,
INDEX_MARGIN / 2
);
this._ctx.fillText(
(8 - idx).toString(),
INDEX_MARGIN / 2 - fontsize / 4,
BOX_SIDE * idx + BOX_SIDE / 2 + INDEX_MARGIN + fontsize / 4
);
}
}
screenCoordsToRowCol(cx, cy) {
let canvasRect = this._canvas.getBoundingClientRect();
if (cx > canvasRect.right || cy > canvasRect.bottom) {
return null;
}
let canvasX = cx - canvasRect.left;
let canvasY = cy - canvasRect.top;
let row = Math.floor((canvasY - INDEX_MARGIN) / BOX_SIDE);
let col = Math.floor((canvasX - INDEX_MARGIN) / BOX_SIDE);
return [row, col];
}
rowColToScreenCoords(row, col) {
let canvasRect = this._canvas.getBoundingClientRect();
let topPx =
canvasRect.top + INDEX_MARGIN + row * BOX_SIDE + window.pageYOffset;
let leftPx =
canvasRect.left + INDEX_MARGIN + col * BOX_SIDE + window.pageXOffset;
return [topPx, leftPx];
}
}
function boardIndexToRowCol(boardIndex) {
let [colLetter, rowDigit] = boardIndex;
let row = 8 - parseInt(rowDigit);
let col = INDEX_LETTERS.indexOf(colLetter);
return [row, col];
}
export function rowColToBoardIndex(row, col) {
let rowDigit = 8 - row;
let colLetter = INDEX_LETTERS[col];
return `${colLetter}${rowDigit}`;
}
export class ActiveSquares {
constructor() {
this.selectedSquare = null;
this.moveSquares = [];
}
selectSquare(canvas, position) {
let [r, c] = boardIndexToRowCol(position);
canvas.drawSquareColor("#c0c0ff", r, c);
this.selectedSquare = position;
}
setMoveSquares(canvas, moveSquares) {
moveSquares.forEach((position) => {
let [r, c] = boardIndexToRowCol(position);
canvas.drawSquareColor("#ffffc0", r, c);
this.moveSquares.push(position);
});
}
unsetMoveSquares(canvas) {
this.moveSquares.forEach((position) => {
let [r, c] = boardIndexToRowCol(position);
canvas.drawSquareColor(canvas.decideColor(r, c), r, c);
});
this.moveSquares = [];
}
unselectSquare(canvas) {
if (this.selectedSquare === null) return;
let [r, c] = boardIndexToRowCol(this.selectedSquare);
canvas.drawSquareColor(canvas.decideColor(r, c), r, c);
this.selectedSquare = null;
}
}
class PieceVis {
constructor(piece) {
this.piece = piece;
this.im = null;
}
get ["icon"]() {
return `./icons/${this.piece.piece_type}_${this.piece.color}.svg`;
}
draw(canvas, position) {
let [topPx, leftPx] = canvas.rowColToScreenCoords(
...boardIndexToRowCol(position),
canvas
);
if (this.im === null) {
let im = new Image(BOX_SIDE, BOX_SIDE);
im.src = this.icon;
im.onload = () => {
im.style.position = "absolute";
im.style.top = `${topPx}px`;
im.style.left = `${leftPx}px`;
};
document.body.appendChild(im);
this.im = im;
} else {
this.im.style.top = `${topPx}px`;
this.im.style.left = `${leftPx}px`;
}
}
undraw() {
if (this.im !== null) {
this.im.remove();
this.im = null;
}
}
}
function pieceEqual(pieceA, pieceB) {
return (
pieceA.color === pieceB.color && pieceA.piece_type === pieceB.piece_type
);
}
export class ConfigVis {
constructor(configuration) {
this.configuration = configuration;
this.piecesVis = new Map();
}
draw(canvas) {
for (let [position, piece] of this.configuration) {
if (this.piecesVis.has(position)) {
if (pieceEqual(this.piecesVis.get(position).piece, piece)) {
continue;
}
}
let pv = this.piecesVis.get(position);
if (pv !== undefined) {
pv.undraw();
}
pv = new PieceVis(piece);
pv.draw(canvas, position);
this.piecesVis.set(position, pv);
}
for (let [position, pv] of this.piecesVis) {
if (!this.configuration.has(position)) {
pv.undraw();
this.piecesVis.delete(position);
}
}
}
undraw() {
for (let pv of this.piecesVis.values()) {
pv.undraw();
}
this.piecesVis.clear();
}
}

85
rs/Cargo.lock generated Normal file
View File

@@ -0,0 +1,85 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
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]]
name = "schach"
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"

9
rs/Cargo.toml Normal file
View File

@@ -0,0 +1,9 @@
[package]
name = "schach"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rand = "0.8.4"

417
rs/src/board.rs Normal file
View File

@@ -0,0 +1,417 @@
use std::collections::HashMap;
#[derive(Eq, PartialEq, Clone, Debug)]
pub enum Color {
Black,
White,
}
impl Color {
fn get_home_rank(&self) -> Rank {
match self {
Color::Black => Rank::_8,
Color::White => Rank::_1,
}
}
fn get_pawn_rank(&self) -> Rank {
match self {
Color::Black => Rank::_7,
Color::White => Rank::_2,
}
}
pub fn other(&self) -> Color {
match self {
Color::Black => Color::White,
Color::White => Color::Black,
}
}
}
#[derive(Debug)]
pub struct Move_ {
pub source: Position,
pub target: Position,
}
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
pub struct Position {
pub file: File,
pub rank: Rank,
}
enum Dir {
Left,
Right,
Up,
Down,
}
impl Position {
pub fn new(file: File, rank: Rank) -> Position {
Position {
file,
rank,
}
}
fn chain(&self, dirs: &[&Dir]) -> Option<Position> {
dirs.iter().try_fold(self.clone(), |pos, dir| pos.delta(dir))
}
fn delta(&self, direction: &Dir) -> Option<Position> {
let (file, rank) = match direction {
Dir::Left => (self.file.decr()?, self.rank),
Dir::Right => (self.file.incr()?, self.rank),
Dir::Up => (self.file, self.rank.incr()?),
Dir::Down => (self.file, self.rank.decr()?),
};
Some(Position {
file,
rank,
})
}
}
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub enum Rank {
_1,
_2,
_3,
_4,
_5,
_6,
_7,
_8,
}
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub enum File {
A,
B,
C,
D,
E,
F,
G,
H,
}
pub trait GridAxis
where
Self: Sized,
Self: Clone,
{
fn incr(&self) -> Option<Self>;
fn decr(&self) -> Option<Self>;
}
impl GridAxis for Rank {
fn incr(&self) -> Option<Self> {
match self {
Rank::_1 => Some(Rank::_2),
Rank::_2 => Some(Rank::_3),
Rank::_3 => Some(Rank::_4),
Rank::_4 => Some(Rank::_5),
Rank::_5 => Some(Rank::_6),
Rank::_6 => Some(Rank::_7),
Rank::_7 => Some(Rank::_8),
Rank::_8 => None,
}
}
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 {
fn incr(&self) -> Option<Self> {
match self {
File::A => Some(File::B),
File::B => Some(File::C),
File::C => Some(File::D),
File::D => Some(File::E),
File::E => Some(File::F),
File::F => Some(File::G),
File::G => Some(File::H),
File::H => None,
}
}
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)]
pub enum PieceType {
Pawn,
Knight,
}
impl PieceType {
// Pawn
fn new_pawn(color: Color, file: File) -> (Piece, Position) {
let pos = Position::new(file, color.get_pawn_rank());
(
Piece {
piece_type: PieceType::Pawn,
color,
},
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()
}
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 color: Color,
pub piece_type: PieceType,
}
struct Ray {
positions: Vec<Position>,
}
impl Piece {
fn get_move_rays(&self, position: &Position) -> Vec<Ray> {
match self.piece_type {
PieceType::Pawn => {
PieceType::pawn_move_rays(position, &self.color)
}
PieceType::Knight => PieceType::knight_rays(position),
}
}
fn get_capture_rays(&self, position: &Position) -> Vec<Ray> {
match self.piece_type {
PieceType::Pawn => {
PieceType::pawn_capture_rays(position, &self.color)
}
PieceType::Knight => PieceType::knight_rays(position),
}
}
}
#[derive(Clone)]
pub struct Board {
state: HashMap<Position, Piece>,
}
impl Board {
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)
}
pub fn set_at(
&mut self,
position: Position,
piece_or_empty: Option<Piece>,
) {
match piece_or_empty {
Some(piece) => self.state.insert(position, piece),
None => self.state.remove(&position),
};
}
pub fn relocate(
&mut self,
source: &Position,
target: Position,
) -> Result<(), ()> {
let (_, piece) = self.state.remove_entry(source).ok_or(())?;
self.set_at(target, Some(piece));
Ok(())
}
pub fn reset(&mut self) {
self.state.clear();
for piece_type in &[PieceType::Pawn, PieceType::Knight] {
for (piece, position) in piece_type.initial_setup() {
self.set_at(position, Some(piece));
}
}
}
pub fn iter(&self) -> impl Iterator<Item = (&Position, &Piece)> {
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(
&self,
position: &Position,
) -> Result<impl Iterator<Item = Position> + '_, ()> {
Ok(self.find_moves(position)?.chain(self.find_captures(position)?))
}
pub fn all_moves_for_color(
&self,
color: Color,
) -> impl Iterator<Item = Move_> + '_ {
self.iter()
.filter_map(move |(source, piece)| {
if piece.color == color {
if let Ok(targets) = self.get_legal_moves(source) {
Some(targets.map(|target| Move_ {
source: source.clone(),
target,
}))
} else {
None
}
} 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)
}
}
}

8
rs/src/main.rs Normal file
View File

@@ -0,0 +1,8 @@
extern crate rand;
mod board;
mod engine;
mod ui;
fn main() -> Result<(), ()> {
ui::Ui::new().run()
}

315
rs/src/ui.rs Normal file
View File

@@ -0,0 +1,315 @@
use crate::board;
use crate::board::GridAxis;
use crate::engine;
use std::io::BufRead;
impl std::fmt::Display for board::Color {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
board::Color::White => write!(f, "W"),
board::Color::Black => write!(f, "B"),
}
}
}
impl std::fmt::Display for board::PieceType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
board::PieceType::Pawn => write!(f, "P"),
board::PieceType::Knight => write!(f, "N"),
}
}
}
impl std::fmt::Display for board::Piece {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}{}", self.piece_type, self.color)
}
}
trait GridAxisIO: std::fmt::Display
where
Self: GridAxis,
{
fn parse(c: char) -> Result<Self, ()>;
}
impl GridAxisIO for board::Rank {
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 {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
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 {
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 {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
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)
}
}
impl board::Position {
fn parse(s: &str) -> Result<Self, ()> {
let mut chars = s.chars();
let file = board::File::parse(chars.next().ok_or(())?)?;
let rank = board::Rank::parse(chars.next().ok_or(())?)?;
if chars.next().is_none() {
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 {
Err(())
}
}
}
impl std::fmt::Display for board::Position {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}{}", self.file, self.rank)
}
}
impl std::fmt::Display for board::Board {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
for (position, piece) in self.iter() {
write!(f, "{}{}", piece, position)?;
}
Ok(())
}
}
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 {
engine: engine::Engine,
}
impl Ui {
pub fn new() -> Self {
Ui {
engine: engine::Engine::new(),
}
}
fn get_state(&self, args: &[&str]) -> Result<String, String> {
if let [] = args {
let result = format!("{}", self.engine.get_state());
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> {
if let [position_str] = args {
let position =
board::Position::parse(position_str).map_err(|_| {
format!("Error parsing position {}", position_str)
})?;
match self.engine.board.get_legal_moves(&position) {
Err(_) => {
let error = format!("No moves possible from {}", position);
Err(error)
}
Ok(positions) => {
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> {
if let [source_str, target_str] = args {
let source = board::Position::parse(source_str)
.map_err(|_| format!("Error parsing source {}", source_str))?;
let target = board::Position::parse(target_str)
.map_err(|_| format!("Error parsing target {}", target_str))?;
match self.engine.make_move(&source, target) {
Err(()) => Err(format!(
"Move not possible {}-{}",
source_str, target_str
)),
Ok(()) => Ok(format!("{}", self.engine.get_state())),
}
} 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> {
let mut cmd = s.split(',');
// There will be at least an empty string => otherwise panic
let cmd_type = cmd.next().unwrap();
let args: Vec<&str> = cmd.collect();
match cmd_type {
"get_state" => self.get_state(&args),
"get_moves" => self.get_moves(&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("Invalid command".to_owned()),
}
}
pub fn run(&mut self) -> Result<(), ()> {
let stdin = std::io::stdin();
let mut input_lines = stdin.lock().lines();
loop {
match input_lines.next() {
None => break Ok(()),
Some(line) => match line {
Err(_) => break Err(()),
Ok(line) => match self.handle_command(&line) {
Ok(result) => println!("ok,{}", result),
Err(reason) => println!("err,{}", reason),
},
},
}
}
}
}