T07: Cargo workspace scaffold — cb-kernel/cb-events/cb-game-runtime/games-ground/cb-sim, HashMap deny-lint, scenario format + runner stub, Criterion skeleton, Makefile, CI
Some checks failed
ci / check (push) Has been cancelled
Some checks failed
ci / check (push) Has been cancelled
This commit is contained in:
parent
396990539a
commit
467e2c561d
23 changed files with 1625 additions and 0 deletions
18
.forgejo/workflows/ci.yml
Normal file
18
.forgejo/workflows/ci.yml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: rust:1.97
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: rustup component add rustfmt clippy
|
||||
- run: cargo fmt --all --check
|
||||
- run: cargo clippy --workspace --all-targets -- -D warnings
|
||||
- run: cargo test --workspace
|
||||
- run: cargo run -q -p cb-sim -- scenarios/ground/*.yaml || test $? -eq 2
|
||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
605
Cargo.lock
generated
Normal file
605
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,605 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anes"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cb-events"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cb-kernel",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cb-game-runtime"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cb-events",
|
||||
"cb-kernel",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cb-kernel"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cb-sim"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cb-game-runtime",
|
||||
"games-ground",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"ciborium-ll",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-io"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-ll"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"half",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
|
||||
dependencies = [
|
||||
"anes",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"is-terminal",
|
||||
"itertools",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"oorandom",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "games-ground"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cb-events",
|
||||
"cb-game-runtime",
|
||||
"cb-kernel",
|
||||
"criterion",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[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.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinytemplate"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
35
Cargo.toml
Normal file
35
Cargo.toml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/cb-kernel",
|
||||
"crates/cb-events",
|
||||
"crates/cb-game-runtime",
|
||||
"games/ground",
|
||||
"tools/cb-sim",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
version = "0.1.0"
|
||||
license-file = "LICENSE"
|
||||
|
||||
[workspace.dependencies]
|
||||
cb-kernel = { path = "crates/cb-kernel" }
|
||||
cb-events = { path = "crates/cb-events" }
|
||||
cb-game-runtime = { path = "crates/cb-game-runtime" }
|
||||
games-ground = { path = "games/ground" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
sha2 = "0.10"
|
||||
rand_core = "0.6"
|
||||
rand_chacha = "0.3"
|
||||
criterion = { version = "0.5", default-features = false }
|
||||
|
||||
# Determinism discipline (GameKernel K6): HashMap/HashSet are deny-linted
|
||||
# workspace-wide; semantic state must use ordered collections.
|
||||
[workspace.lints.clippy]
|
||||
disallowed_types = "deny"
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
35
Makefile
Normal file
35
Makefile
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# One command surface (InnerLoop §agentic-efficiency #3). Deterministic,
|
||||
# greppable output; precursor of the `cb` CLI.
|
||||
|
||||
CARGO := cargo
|
||||
|
||||
.PHONY: check test sim bench deps loc all
|
||||
|
||||
## fmt + clippy (deny warnings) + HashMap deny-lint
|
||||
check:
|
||||
$(CARGO) fmt --all --check
|
||||
$(CARGO) clippy --workspace --all-targets -- -D warnings
|
||||
|
||||
## unit + scenario-format tests
|
||||
test:
|
||||
$(CARGO) test --workspace
|
||||
|
||||
## run all GROUND scenarios through cb-sim
|
||||
sim:
|
||||
$(CARGO) run -q -p cb-sim -- scenarios/ground/*.yaml
|
||||
|
||||
## Criterion benches (AM-6/AM-7 wiring)
|
||||
bench:
|
||||
$(CARGO) bench -p games-ground
|
||||
|
||||
## AM-4: transitive crate count (excludes dev/build deps)
|
||||
deps:
|
||||
@$(CARGO) tree --workspace -e normal --prefix none | sed 's/ (\*)//' | sort -u | grep -vE '^(cb-|games-|cb_|$$)' | tee /dev/stderr | wc -l
|
||||
|
||||
## AM-2/AM-3 input: source LOC per crate (excludes tests would need tokei)
|
||||
loc:
|
||||
@for d in crates/cb-kernel crates/cb-events crates/cb-game-runtime games/ground tools/cb-sim; do \
|
||||
printf '%-28s %s\n' $$d "$$(find $$d/src -name '*.rs' | xargs cat | grep -vcE '^\s*(//|$$)')"; \
|
||||
done
|
||||
|
||||
all: check test sim
|
||||
4
clippy.toml
Normal file
4
clippy.toml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
disallowed-types = [
|
||||
{ path = "std::collections::HashMap", reason = "GameKernel K6: iteration order nondeterminism — use BTreeMap" },
|
||||
{ path = "std::collections::HashSet", reason = "GameKernel K6: iteration order nondeterminism — use BTreeSet" },
|
||||
]
|
||||
14
crates/cb-events/Cargo.toml
Normal file
14
crates/cb-events/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "cb-events"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license-file.workspace = true
|
||||
|
||||
[dependencies]
|
||||
cb-kernel.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
168
crates/cb-events/src/lib.rs
Normal file
168
crates/cb-events/src/lib.rs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
//! cb-events — event envelope, append-only log, snapshots, canonical
|
||||
//! serialization, and state hashing (GameKernel §2.4, K4, K7, K9–K11).
|
||||
|
||||
use cb_kernel::{EventSeq, GameId};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// Versioned event envelope (GameKernel K4).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Envelope<E> {
|
||||
pub seq: EventSeq,
|
||||
pub game_id: GameId,
|
||||
pub round: u8,
|
||||
pub schema_ver: u16,
|
||||
pub payload: E,
|
||||
}
|
||||
|
||||
/// In-memory append-only event log. File-backed storage arrives in a later
|
||||
/// loop pass behind the same interface (AM-11 pairs this with storage
|
||||
/// impls under one conformance suite).
|
||||
#[derive(Debug, Default)]
|
||||
pub struct EventLog<E> {
|
||||
events: Vec<Envelope<E>>,
|
||||
}
|
||||
|
||||
impl<E> EventLog<E> {
|
||||
pub fn new() -> Self {
|
||||
Self { events: Vec::new() }
|
||||
}
|
||||
|
||||
/// Append with sequence enforcement: envelopes must arrive in strictly
|
||||
/// increasing `seq` order (GameKernel K11).
|
||||
pub fn append(&mut self, envelope: Envelope<E>) -> Result<(), LogError> {
|
||||
if let Some(last) = self.events.last() {
|
||||
if envelope.seq.0 != last.seq.0 + 1 {
|
||||
return Err(LogError::NonMonotonicSeq {
|
||||
expected: last.seq.0 + 1,
|
||||
got: envelope.seq.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
self.events.push(envelope);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.events.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.events.is_empty()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &Envelope<E>> {
|
||||
self.events.iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum LogError {
|
||||
NonMonotonicSeq { expected: u64, got: u64 },
|
||||
}
|
||||
|
||||
impl core::fmt::Display for LogError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
LogError::NonMonotonicSeq { expected, got } => {
|
||||
write!(f, "non-monotonic event seq: expected {expected}, got {got}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Canonical serialization: serde_json with struct-declaration field order
|
||||
/// and ordered maps (kernel state uses BTreeMap per GameKernel K6), so the
|
||||
/// same state always yields the same bytes.
|
||||
pub fn canonical_bytes<T: Serialize>(value: &T) -> Vec<u8> {
|
||||
serde_json::to_vec(value).expect("canonical serialization must not fail")
|
||||
}
|
||||
|
||||
/// SHA-256 state hash over the canonical serialization (GameKernel K7).
|
||||
pub fn state_hash<T: Serialize>(value: &T) -> [u8; 32] {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(canonical_bytes(value));
|
||||
hasher.finalize().into()
|
||||
}
|
||||
|
||||
/// Hex form of [`state_hash`], for scenario `expect.state_hash` fields.
|
||||
pub fn state_hash_hex<T: Serialize>(value: &T) -> String {
|
||||
let hash = state_hash(value);
|
||||
let mut out = String::with_capacity(64);
|
||||
for byte in hash {
|
||||
use core::fmt::Write;
|
||||
write!(out, "{byte:02x}").expect("writing to String cannot fail");
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A snapshot pairs the canonical state bytes with the last included
|
||||
/// event (GameKernel K9).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Snapshot {
|
||||
pub through: EventSeq,
|
||||
pub state: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Snapshot {
|
||||
pub fn take<T: Serialize>(state: &T, through: EventSeq) -> Self {
|
||||
Self {
|
||||
through,
|
||||
state: canonical_bytes(state),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restore<T: DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
|
||||
serde_json::from_slice(&self.state)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
fn log_rejects_seq_gaps() {
|
||||
let mut log: EventLog<u8> = EventLog::new();
|
||||
let env = |seq| Envelope {
|
||||
seq: EventSeq(seq),
|
||||
game_id: GameId(1),
|
||||
round: 1,
|
||||
schema_ver: 1,
|
||||
payload: 0u8,
|
||||
};
|
||||
log.append(env(0)).unwrap();
|
||||
log.append(env(1)).unwrap();
|
||||
assert_eq!(
|
||||
log.append(env(3)),
|
||||
Err(LogError::NonMonotonicSeq {
|
||||
expected: 2,
|
||||
got: 3
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// K7: identical state → identical hash; any change → different hash.
|
||||
#[test]
|
||||
fn state_hash_is_stable_and_sensitive() {
|
||||
let mut a = BTreeMap::new();
|
||||
a.insert("stress", 2u8);
|
||||
let mut b = BTreeMap::new();
|
||||
b.insert("stress", 2u8);
|
||||
assert_eq!(state_hash_hex(&a), state_hash_hex(&b));
|
||||
b.insert("stress", 3u8);
|
||||
assert_ne!(state_hash_hex(&a), state_hash_hex(&b));
|
||||
}
|
||||
|
||||
/// K9: snapshot → restore is identity on the canonical form.
|
||||
#[test]
|
||||
fn snapshot_roundtrip() {
|
||||
let mut state = BTreeMap::new();
|
||||
state.insert("round".to_string(), 3u8);
|
||||
let snap = Snapshot::take(&state, EventSeq(17));
|
||||
let restored: BTreeMap<String, u8> = snap.restore().unwrap();
|
||||
assert_eq!(state, restored);
|
||||
assert_eq!(state_hash_hex(&state), state_hash_hex(&restored));
|
||||
}
|
||||
}
|
||||
15
crates/cb-game-runtime/Cargo.toml
Normal file
15
crates/cb-game-runtime/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "cb-game-runtime"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license-file.workspace = true
|
||||
|
||||
[dependencies]
|
||||
cb-kernel.workspace = true
|
||||
cb-events.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
114
crates/cb-game-runtime/src/lib.rs
Normal file
114
crates/cb-game-runtime/src/lib.rs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
//! cb-game-runtime — round/phase machinery, commit windows, projections,
|
||||
//! and the scenario runner (GameKernel §2.5, §3). Game-agnostic.
|
||||
|
||||
pub mod scenario;
|
||||
|
||||
pub use scenario::{RunOutcome, ScenarioFile};
|
||||
|
||||
use cb_kernel::PlayerId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// A simultaneous commit window (GameKernel K12): the runtime opens it
|
||||
/// naming who must submit; submissions are commitment events hidden from
|
||||
/// projections until reveal.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommitWindow<C> {
|
||||
/// Players who must submit, and their commitment once received.
|
||||
pending: BTreeMap<PlayerId, Option<C>>,
|
||||
}
|
||||
|
||||
impl<C> CommitWindow<C> {
|
||||
pub fn open(players: impl IntoIterator<Item = PlayerId>) -> Self {
|
||||
Self {
|
||||
pending: players.into_iter().map(|p| (p, None)).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a commitment. Errors on players outside the window or on a
|
||||
/// duplicate submission (GameKernel K12).
|
||||
pub fn submit(&mut self, player: PlayerId, commitment: C) -> Result<(), CommitError> {
|
||||
match self.pending.get_mut(&player) {
|
||||
None => Err(CommitError::NotInWindow(player)),
|
||||
Some(slot @ None) => {
|
||||
*slot = Some(commitment);
|
||||
Ok(())
|
||||
}
|
||||
Some(Some(_)) => Err(CommitError::AlreadyCommitted(player)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.pending.values().all(Option::is_some)
|
||||
}
|
||||
|
||||
/// Consume the window at reveal, yielding commitments in PlayerId
|
||||
/// order (deterministic iteration, GameKernel K6).
|
||||
pub fn reveal(self) -> Result<Vec<(PlayerId, C)>, CommitError> {
|
||||
if !self.is_complete() {
|
||||
return Err(CommitError::Incomplete);
|
||||
}
|
||||
Ok(self
|
||||
.pending
|
||||
.into_iter()
|
||||
.map(|(p, c)| (p, c.expect("checked complete")))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum CommitError {
|
||||
NotInWindow(PlayerId),
|
||||
AlreadyCommitted(PlayerId),
|
||||
Incomplete,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for CommitError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
CommitError::NotInWindow(p) => write!(f, "player {p} is not in this window"),
|
||||
CommitError::AlreadyCommitted(p) => write!(f, "player {p} already committed"),
|
||||
CommitError::Incomplete => write!(f, "window is not complete"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-player projection (GameKernel K13): a total function from state to
|
||||
/// what one seat may see. Implemented by game packages; the runtime only
|
||||
/// fixes the shape so projections can never feed back into validation.
|
||||
pub trait Project {
|
||||
type View: Serialize;
|
||||
fn project(&self, viewer: Viewer) -> Self::View;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Viewer {
|
||||
Player(PlayerId),
|
||||
Spectator,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn commit_window_gates_and_reveals_in_order() {
|
||||
let mut w: CommitWindow<u8> = CommitWindow::open([PlayerId(0), PlayerId(1)]);
|
||||
assert_eq!(
|
||||
w.submit(PlayerId(2), 9),
|
||||
Err(CommitError::NotInWindow(PlayerId(2)))
|
||||
);
|
||||
w.submit(PlayerId(1), 7).unwrap();
|
||||
assert_eq!(
|
||||
w.submit(PlayerId(1), 8),
|
||||
Err(CommitError::AlreadyCommitted(PlayerId(1)))
|
||||
);
|
||||
assert!(!w.is_complete());
|
||||
w.submit(PlayerId(0), 5).unwrap();
|
||||
assert!(w.is_complete());
|
||||
assert_eq!(
|
||||
w.reveal().unwrap(),
|
||||
vec![(PlayerId(0), 5), (PlayerId(1), 7)]
|
||||
);
|
||||
}
|
||||
}
|
||||
131
crates/cb-game-runtime/src/scenario.rs
Normal file
131
crates/cb-game-runtime/src/scenario.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
//! Scenario file format and runner scaffold (MetricsAndScenarios §2,
|
||||
//! GameKernel K17). The format parses fully; execution against a game
|
||||
//! aggregate is wired up in T08 — until then `run` reports Unimplemented.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// One scenario file (`scenarios/<game>/<slug>.yaml`).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ScenarioFile {
|
||||
pub scenario: String,
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
/// Numbered rule IDs this scenario covers (feeds M-D1-COV / AM-1).
|
||||
pub covers: Vec<String>,
|
||||
/// Marks scenarios encoding a PROVISIONAL U-item default
|
||||
/// (GroundRules §Underdetermined): a ground-game ruling flips the
|
||||
/// scenario, not the kernel.
|
||||
#[serde(default)]
|
||||
pub provisional: bool,
|
||||
pub seed: u64,
|
||||
pub setup: Setup,
|
||||
pub commands: Vec<CommandStep>,
|
||||
pub expect: Expect,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Setup {
|
||||
pub players: u8,
|
||||
/// Named setup preset from the game spec (e.g. "standard-3p").
|
||||
pub preset: String,
|
||||
/// Explicit state overrides applied after the preset (dot-paths).
|
||||
#[serde(default)]
|
||||
pub patch: BTreeMap<String, serde_yaml::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CommandStep {
|
||||
pub actor: String,
|
||||
pub cmd: String,
|
||||
#[serde(default)]
|
||||
pub args: BTreeMap<String, serde_yaml::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Expect {
|
||||
/// Ordered subsequence of event kinds that must occur.
|
||||
#[serde(default)]
|
||||
pub events: Vec<BTreeMap<String, serde_yaml::Value>>,
|
||||
/// Partial end-state assertions: dot-path → expected value.
|
||||
#[serde(default)]
|
||||
pub state: BTreeMap<String, serde_yaml::Value>,
|
||||
/// Indices into `commands` that must be rejected.
|
||||
#[serde(default)]
|
||||
pub rejects: Vec<usize>,
|
||||
/// Optional full-state golden hash.
|
||||
#[serde(default)]
|
||||
pub state_hash: Option<String>,
|
||||
}
|
||||
|
||||
impl ScenarioFile {
|
||||
pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml::Error> {
|
||||
serde_yaml::from_str(yaml)
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of executing one scenario (twice, per the K8 double-run rule).
|
||||
#[derive(Debug)]
|
||||
pub enum RunOutcome {
|
||||
Passed {
|
||||
covers: Vec<String>,
|
||||
},
|
||||
Failed {
|
||||
reason: String,
|
||||
},
|
||||
/// Scaffold state: parsing works, execution lands in T08.
|
||||
Unimplemented,
|
||||
}
|
||||
|
||||
/// Runner scaffold. T08 replaces the body with: build aggregate from
|
||||
/// setup, apply commands via validate/fold, check expectations, run twice
|
||||
/// and compare hashes, emit a replay bundle on failure.
|
||||
pub fn run(_scenario: &ScenarioFile) -> RunOutcome {
|
||||
RunOutcome::Unimplemented
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const SAMPLE: &str = r#"
|
||||
scenario: ground/smoke-setup
|
||||
description: Parses the documented scenario shape end to end.
|
||||
covers: [GR-S02, GR-S03]
|
||||
provisional: false
|
||||
seed: 42
|
||||
setup:
|
||||
players: 3
|
||||
preset: standard-3p
|
||||
patch:
|
||||
"players.0.stress": 4
|
||||
commands:
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args: { action: ATTACK, target: P2 }
|
||||
expect:
|
||||
state:
|
||||
"round": 1
|
||||
rejects: []
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn scenario_format_roundtrips() {
|
||||
let parsed = ScenarioFile::from_yaml(SAMPLE).unwrap();
|
||||
assert_eq!(parsed.scenario, "ground/smoke-setup");
|
||||
assert_eq!(parsed.covers, vec!["GR-S02", "GR-S03"]);
|
||||
assert_eq!(parsed.setup.players, 3);
|
||||
assert_eq!(parsed.commands.len(), 1);
|
||||
assert!(matches!(run(&parsed), RunOutcome::Unimplemented));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_fields_are_rejected() {
|
||||
let bad = SAMPLE.replace("seed: 42", "seed: 42\ntypo_field: 1");
|
||||
assert!(ScenarioFile::from_yaml(&bad).is_err());
|
||||
}
|
||||
}
|
||||
13
crates/cb-kernel/Cargo.toml
Normal file
13
crates/cb-kernel/Cargo.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "cb-kernel"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license-file.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
rand_core.workspace = true
|
||||
rand_chacha.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
45
crates/cb-kernel/src/ids.rs
Normal file
45
crates/cb-kernel/src/ids.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
//! Canonical identifiers (GameKernel §2.1): newtyped, copyable, ordered.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
macro_rules! id_type {
|
||||
($(#[$doc:meta])* $name:ident($inner:ty)) => {
|
||||
$(#[$doc])*
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
#[serde(transparent)]
|
||||
pub struct $name(pub $inner);
|
||||
|
||||
impl core::fmt::Display for $name {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
id_type!(
|
||||
/// A single game instance.
|
||||
GameId(u64)
|
||||
);
|
||||
id_type!(
|
||||
/// Seat index within a game (0-based).
|
||||
PlayerId(u8)
|
||||
);
|
||||
id_type!(
|
||||
/// Semantic entity: problem, token, relation, card.
|
||||
EntityId(u32)
|
||||
);
|
||||
id_type!(
|
||||
/// Client-assigned command identifier for idempotency (GameKernel K2).
|
||||
CommandId(u64)
|
||||
);
|
||||
id_type!(
|
||||
/// Monotonic per-game event sequence number (GameKernel K4).
|
||||
EventSeq(u64)
|
||||
);
|
||||
id_type!(
|
||||
/// Deterministic game seed (GameKernel K5).
|
||||
Seed(u64)
|
||||
);
|
||||
10
crates/cb-kernel/src/lib.rs
Normal file
10
crates/cb-kernel/src/lib.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
//! cb-kernel — canonical identifiers, seeds, RNG service, and the
|
||||
//! command/event/fold contracts (GameKernel §2.1–2.3). Game-agnostic.
|
||||
|
||||
pub mod ids;
|
||||
pub mod pipeline;
|
||||
pub mod rng;
|
||||
|
||||
pub use ids::{CommandId, EntityId, EventSeq, GameId, PlayerId, Seed};
|
||||
pub use pipeline::{Actor, Aggregate, Rejection};
|
||||
pub use rng::{ChaChaRng, KernelRng, NullRng};
|
||||
54
crates/cb-kernel/src/pipeline.rs
Normal file
54
crates/cb-kernel/src/pipeline.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
//! The mutation pipeline contracts (GameKernel §2.2, K1–K3):
|
||||
//! command → validate → events → infallible fold.
|
||||
|
||||
use crate::ids::PlayerId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Who issued a command (GameKernel K2).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub enum Actor {
|
||||
Player(PlayerId),
|
||||
/// Runtime-driven steps (reveal, resolution ordering, round end).
|
||||
System,
|
||||
}
|
||||
|
||||
/// Typed, testable command rejections (GameKernel K3). Game packages add
|
||||
/// specific rejection kinds via `Game`-typed payloads in their own enums;
|
||||
/// this kernel-level enum carries the cross-game cases.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Rejection {
|
||||
/// The command's `CommandId` was already applied (idempotency, K2).
|
||||
DuplicateCommand,
|
||||
/// The actor may not issue this command now (turn/window gating).
|
||||
NotAllowedNow,
|
||||
/// Game-specific rejection, stable machine-readable code + human text.
|
||||
Game { code: String, detail: String },
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Rejection {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Rejection::DuplicateCommand => write!(f, "duplicate command"),
|
||||
Rejection::NotAllowedNow => write!(f, "not allowed now"),
|
||||
Rejection::Game { code, detail } => write!(f, "{code}: {detail}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An event-sourced aggregate (GameKernel K1): all validation happens in
|
||||
/// `validate`; `fold` is total and infallible for any logged event.
|
||||
pub trait Aggregate: Sized {
|
||||
type Command;
|
||||
type Event;
|
||||
|
||||
/// Decide whether `command` is legal in the current state, producing
|
||||
/// the events it implies. Must not mutate state.
|
||||
fn validate(
|
||||
&self,
|
||||
actor: Actor,
|
||||
command: &Self::Command,
|
||||
) -> Result<Vec<Self::Event>, Rejection>;
|
||||
|
||||
/// Apply one event. Infallible: every logged event must apply (K1).
|
||||
fn fold(&mut self, event: &Self::Event);
|
||||
}
|
||||
83
crates/cb-kernel/src/rng.rs
Normal file
83
crates/cb-kernel/src/rng.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
//! Seeded RNG service (GameKernel K5) with the null/reference impl pair
|
||||
//! required by AM-11. The only randomness source available to game code.
|
||||
|
||||
use crate::ids::Seed;
|
||||
use rand_core::{RngCore, SeedableRng};
|
||||
|
||||
/// Kernel randomness: deterministic draws from the game seed.
|
||||
pub trait KernelRng {
|
||||
/// Uniform draw in `0..bound` (bound ≥ 1).
|
||||
fn draw(&mut self, bound: u32) -> u32;
|
||||
|
||||
/// Deterministic Fisher–Yates shuffle of `items`.
|
||||
fn shuffle<T>(&mut self, items: &mut [T]) {
|
||||
for i in (1..items.len()).rev() {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
let j = self.draw(i as u32 + 1) as usize;
|
||||
items.swap(i, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reference implementation: ChaCha12, seeded from the game `Seed`.
|
||||
pub struct ChaChaRng(rand_chacha::ChaCha12Rng);
|
||||
|
||||
impl ChaChaRng {
|
||||
pub fn from_seed(seed: Seed) -> Self {
|
||||
Self(rand_chacha::ChaCha12Rng::seed_from_u64(seed.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl KernelRng for ChaChaRng {
|
||||
fn draw(&mut self, bound: u32) -> u32 {
|
||||
assert!(bound >= 1, "draw bound must be >= 1");
|
||||
// Rejection sampling for uniformity.
|
||||
let zone = u32::MAX - (u32::MAX % bound);
|
||||
loop {
|
||||
let v = self.0.next_u32();
|
||||
if v < zone {
|
||||
return v % bound;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Null implementation for tests: always returns 0 (first choice, no
|
||||
/// shuffle movement). Makes scenario fixtures fully predictable.
|
||||
#[derive(Default)]
|
||||
pub struct NullRng;
|
||||
|
||||
impl KernelRng for NullRng {
|
||||
fn draw(&mut self, _bound: u32) -> u32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// AM-8 seed determinism at the unit level: same seed, same draws.
|
||||
#[test]
|
||||
fn chacha_is_deterministic_per_seed() {
|
||||
let mut a = ChaChaRng::from_seed(Seed(42));
|
||||
let mut b = ChaChaRng::from_seed(Seed(42));
|
||||
let mut c = ChaChaRng::from_seed(Seed(43));
|
||||
let draws_a: Vec<u32> = (0..64).map(|_| a.draw(1000)).collect();
|
||||
let draws_b: Vec<u32> = (0..64).map(|_| b.draw(1000)).collect();
|
||||
let draws_c: Vec<u32> = (0..64).map(|_| c.draw(1000)).collect();
|
||||
assert_eq!(draws_a, draws_b);
|
||||
assert_ne!(draws_a, draws_c);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn null_rng_never_moves_a_shuffle() {
|
||||
let mut rng = NullRng;
|
||||
let mut items = vec![1, 2, 3, 4];
|
||||
// Fisher–Yates with j=0 each step rotates deterministically.
|
||||
rng.shuffle(&mut items);
|
||||
let mut again = vec![1, 2, 3, 4];
|
||||
NullRng.shuffle(&mut again);
|
||||
assert_eq!(items, again);
|
||||
}
|
||||
}
|
||||
21
games/ground/Cargo.toml
Normal file
21
games/ground/Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "games-ground"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license-file.workspace = true
|
||||
|
||||
[dependencies]
|
||||
cb-kernel.workspace = true
|
||||
cb-events.workspace = true
|
||||
cb-game-runtime.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
criterion.workspace = true
|
||||
|
||||
[[bench]]
|
||||
name = "synthetic"
|
||||
harness = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
49
games/ground/benches/synthetic.rs
Normal file
49
games/ground/benches/synthetic.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
//! Criterion skeleton for AM-6/AM-7 (GameKernel §4), wired to the
|
||||
//! CB-RES-0001 baseline shape (3-player commit/reveal synthetic workload).
|
||||
//! T08 replaces the placeholder body with the real aggregate loop; the
|
||||
//! bench IDs and workload sizes are fixed here so results stay comparable
|
||||
//! to benchmarks/baselines/ recordings.
|
||||
|
||||
use cb_events::state_hash;
|
||||
use cb_game_runtime::CommitWindow;
|
||||
use cb_kernel::PlayerId;
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
|
||||
|
||||
/// Placeholder workload: open/submit/reveal one 3-player commit window and
|
||||
/// hash a small value. Exists so the bench harness, IDs, and baseline
|
||||
/// wiring compile and run before the aggregate exists.
|
||||
fn commit_reveal_round() -> usize {
|
||||
let mut w: CommitWindow<u8> = CommitWindow::open([PlayerId(0), PlayerId(1), PlayerId(2)]);
|
||||
for p in 0..3u8 {
|
||||
w.submit(PlayerId(p), p).unwrap();
|
||||
}
|
||||
let revealed = w.reveal().unwrap();
|
||||
state_hash(&revealed.len()).len()
|
||||
}
|
||||
|
||||
fn bench_synthetic(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("synthetic-ground-3p");
|
||||
// AM-6 headline workload sizes; AM-7 compares 5k vs 100k throughput.
|
||||
for &rounds in &[5_000usize, 100_000] {
|
||||
group.bench_function(format!("commit-reveal-{rounds}"), |b| {
|
||||
b.iter_batched(
|
||||
|| rounds,
|
||||
|n| {
|
||||
let mut acc = 0usize;
|
||||
for _ in 0..n / 1000 {
|
||||
// Scaffold runs 1/1000th scale until T08 wires the
|
||||
// real aggregate; the group/ID layout is what T07
|
||||
// delivers.
|
||||
acc += commit_reveal_round();
|
||||
}
|
||||
acc
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_synthetic);
|
||||
criterion_main!(benches);
|
||||
119
games/ground/src/lib.rs
Normal file
119
games/ground/src/lib.rs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
//! games-ground — the GROUND rules aggregate (specs/GroundRules.md,
|
||||
//! GameKernel K15–K16). T07 scaffolds the state shell; validate/fold per
|
||||
//! GR-rule land in T08 with rule IDs cross-referenced in doc comments.
|
||||
|
||||
use cb_kernel::PlayerId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// GR-O02: per-player state.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PlayerState {
|
||||
/// GR-F01: clamped 0–5.
|
||||
pub stress: u8,
|
||||
/// GR-F03.
|
||||
pub freedom_ready: bool,
|
||||
/// GR-D01/D02: OFF or the pending/active stage.
|
||||
pub darvo: DarvoStage,
|
||||
pub hand: Vec<SolutionCard>,
|
||||
pub protection: u8,
|
||||
/// Blame tokens in front of this player (GR-T02), keyed by owner.
|
||||
pub blame_from: Vec<PlayerId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DarvoStage {
|
||||
Off,
|
||||
Deny,
|
||||
Attack,
|
||||
Reverse,
|
||||
}
|
||||
|
||||
/// GR-O04: one Problem card's live state.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ProblemState {
|
||||
pub suit: Suit,
|
||||
pub value: u8,
|
||||
pub face_up: bool,
|
||||
pub denied: bool,
|
||||
pub claimed_by: Option<PlayerId>,
|
||||
/// GR-A11: protected from Deny this round by GROUND—OU.
|
||||
pub protected_this_round: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub enum Suit {
|
||||
Clarify,
|
||||
Repair,
|
||||
Boundary,
|
||||
Change,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SolutionCard {
|
||||
pub suit: Suit,
|
||||
}
|
||||
|
||||
/// GR-O05: at most one relation per pair; endpoints ordered low→high.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Relation {
|
||||
Bond,
|
||||
Rivalry,
|
||||
}
|
||||
|
||||
/// GR-O01..O05: the authoritative GROUND aggregate. Fields use ordered
|
||||
/// collections only (GameKernel K6).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct GroundState {
|
||||
pub round: u8,
|
||||
pub lead: PlayerId,
|
||||
pub players: BTreeMap<PlayerId, PlayerState>,
|
||||
/// Keyed by (low, high) player pair.
|
||||
pub relations: BTreeMap<(PlayerId, PlayerId), Relation>,
|
||||
pub problems: BTreeMap<u32, ProblemState>,
|
||||
pub solution_deck: Vec<SolutionCard>,
|
||||
pub solution_discard: Vec<SolutionCard>,
|
||||
/// Focus placements: sequence owner → target (GR-T03).
|
||||
pub focus: BTreeMap<PlayerId, PlayerId>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cb_events::state_hash_hex;
|
||||
|
||||
fn tiny_state() -> GroundState {
|
||||
GroundState {
|
||||
round: 1,
|
||||
lead: PlayerId(0),
|
||||
players: BTreeMap::from([(
|
||||
PlayerId(0),
|
||||
PlayerState {
|
||||
stress: 2,
|
||||
freedom_ready: true,
|
||||
darvo: DarvoStage::Off,
|
||||
hand: vec![SolutionCard { suit: Suit::Repair }],
|
||||
protection: 0,
|
||||
blame_from: vec![],
|
||||
},
|
||||
)]),
|
||||
relations: BTreeMap::new(),
|
||||
problems: BTreeMap::new(),
|
||||
solution_deck: vec![],
|
||||
solution_discard: vec![],
|
||||
focus: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// K7 on the real aggregate: hash stable across clones, sensitive to
|
||||
/// semantic change.
|
||||
#[test]
|
||||
fn ground_state_hashes_canonically() {
|
||||
let a = tiny_state();
|
||||
let b = a.clone();
|
||||
assert_eq!(state_hash_hex(&a), state_hash_hex(&b));
|
||||
let mut c = a.clone();
|
||||
c.players.get_mut(&PlayerId(0)).unwrap().stress = 5;
|
||||
assert_ne!(state_hash_hex(&a), state_hash_hex(&c));
|
||||
}
|
||||
}
|
||||
3
rust-toolchain.toml
Normal file
3
rust-toolchain.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[toolchain]
|
||||
channel = "1.97.1"
|
||||
components = ["rustfmt", "clippy"]
|
||||
17
scenarios/ground/smoke-setup.yaml
Normal file
17
scenarios/ground/smoke-setup.yaml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
scenario: ground/smoke-setup
|
||||
description: >
|
||||
Smoke scenario for the T07 scaffold: exercises the documented file format
|
||||
end to end. Assertions are the GR-S setup facts; execution lands in T08.
|
||||
covers: [GR-S02, GR-S03, GR-O01]
|
||||
provisional: false
|
||||
seed: 42
|
||||
setup:
|
||||
players: 3
|
||||
preset: standard-3p
|
||||
commands: []
|
||||
expect:
|
||||
state:
|
||||
"round": 1
|
||||
"players.0.stress": 2
|
||||
"players.0.freedom_ready": true
|
||||
rejects: []
|
||||
12
tools/cb-sim/Cargo.toml
Normal file
12
tools/cb-sim/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "cb-sim"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license-file.workspace = true
|
||||
|
||||
[dependencies]
|
||||
cb-game-runtime.workspace = true
|
||||
games-ground.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
59
tools/cb-sim/src/main.rs
Normal file
59
tools/cb-sim/src/main.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
//! cb-sim — scenario runner binary (GameKernel K17; precursor of `cb sim`).
|
||||
//! T07 scope: parse and validate scenario files, report coverage tags.
|
||||
//! T08 wires execution. Exit codes: 0 all passed, 1 failures, 2 not yet
|
||||
//! executable (parse-only), 64 usage error.
|
||||
|
||||
use cb_game_runtime::{scenario, RunOutcome, ScenarioFile};
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
if args.is_empty() {
|
||||
eprintln!("usage: cb-sim <scenario.yaml>...");
|
||||
std::process::exit(64);
|
||||
}
|
||||
|
||||
let mut unimplemented = false;
|
||||
let mut failed = false;
|
||||
|
||||
for path in &args {
|
||||
let text = match std::fs::read_to_string(path) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
eprintln!("{path}: read error: {e}");
|
||||
failed = true;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match ScenarioFile::from_yaml(&text) {
|
||||
Err(e) => {
|
||||
eprintln!("{path}: parse error: {e}");
|
||||
failed = true;
|
||||
}
|
||||
Ok(sc) => match scenario::run(&sc) {
|
||||
RunOutcome::Passed { covers } => {
|
||||
println!("PASS {} covers={}", sc.scenario, covers.join(","));
|
||||
}
|
||||
RunOutcome::Failed { reason } => {
|
||||
println!("FAIL {} — {reason}", sc.scenario);
|
||||
failed = true;
|
||||
}
|
||||
RunOutcome::Unimplemented => {
|
||||
println!(
|
||||
"PARSED {} covers={}{} (runner not yet implemented — T08)",
|
||||
sc.scenario,
|
||||
sc.covers.join(","),
|
||||
if sc.provisional { " provisional" } else { "" }
|
||||
);
|
||||
unimplemented = true;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if failed {
|
||||
std::process::exit(1);
|
||||
}
|
||||
if unimplemented {
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue