Add tests, fix offset logic, and bump dependencies

- Introduce a comprehensive test suite (cli, app, board, tasks, audio)
  and a
  new TESTING.md document describing test execution.
- Correct vertical offset calculation in `BoardState` to use saturating
  subtraction and ceiling division.
- Clamp initial `Offset` values to the provided maximum in
  `Offset::new`.
- Export `load_audio` from the audio thread module and switch to
  `LoopedDecoder` for proper OGG looping.
- Update several crate versions: clap, ratatui, log, rodio, and
  bitflags.
This commit is contained in:
GarandPLG committed 2026-10-18 20:29:04 +02:00
1 parent 9bc42dc898
commit fcdbbaf74c
16 files changed
+796 -225

No files matched your search

Generated
+211 -198
View File
@@ -24,7 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d"
dependencies = [
"alsa-sys",
"bitflags 2.11.0",
"bitflags 2.13.2",
"cfg-if",
"libc",
]
@@ -56,9 +56,9 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
@@ -75,7 +75,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
"windows-sys",
]
[[package]]
@@ -86,7 +86,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
"windows-sys",
]
[[package]]
@@ -95,6 +95,15 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "approx"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [
"num-traits",
]
[[package]]
name = "arrayvec"
version = "0.7.6"
@@ -145,9 +154,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.11.0"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "block-buffer"
@@ -173,6 +182,12 @@ version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "by_address"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
[[package]]
name = "bytemuck"
version = "1.25.0"
@@ -194,12 +209,6 @@ dependencies = [
"rustversion",
]
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cfg-if"
version = "1.0.4"
@@ -225,9 +234,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.6.0"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
dependencies = [
"clap_builder",
"clap_derive",
@@ -235,9 +244,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.6.0"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
dependencies = [
"anstream",
"anstyle",
@@ -247,14 +256,14 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.6.0"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.117",
"syn 3.0.9",
]
[[package]]
@@ -304,11 +313,11 @@ dependencies = [
[[package]]
name = "coreaudio-rs"
version = "0.14.1"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16dd574a72a021b90c7656c474ea31d11a2f0366a8eff574186e761e0b9e3586"
checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.13.2",
"libc",
"objc2-audio-toolbox",
"objc2-core-audio",
@@ -318,11 +327,12 @@ dependencies = [
[[package]]
name = "cpal"
version = "0.17.3"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7"
checksum = "6f02e8d0327b42d3e2e4ab2119af397344eb9fc54a34bf0ddeaa1277af8681f1"
dependencies = [
"alsa",
"block2",
"coreaudio-rs",
"dasp_sample",
"jni",
@@ -340,10 +350,9 @@ dependencies = [
"objc2-core-audio-types",
"objc2-core-foundation",
"objc2-foundation",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows",
"windows-core",
]
[[package]]
@@ -364,13 +373,19 @@ dependencies = [
"libc",
]
[[package]]
name = "critical-section"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "crossterm"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.13.2",
"crossterm_winapi",
"derive_more",
"document-features",
@@ -504,7 +519,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.13.2",
"objc2",
]
@@ -545,7 +560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys",
]
[[package]]
@@ -614,30 +629,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -694,6 +685,17 @@ dependencies = [
"foldhash 0.2.0",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
]
[[package]]
name = "heck"
version = "0.5.0"
@@ -775,18 +777,32 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jni"
version = "0.21.1"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
dependencies = [
"cesu8",
"cfg-if",
"combine",
"jni-sys 0.3.1",
"jni-macros",
"jni-sys 0.4.1",
"log",
"thiserror 1.0.69",
"simd_cesu8",
"thiserror 2.0.18",
"walkdir",
"windows-sys 0.45.0",
"windows-link",
]
[[package]]
name = "jni-macros"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"simd_cesu8",
"syn 2.0.117",
]
[[package]]
@@ -823,8 +839,6 @@ version = "0.3.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
dependencies = [
"cfg-if",
"futures-util",
"once_cell",
"wasm-bindgen",
]
@@ -876,7 +890,7 @@ version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.13.2",
]
[[package]]
@@ -902,17 +916,17 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.29"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lru"
version = "0.16.3"
version = "0.18.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5"
dependencies = [
"hashbrown 0.16.1",
"hashbrown 0.17.1",
]
[[package]]
@@ -927,12 +941,9 @@ dependencies = [
[[package]]
name = "mach2"
version = "0.5.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
dependencies = [
"libc",
]
checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b"
[[package]]
name = "memchr"
@@ -970,7 +981,7 @@ dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.61.2",
"windows-sys",
]
[[package]]
@@ -979,7 +990,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.13.2",
"jni-sys 0.3.1",
"log",
"ndk-sys",
@@ -1008,7 +1019,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.13.2",
"cfg-if",
"cfg_aliases",
"libc",
@@ -1128,7 +1139,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.13.2",
"libc",
"objc2",
"objc2-core-audio",
@@ -1143,6 +1154,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"
dependencies = [
"bitflags 2.13.2",
"objc2",
"objc2-foundation",
]
@@ -1166,7 +1178,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.13.2",
"objc2",
]
@@ -1176,7 +1188,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.13.2",
"block2",
"dispatch2",
"libc",
@@ -1195,7 +1207,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.13.2",
"block2",
"libc",
"objc2",
@@ -1223,6 +1235,39 @@ dependencies = [
"num-traits",
]
[[package]]
name = "palette"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddeed8580d347d2abf3dcf06a5f0b3dc020258338526b277847cd4248a70fc64"
dependencies = [
"approx",
"libm",
"palette_derive",
"palette_math",
]
[[package]]
name = "palette_derive"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88537020289b719d81be994ccf1bbf4990f477e2f69ee52fe3e45f43a02e56be"
dependencies = [
"by_address",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "palette_math"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e6eb142958d64335fb0e345c5b9ead2ecd6fc438c307e9d7d3c4fd428dbaf12"
dependencies = [
"libm",
]
[[package]]
name = "parking_lot"
version = "0.12.5"
@@ -1341,12 +1386,6 @@ dependencies = [
"siphasher",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkg-config"
version = "0.3.32"
@@ -1452,31 +1491,35 @@ dependencies = [
[[package]]
name = "ratatui"
version = "0.30.0"
version = "0.30.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc"
checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d"
dependencies = [
"instability",
"ratatui-core",
"ratatui-crossterm",
"ratatui-macros",
"ratatui-termina",
"ratatui-termwiz",
"ratatui-widgets",
"serde",
]
[[package]]
name = "ratatui-core"
version = "0.1.0"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293"
checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.13.2",
"compact_str",
"hashbrown 0.16.1",
"indoc",
"critical-section",
"hashbrown 0.17.1",
"itertools",
"kasuari",
"lru",
"palette",
"serde",
"strum",
"thiserror 2.0.18",
"unicode-segmentation",
@@ -1486,9 +1529,9 @@ dependencies = [
[[package]]
name = "ratatui-crossterm"
version = "0.1.0"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3"
checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0"
dependencies = [
"cfg-if",
"crossterm",
@@ -1498,19 +1541,30 @@ dependencies = [
[[package]]
name = "ratatui-macros"
version = "0.7.0"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4"
checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814"
dependencies = [
"ratatui-core",
"ratatui-widgets",
]
[[package]]
name = "ratatui-termwiz"
name = "ratatui-termina"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c"
checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2"
dependencies = [
"instability",
"ratatui-core",
"termina",
]
[[package]]
name = "ratatui-termwiz"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977"
dependencies = [
"ratatui-core",
"termwiz",
@@ -1518,17 +1572,18 @@ dependencies = [
[[package]]
name = "ratatui-widgets"
version = "0.3.0"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db"
checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1"
dependencies = [
"bitflags 2.11.0",
"hashbrown 0.16.1",
"bitflags 2.13.2",
"hashbrown 0.17.1",
"indoc",
"instability",
"itertools",
"line-clipping",
"ratatui-core",
"serde",
"strum",
"time",
"unicode-segmentation",
@@ -1541,7 +1596,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.13.2",
]
[[package]]
@@ -1575,9 +1630,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rodio"
version = "0.22.2"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a536bb79db59098ef71a4dd4246c02eb87b316deceb1b68e0cde7167ec01eb"
checksum = "36e3ee0a5ca418bac59a15d5506e498ab138359edbb67257a6b8a3b93d411f39"
dependencies = [
"cpal",
"dasp_sample",
@@ -1610,11 +1665,11 @@ version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.13.2",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
"windows-sys",
]
[[package]]
@@ -1735,6 +1790,22 @@ dependencies = [
"libc",
]
[[package]]
name = "simd_cesu8"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
dependencies = [
"rustc_version",
"simdutf8",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "simplelog"
version = "0.12.2"
@@ -1752,12 +1823,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.1"
@@ -1778,18 +1843,18 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.27.2"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.27.2"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
dependencies = [
"heck",
"proc-macro2",
@@ -1966,6 +2031,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "termcolor"
version = "1.4.1"
@@ -1975,6 +2051,19 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "termina"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e"
dependencies = [
"bitflags 2.13.2",
"parking_lot",
"rustix",
"signal-hook",
"windows-sys",
]
[[package]]
name = "terminfo"
version = "0.9.0"
@@ -2004,7 +2093,7 @@ checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7"
dependencies = [
"anyhow",
"base64",
"bitflags 2.11.0",
"bitflags 2.13.2",
"fancy-regex",
"filedescriptor",
"finl_unicode",
@@ -2280,16 +2369,6 @@ dependencies = [
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.117"
@@ -2350,7 +2429,7 @@ version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.13.2",
"hashbrown 0.15.5",
"indexmap",
"semver",
@@ -2460,7 +2539,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
"windows-sys",
]
[[package]]
@@ -2570,15 +2649,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -2588,21 +2658,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
@@ -2612,48 +2667,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "winnow"
version = "1.0.1"
@@ -2721,7 +2734,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags 2.11.0",
"bitflags 2.13.2",
"indexmap",
"log",
"serde",
+4 -4
View File
@@ -10,9 +10,9 @@ opt-level = 3
strip = true
[dependencies]
clap = { version = "4.5.53", features = ["derive"] }
ratatui = "0.30.0"
log = "0.4.29"
clap = { version = "4.6.7", features = ["derive"] }
ratatui = "0.30.2"
log = "0.4.34"
simplelog = "0.12.2"
rodio = "0.22.2"
rodio = "0.23.0"
once_cell = "1.21.4"
+1 -1
View File
@@ -25,7 +25,7 @@ nix develop -c cargo run -- --disable-audio --view skirmish
Ostatnie polecenie wymaga interaktywnego terminala. `shellHook` sam wykonuje `cargo run -- -h`, więc wejście do środowiska także kompiluje program i drukuje pomoc. Formatowanie stosuj wyłącznie do uzgodnionych zmian; nie używaj automatycznego `clippy --fix` do porządkowania całego repozytorium.
Stan bazowy: testy i formatowanie przechodzą; testów jest zero, Clippy zgłasza 54 ostrzeżenia dla biblioteki. Nie traktuj tego jako potwierdzenia działania rozgrywki. Nowa nietrywialna logika wymaga małego testu regresji. Nie dodawaj zależności ani warstw abstrakcji bez potrzeby.
Stan bazowy po dodaniu zestawu testów: 23 testy i formatowanie przechodzą; Clippy zgłasza 51 ostrzeżeń dla biblioteki. Zakres i polecenia opisano w [TESTING.md](TESTING.md). Nie traktuj tego jako potwierdzenia działania pełnej rozgrywki. Nowa nietrywialna logika wymaga małego testu regresji. Nie dodawaj zależności ani warstw abstrakcji bez potrzeby.
Audio wymaga ALSA i urządzenia odtwarzania; do pracy nad TUI używaj `--disable-audio`. `--mute` nadal uruchamia wątek audio. Ścieżki zasobów i pakowanie sprawdzaj osobno przy zmianach audio/Nix. Rozróżniaj lokalny build Cargo, pakiet `.#develop` i domyślny pakiet Nix.
+5 -3
View File
@@ -88,6 +88,8 @@ Projektowane wykonanie: `advance_turn` skanuje komórki, wyjmuje jednostkę prze
Główny wątek wysyła `AudioCmd`; osobny wątek obsługuje głośność, mute, część i soundtrack. Cache wykorzystuje `Lazy`, `RwLock`, `Arc` i bajty OGG. Calm ładuje `calm.ogg`; Buildup, Assault i Outro korzystają obecnie z `test.ogg`. Błędy urządzenia, odczytu i dekodowania mogą zakończyć wątek przez `expect`.
Rodio zapętla źródło przez `Decoder::new_looped`. Wątek sterujący blokuje się na odbiorniku (`for cmd in rx`), zamiast aktywnie sprawdzać komendy i koniec utworu. Zmiana części lub soundtracku dopisuje zapętlone źródło i pomija bieżące przez `skip_one`. Zamknięcie kanału kończy wątek. Test `audio_repeats_after_end_of_file` sprawdza przekroczenie końca rzeczywistego pliku OGG bez urządzenia audio.
`flake.nix` dostarcza devShell dla `x86_64-linux`, domyślny pakiet z `default.nix` i pakiet `.#develop` budowany przez Naersk. Obie ścieżki pakowania kopiują soundtracki do `$out/bin` i zmieniają nazwę binarki na `war-in-tunnels`. `default.nix` ma stały `cargoHash`; po zmianie zależności wymaga osobnej weryfikacji. Samo `cargo test` nie sprawdza pakowania.
## Punkty wymagające uwagi przed dalszą implementacją
@@ -95,12 +97,12 @@ Główny wątek wysyła `AudioCmd`; osobny wątek obsługuje głośność, mute,
To obserwacje statyczne, nie lista napraw wykonanych w tej sesji:
- **Symulacja wyłączona:** `SkirmishState::tick_update` nie wywołuje `advance_turn`.
- **Postęp zadania:** `DiggingTask::progress` oblicza nową wartość i ją odrzuca (`let _ = ...`); nie aktualizuje pola `progress`.
- **Postęp zadania:** naprawiono zapisywanie przyrostu i ograniczenie do 0–100; zachowanie jest objęte testami w `tests/tasks.rs`. Nie oznacza to uruchomienia symulacji.
- **Ruch w szkicu symulacji:** `advance_turn` pobiera cel bez warunku osiągnięcia postępu, nie aktualizuje współrzędnych jednostki i nie usuwa zakończonego zadania z kolejki. Jednostka przeniesiona na późniejszą komórkę może zostać obsłużona ponownie w tym samym skanowaniu. Zapis celu może nadpisać inną jednostkę i strukturę. Przed aktywacją potrzebne są reguły kolizji i testy pojedynczej tury.
- **Zaznaczenie:** jednostka startowa jest zapamiętywana oddzielnie; przy uruchomieniu ruchu trzeba sprawdzić ważność jej pozycji i zachowanie dla pustej komórki oraz jednostki przeciwnika.
- **Audio:** pętla `handle_audio` wymaga sprawdzenia zużycia CPU przy braku komend; testy Cargo nie weryfikują urządzenia ani odtwarzania.
- **Audio:** usunięto aktywne odpytywanie; pomiar CPU na docelowym urządzeniu i odsłuch pozostają do wykonania. Test dekodera nie weryfikuje urządzenia audio.
- **Granice UI:** małe terminale, resize i trzy poziomy zoomu wymagają interaktywnego sprawdzenia. Indeksowanie planszy zakłada poprawne współrzędne.
- **Pokrycie:** brak testów jednostkowych/integracyjnych i konfiguracji CI w analizowanym drzewie. Clippy zgłasza 54 ostrzeżenia; nie uruchamiać automatycznych poprawek poza zakresem zadania.
- **Pokrycie:** 23 testy CLI, sterowania, renderowania, planszy, zadań i dekodowania audio; szczegóły w [TESTING.md](TESTING.md). Brak testów pełnej symulacji i konfiguracji CI. Clippy zgłasza 51 ostrzeżeń; nie uruchamiać automatycznych poprawek poza zakresem zadania.
## Gdzie zacząć konkretną zmianę
+37
View File
@@ -1,5 +1,42 @@
# Dziennik prac
## 2026-10-18 — przeniesienie testu audio
Przeniesiono `audio_repeats_after_end_of_file` z `src/app/threads/audio.rs` do `tests/audio.rs`, bez zmiany jego asercji. Udostępniono `load_audio` przez `app::threads`, aby test integracyjny nadal sprawdzał produkcyjną funkcję dekodowania. Zaktualizowano `docs/TESTING.md`.
Weryfikacja przez `nix develop -c`: `cargo check --all-targets`, `cargo test --all-targets` i `cargo fmt --all -- --check` zakończone sukcesem. Zestaw nadal liczy 23 testy. Nie powtarzano odsłuchu ani pomiarów CPU; zachowanie odtwarzania nie było zmieniane.
## 2026-10-18 — zestaw testów projektu
Dodano 22 testy integracyjne w `tests/cli.rs`, `tests/app.rs`, `tests/board.rs` i `tests/tasks.rs`. Razem z wcześniejszym testem audio zestaw liczy 23 przypadki. Zakres i polecenia zapisano w `docs/TESTING.md`, a mapę i instrukcje agentów zaktualizowano.
Testy obejmują walidację CLI i uruchomienie binarki bez TUI, menu i klawisze potyczki, przesyłanie komend audio, renderowanie ekranów przez TestBackend, generowanie planszy, granice kursora i kamery, zaznaczanie/cofanie/anulowanie ścieżki, kolejki i postęp zadań.
Najpierw uruchomiono testy na istniejącym kodzie. Wykryły trzy błędy, następnie poprawione w ich wspólnych implementacjach:
- `BoardState::new`: odejmowanie liczby widocznych wierszy od wysokości mapy powodowało panic przy dużym terminalu; użyto odejmowania nasycającego.
- `Offset`: konstruktor i settery pozwalały przekroczyć limit, także po resize; wartość jest teraz ograniczana do maksimum.
- `DiggingTask::progress`: obliczony postęp był odrzucany; jest teraz zapisywany z ograniczeniem do 0–100.
Nie włączano `advance_turn` ani nie zmieniano zasad symulacji. Zastane zmiany zależności w `Cargo.toml` i `Cargo.lock` oraz wcześniejsze zmiany audio pozostawiono; nie dodano zależności testowych.
Weryfikacja przez `nix develop -c`: `cargo check --all-targets`, `cargo test --all-targets` (23 zaliczone, 0 błędów), `cargo fmt --all -- --check` i `cargo clippy --all-targets` zakończone sukcesem. Clippy raportuje 51 ostrzeżeń dla biblioteki; nowe pliki testów nie zgłaszają ostrzeżeń. Nie sprawdzano urządzenia audio, pełnej symulacji ani pakowania Nix.
## 2026-10-18 — usunięcie aktywnej pętli audio
Przyczyna obciążenia: `handle_audio` bez oczekiwania wykonywał `rx.try_iter()` i `player.empty()`, także podczas normalnego odtwarzania bez komend.
Zmiany w `src/app/threads/audio.rs`:
- Zapętlanie przeniesiono do `Decoder::new_looped`; nie trzeba sprawdzać końca utworu ani ponownie kopiować danych z cache przy każdym powtórzeniu.
- Odbiór komend blokuje wątek przez `for cmd in rx`; zamknięcie kanału kończy wątek. Zachowano obsługę mute, głośności, zmiany części i soundtracku oraz dotychczasowy poziom wzmocnienia.
- Dodano test regresji odczytujący cały `calm.ogg` i kolejne 1024 próbki z zapętlonego źródła. Nie wymaga urządzenia audio.
- Zaktualizowano `docs/CODE_MAP.md`. Bez zmian zależności, CLI, zasobów i konfiguracji Nix.
Weryfikacja przez `nix develop -c`: `cargo check --all-targets`, `cargo test --all-targets` (1 test zaliczony), `cargo fmt --all -- --check` i `cargo clippy --all-targets` zakończone sukcesem. Clippy nadal raportuje 54 ostrzeżenia dla biblioteki.
Nie wykonano pomiaru CPU przed/po na urządzeniu docelowym ani odsłuchu i interaktywnego sprawdzenia komend. Usunięcie aktywnego odpytywania wynika z przepływu kodu; konkretnego procentowego spadku CPU nie zmierzono.
## 2026-10-18 — obowiązek poprawnej kompilacji
Dopisano do `docs/AGENTS.md` zakaz kończenia zadania z błędami kompilacji Rust oraz obowiązek wykonania `nix develop -c cargo check --all-targets` i usunięcia błędów przed zakończeniem. Zmiana dotyczy wyłącznie dokumentacji.
+46
View File
@@ -0,0 +1,46 @@
# Testy
Uruchomienie całego zestawu z katalogu głównego:
```sh
nix develop -c cargo test --all-targets
```
Testy korzystają z wbudowanego mechanizmu Rust, Clap i `ratatui::backend::TestBackend`. Nie dodano zależności. Nie wymagają terminala interaktywnego ani urządzenia audio. Test audio używa pliku `soundtracks/default/calm.ogg`, więc zasoby muszą być dostępne w repozytorium.
## Zakres
| Plik | Liczba | Zachowania |
|---|---:|---|
| `tests/cli.rs` | 5 | Domyślne ustawienia, granice wszystkich opcji liczbowych, XP (także NaN i infinity), błędne enumy, flagi audio, uruchomienie binarki z help/version i błędnym argumentem |
| `tests/app.rs` | 7 | Nawigacja menu, wybór ekranów, wyjście, ignorowanie zwolnienia klawisza, komendy audio i zamknięty kanał, panel, zoom, ruch, renderowanie wszystkich widoków bez mutacji stanu, wejście przed inicjalizacją |
| `tests/board.rs` | 7 | Rozmieszczenie baz/złóż/jednostek, granice kursora i kamery, zatwierdzanie/anulowanie ścieżki, cofanie z powtórzonym polem, duży viewport, resize i zoom |
| `tests/tasks.rs` | 3 | Przyrost i limit postępu, reset postępu przy zmianie celu, pusta ścieżka, kolejność zadań górnika |
| `tests/audio.rs` | 1 | Zapętlanie rzeczywistego pliku OGG po jego końcu |
Łącznie: 23 testy. Wszystkie testy znajdują się w `tests/` i sprawdzają publiczne interfejsy biblioteki lub binarkę. Funkcja `load_audio` jest eksportowana przez `app::threads`, dzięki czemu test korzysta z tego samego dekodera co odtwarzanie.
Wybrane zestawy i pojedynczy przypadek:
```sh
nix develop -c cargo test --test cli
nix develop -c cargo test --test app
nix develop -c cargo test --test board
nix develop -c cargo test --test tasks
nix develop -c cargo test --test audio
nix develop -c cargo test --test board large_viewport_and_zoom_keep_camera_inside_map
```
Kontrole uzupełniające:
```sh
nix develop -c cargo check --all-targets
nix develop -c cargo fmt --all -- --check
nix develop -c cargo clippy --all-targets
```
`shellHook` środowiska Nix dodatkowo wykonuje `cargo run -- -h`. Wynik pomocy nie jest wynikiem testów; właściwy raport zawiera podsumowania `test result`.
## Ograniczenia
Zestaw nie weryfikuje pełnej rozgrywki ani `advance_turn`, którego wywołanie nadal jest zakomentowane. Nie potwierdza odsłuchu, zużycia CPU, obsługi urządzeń, pakowania Nix ani wszystkich układów małych terminali. Testy renderowania są kontrolą podstawową, bez utrwalania pełnych zrzutów ekranów. Brak konfiguracji CI; polecenia należy wykonywać lokalnie przed zakończeniem zmian.
+1 -1
View File
@@ -51,7 +51,7 @@ impl BoardState {
let h_max_offset: usize = Self::max_offset(map_width, cols);
let vertical_offset: Offset = Offset::new(
Some((map_height - rows as usize + 1) / 2),
Some(map_height.saturating_sub(rows).div_ceil(2)),
Some(v_max_offset),
);
let horizontal_offset: Offset = Offset::new(None, Some(h_max_offset));
+3 -2
View File
@@ -7,7 +7,7 @@ pub struct Offset {
impl Offset {
pub fn new(initial_value: Option<usize>, initial_max: Option<usize>) -> Self {
Self {
value: initial_value.unwrap_or(0),
value: initial_value.unwrap_or(0).min(initial_max.unwrap_or(0)),
max: initial_max.unwrap_or(0),
}
}
@@ -17,11 +17,12 @@ impl Offset {
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
self.value = value.min(self.max);
}
pub fn set_max(&mut self, max: usize) {
self.max = max;
self.value = self.value.min(max);
}
pub fn next(&mut self) {
@@ -1,5 +1,5 @@
use crate::app::states::skirmish_states::tasks::Task;
use std::{collections::VecDeque, ops::Add};
use std::collections::VecDeque;
#[derive(Debug, Clone, PartialEq)]
pub struct DiggingTask {
@@ -49,7 +49,7 @@ impl Task for DiggingTask {
}
fn progress(&mut self) {
let _ = self.progress.add(self.tick_increase).max(0.0).min(100.0);
self.progress = (self.progress + self.tick_increase).clamp(0.0, 100.0);
}
fn is_finished(&self) -> bool {
+8 -13
View File
@@ -1,7 +1,8 @@
use clap::ValueEnum;
use once_cell::sync::Lazy;
use rodio::{
Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source, cpal::BufferSize, source::Amplify,
Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source, cpal::BufferSize,
decoder::LoopedDecoder, source::Amplify,
};
use std::{
collections::HashMap,
@@ -69,10 +70,10 @@ fn load_soundtrack_folder(folder: &str) -> SoundtrackCache {
cache
}
fn load_audio(
pub fn load_audio(
soundtrack: &Soundtrack,
part: &SoundrackParts,
) -> Amplify<Decoder<BufReader<Cursor<Vec<u8>>>>> {
) -> Amplify<LoopedDecoder<BufReader<Cursor<Vec<u8>>>>> {
{
let guard: RwLockReadGuard<'_, HashMap<Soundtrack, Arc<HashMap<SoundrackParts, Vec<u8>>>>> =
PRELOADED_SOUNDTRACKS
@@ -86,7 +87,7 @@ fn load_audio(
if let Some(soundtrack_cache) = guard.get(soundtrack) {
let data: &Vec<u8> = soundtrack_cache.get(part).expect("part missing in cache");
return Decoder::new(BufReader::new(Cursor::new(data.clone())))
return Decoder::new_looped(BufReader::new(Cursor::new(data.clone())))
.expect("decoder issue")
.amplify(0.20);
}
@@ -107,7 +108,7 @@ fn load_audio(
if let Some(soundtrack_cache) = guard.get(soundtrack) {
let data: &Vec<u8> = soundtrack_cache.get(part).expect("part missing in cache");
return Decoder::new(BufReader::new(Cursor::new(data.clone())))
return Decoder::new_looped(BufReader::new(Cursor::new(data.clone())))
.expect("decoder issue")
.amplify(0.20);
}
@@ -123,7 +124,7 @@ fn load_audio(
guard.insert(*soundtrack, Arc::clone(&new_cache));
Decoder::new(BufReader::new(Cursor::new(data.clone())))
Decoder::new_looped(BufReader::new(Cursor::new(data.clone())))
.expect("decoder issue")
.amplify(0.20)
}
@@ -146,8 +147,7 @@ pub fn handle_audio(rx: Receiver<AudioCmd>, mute: bool, soundtrack: Soundtrack)
let mut current_part: SoundrackParts = SoundrackParts::Calm;
player.append(load_audio(&current_soundtrack, &current_part));
loop {
for cmd in rx.try_iter() {
for cmd in rx {
match cmd {
AudioCmd::Mute => {
player.set_volume(if player.volume() == 0.0 { volume } else { 0.0 });
@@ -172,9 +172,4 @@ pub fn handle_audio(rx: Receiver<AudioCmd>, mute: bool, soundtrack: Soundtrack)
}
}
}
if player.empty() {
player.append(load_audio(&current_soundtrack, &current_part));
}
}
}
+1 -1
View File
@@ -2,6 +2,6 @@ mod audio;
mod events;
mod tick;
pub use audio::{AudioCmd, SoundrackParts, Soundtrack, handle_audio};
pub use audio::{AudioCmd, SoundrackParts, Soundtrack, handle_audio, load_audio};
pub use events::handle_ct_events;
pub use tick::{TICK_MS, handle_tick_event};
+161
View File
@@ -0,0 +1,161 @@
use clap::Parser;
use ratatui::{
Terminal,
backend::TestBackend,
crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
layout::Rect,
};
use std::sync::mpsc::{Receiver, channel};
use war_in_tunnels::{
app::{App, GameStates, View, handle_keybindings, threads::AudioCmd},
cli::Cli,
};
fn app() -> (App, Receiver<AudioCmd>) {
let args = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
let (tx, rx) = channel();
let mut app = App::new(args, tx);
app.window_area = Rect::new(0, 0, 120, 40);
app.states = Some(GameStates::new(&app.args, &app.window_area));
(app, rx)
}
fn press(app: &mut App, code: KeyCode) {
handle_keybindings(app, KeyEvent::new(code, KeyModifiers::NONE));
}
#[test]
fn menu_selection_stays_in_bounds_and_opens_each_view() {
let (mut app, _) = app();
for _ in 0..10 {
press(&mut app, KeyCode::Up);
}
assert_eq!(app.states_ref().unwrap().main_menu.selected_view, 1);
for view in [View::Skirmish, View::PerkDecks, View::SkillsConfig] {
press(&mut app, KeyCode::Char(' '));
assert_eq!(app.view, view);
press(&mut app, KeyCode::Esc);
assert_eq!(app.view, View::MainMenu);
press(&mut app, KeyCode::Down);
}
for _ in 0..10 {
press(&mut app, KeyCode::Down);
}
assert_eq!(app.states_ref().unwrap().main_menu.selected_view, 3);
}
#[test]
fn quit_keys_work_in_every_view() {
for view in [
View::MainMenu,
View::Skirmish,
View::PerkDecks,
View::SkillsConfig,
] {
for key in [
KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE),
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
] {
let (mut app, _) = app();
app.view = view;
handle_keybindings(&mut app, key);
assert!(app.exit);
}
}
}
#[test]
fn key_release_does_not_trigger_quit() {
let (mut app, _) = app();
handle_keybindings(
&mut app,
KeyEvent::new_with_kind(
KeyCode::Char('q'),
KeyModifiers::NONE,
KeyEventKind::Release,
),
);
assert!(!app.exit);
}
#[test]
fn music_keys_send_commands_without_opening_audio_device() {
let (mut app, rx) = app();
press(&mut app, KeyCode::Char('m'));
press(&mut app, KeyCode::Char('b'));
press(&mut app, KeyCode::Char('n'));
assert!(matches!(rx.try_recv().unwrap(), AudioCmd::Mute));
assert!(matches!(rx.try_recv().unwrap(), AudioCmd::VolumeUp));
assert!(matches!(rx.try_recv().unwrap(), AudioCmd::VolumeDown));
drop(rx);
press(&mut app, KeyCode::Char('m'));
assert!(!app.exit);
}
#[test]
fn skirmish_keys_move_focus_toggle_panel_and_limit_zoom() {
let (mut app, _) = app();
app.view = View::Skirmish;
press(&mut app, KeyCode::Right);
assert_eq!(
app.states_ref()
.unwrap()
.skirmish
.board
.get_focused_cell()
.get_coords(),
(5, 4)
);
press(&mut app, KeyCode::Tab);
assert!(app.states_ref().unwrap().skirmish.side_panel);
press(&mut app, KeyCode::Tab);
assert!(!app.states_ref().unwrap().skirmish.side_panel);
for _ in 0..4 {
press(&mut app, KeyCode::Char(','));
}
assert_eq!(
app.states_ref().unwrap().skirmish.board.zoom_level,
war_in_tunnels::app::states::skirmish_states::ZoomLevel::ZoomedIn
);
for _ in 0..4 {
press(&mut app, KeyCode::Char('.'));
}
assert_eq!(
app.states_ref().unwrap().skirmish.board.zoom_level,
war_in_tunnels::app::states::skirmish_states::ZoomLevel::ZoomedOut
);
}
#[test]
fn all_views_render_without_real_terminal() {
let (mut app, _) = app();
let mut terminal = Terminal::new(TestBackend::new(120, 40)).unwrap();
for view in [
View::MainMenu,
View::Skirmish,
View::PerkDecks,
View::SkillsConfig,
] {
app.view = view;
let before = app.states.clone();
terminal
.draw(|frame| frame.render_widget(&mut app, frame.area()))
.unwrap();
let buffer = terminal.backend().buffer();
let text: String = buffer.content.iter().map(|cell| cell.symbol()).collect();
assert!(!text.trim().is_empty(), "{view:?}");
if matches!(view, View::PerkDecks | View::SkillsConfig) {
assert!(text.contains("Work in progress"));
}
assert_eq!(app.states, before);
}
}
#[test]
fn common_keys_work_before_states_are_initialized() {
let (mut app, _) = app();
app.states = None;
press(&mut app, KeyCode::Down);
press(&mut app, KeyCode::Char('q'));
assert!(app.exit);
}
+19
View File
@@ -0,0 +1,19 @@
use rodio::Decoder;
use std::{
fs::read,
io::{BufReader, Cursor},
};
use war_in_tunnels::app::threads::{SoundrackParts, Soundtrack, load_audio};
#[test]
fn audio_repeats_after_end_of_file() {
let data = read("soundtracks/default/calm.ogg").unwrap();
let sample_count = Decoder::new(BufReader::new(Cursor::new(data)))
.unwrap()
.count();
assert!(sample_count > 0);
let mut source = load_audio(&Soundtrack::Default, &SoundrackParts::Calm);
assert_eq!(source.by_ref().take(sample_count).count(), sample_count);
assert_eq!(source.take(1024).count(), 1024);
}
+150
View File
@@ -0,0 +1,150 @@
use ratatui::layout::Rect;
use war_in_tunnels::app::states::skirmish_states::{
BoardState, FocusedCell, MoveFocusedCell, Offset, Players, ZoomLevel,
structures::{Structure, Structures},
tasks::Task,
units::Unit,
};
fn board() -> BoardState {
BoardState::new(&Rect::new(0, 0, 120, 40), 42, 11, ZoomLevel::Default, false)
}
#[test]
fn map_has_opposing_bases_ore_and_two_miners() {
let board = board();
assert_eq!(board.cells.len(), 11);
assert!(board.cells.iter().all(|row| row.len() == 42));
for (col, owner, ore_col) in [(2, Players::Player, 1), (39, Players::Enemy, 40)] {
let cell = board.get_ref_cell(5, col);
assert!(matches!(cell.get_structure(), Structures::Base(_)));
assert_eq!(cell.get_structure().get_owner(), owner);
let unit = cell.get_ref_option_unit().as_ref().unwrap();
assert_eq!(unit.get_coords(), (5, col));
assert_eq!(unit.get_owner(), owner);
assert!(matches!(
board.get_ref_cell(5, ore_col).get_structure(),
Structures::Ore(_)
));
}
assert_eq!(
board
.cells
.iter()
.flatten()
.filter(|c| c.get_ref_option_unit().is_some())
.count(),
2
);
assert!(matches!(
board.get_ref_cell(0, 0).get_structure(),
Structures::Stone(_)
));
}
#[test]
fn focus_cannot_leave_map_even_with_extreme_coordinates() {
let mut focus = FocusedCell::new(0, 0, 11, 42);
assert_eq!(focus.move_focused_cell(MoveFocusedCell::Up), (0, 0));
assert_eq!(focus.move_focused_cell(MoveFocusedCell::Left), (0, 0));
focus.set_focused_cell((usize::MAX, usize::MAX));
assert_eq!(focus.get_coords(), (10, 41));
assert_eq!(focus.move_focused_cell(MoveFocusedCell::Down), (10, 41));
assert_eq!(focus.move_focused_cell(MoveFocusedCell::Right), (10, 41));
}
#[test]
fn offsets_remain_within_bounds_when_limits_change() {
let mut offset = Offset::new(Some(100), Some(5));
assert_eq!(offset.get_value(), 5);
offset.next();
assert_eq!(offset.get_value(), 5);
offset.set_max(2);
assert_eq!(offset.get_value(), 2);
offset.set_value(usize::MAX);
assert_eq!(offset.get_value(), 2);
for _ in 0..4 {
offset.prev();
}
assert_eq!(offset.get_value(), 0);
}
#[test]
fn confirming_path_queues_task_without_including_start_cell() {
let mut board = board();
board.change_focused_cell(MoveFocusedCell::Left);
board.toggle_marking();
board.change_focused_cell(MoveFocusedCell::Right);
board.change_focused_cell(MoveFocusedCell::Down);
board.toggle_marking();
let unit = board
.get_ref_cell(5, 2)
.get_ref_option_unit()
.as_ref()
.unwrap();
let tasks = unit.get_ref_tasks();
assert_eq!(tasks.len(), 1);
assert_eq!(
tasks[0].get_path().iter().copied().collect::<Vec<_>>(),
[(5, 3), (6, 3)]
);
assert!(!board.marked_cells.marking_cells);
assert!(board.get_marked_cells().is_empty());
assert!(board.cells.iter().flatten().all(|c| !c.get_marked()));
}
#[test]
fn cancelling_path_clears_marks_without_queuing_task() {
let mut board = board();
board.change_focused_cell(MoveFocusedCell::Left);
board.toggle_marking();
board.change_focused_cell(MoveFocusedCell::Right);
board.marked_cells.marking_cells = false;
board.end_marking_cells(true);
assert!(
board
.get_ref_cell(5, 2)
.get_ref_option_unit()
.as_ref()
.unwrap()
.get_ref_tasks()
.is_empty()
);
assert!(board.get_marked_cells().is_empty());
assert!(board.marked_cells.selected_unit.is_none());
assert!(board.cells.iter().flatten().all(|c| !c.get_marked()));
}
#[test]
fn backtracking_revisited_cell_preserves_earlier_mark() {
let mut board = board();
board.toggle_marking();
board.change_focused_cell(MoveFocusedCell::Right);
board.change_focused_cell(MoveFocusedCell::Left);
board.pop_marked_cell();
assert_eq!(board.get_focused_cell().get_coords(), (5, 4));
assert!(board.get_ref_cell(5, 3).get_marked());
board.pop_marked_cell();
board.pop_marked_cell();
assert_eq!(board.get_focused_cell().get_coords(), (5, 3));
assert_eq!(board.get_marked_cells().len(), 1);
assert!(!board.get_ref_cell(5, 4).get_marked());
}
#[test]
fn large_viewport_and_zoom_keep_camera_inside_map() {
for zoom in [
ZoomLevel::ZoomedIn,
ZoomLevel::Default,
ZoomLevel::ZoomedOut,
] {
let mut board = BoardState::new(&Rect::new(0, 0, 240, 120), 36, 11, zoom, false);
assert_eq!(board.vertical_offset.get_value(), 0);
board.horizontal_offset.set_value(usize::MAX);
board.change_resize(&Rect::new(0, 0, 500, 120), false);
assert_eq!(board.horizontal_offset.get_value(), 0);
board.change_zoom(ZoomLevel::ZoomedOut);
assert!(board.is_focused_cell_visible());
assert_eq!(board.cells.len(), 11);
}
}
+97
View File
@@ -0,0 +1,97 @@
use clap::{Parser, error::ErrorKind};
use std::process::Command;
use war_in_tunnels::{app::View, cli::Cli};
#[test]
fn defaults_start_in_menu_without_logging() {
let args = Cli::try_parse_from(["game"]).unwrap();
assert_eq!(args.view, View::MainMenu);
assert_eq!((args.map_width, args.map_height), (42, 11));
assert_eq!(args.username, "Player");
assert!(!args.log);
assert!(!args.disable_audio);
assert!(!args.mute);
}
#[test]
fn numeric_options_accept_boundaries_and_reject_out_of_range_values() {
for (flag, min, max) in [
("--map-width", 36, 108),
("--map-height", 11, 39),
("--starting-wood", 1, 150),
("--starting-iron", 1, 150),
("--supply-limit", 49, 149),
("--skill-points-limit", 90, 690),
] {
for value in [min, max] {
assert!(
Cli::try_parse_from(["game", flag, &value.to_string()]).is_ok(),
"{flag}={value}"
);
}
for value in [min - 1, max + 1] {
assert!(
Cli::try_parse_from(["game", flag, &value.to_string()]).is_err(),
"{flag}={value}"
);
}
}
}
#[test]
fn xp_modifier_rejects_non_finite_malformed_and_out_of_range_values() {
for value in ["0.5", "1", "2.0"] {
let args = Cli::try_parse_from(["game", "--xp-modifier", value]).unwrap();
assert_eq!(args.xp_modifier, value.parse::<f32>().unwrap());
}
for value in ["0.49", "2.01", "NaN", "inf", "text"] {
assert!(
Cli::try_parse_from(["game", "--xp-modifier", value]).is_err(),
"{value}"
);
}
}
#[test]
fn enum_options_and_audio_flags_are_parsed() {
let args =
Cli::try_parse_from(["game", "--view", "skirmish", "--disable-audio", "--mute"]).unwrap();
assert_eq!(args.view, View::Skirmish);
assert!(args.disable_audio && args.mute);
for flag in [
"--view",
"--game-mode",
"--zoom-level",
"--perk-deck",
"--soundtrack",
] {
assert_eq!(
Cli::try_parse_from(["game", flag, "unknown"])
.unwrap_err()
.kind(),
ErrorKind::InvalidValue
);
}
}
#[test]
fn binary_help_and_version_work_without_terminal_or_audio() {
for (flag, expected) in [("--help", "Usage:"), ("--version", "0.1.0")] {
let output = Command::new(env!("CARGO_BIN_EXE_war_in_tunnels"))
.arg(flag)
.output()
.unwrap();
assert!(output.status.success());
assert!(String::from_utf8(output.stdout).unwrap().contains(expected));
}
let output = Command::new(env!("CARGO_BIN_EXE_war_in_tunnels"))
.args(["--map-width", "0"])
.output()
.unwrap();
assert!(!output.status.success());
assert!(
String::from_utf8(output.stderr)
.unwrap()
.contains("--map-width")
);
}
+50
View File
@@ -0,0 +1,50 @@
use war_in_tunnels::app::states::skirmish_states::{
Players,
tasks::{DiggingTask, Task, Tasks},
units::{MinerUnit, Unit},
};
#[test]
fn digging_progress_accumulates_and_stops_at_completion() {
let mut task = DiggingTask::new([(5, 3)].into(), 40.0);
assert!(!task.is_finished());
task.progress();
assert_eq!(task.get_progress(), 40.0);
assert!(!task.is_finished());
for _ in 0..3 {
task.progress();
}
assert_eq!(task.get_progress(), 100.0);
assert!(task.is_finished());
}
#[test]
fn advancing_path_resets_progress_and_handles_empty_path() {
let mut task = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0);
task.progress();
task.pop_front();
assert_eq!(task.current_target(), Some((5, 4)));
assert_eq!(task.get_progress(), 0.0);
task.pop_front();
task.pop_front();
assert_eq!(task.current_target(), None);
assert!(task.get_path().is_empty());
}
#[test]
fn miners_keep_tasks_in_submission_order() {
let mut miner = MinerUnit::new(Players::Player, (5, 2));
for target in [(5, 3), (6, 3)] {
miner.set_task(Tasks::Digging(DiggingTask::new([target].into(), 1.0)));
}
assert_eq!(miner.get_ref_tasks().len(), 2);
assert_eq!(
*miner
.get_mut_tasks()
.pop_front()
.unwrap()
.get_path_front_coords(),
(5, 3)
);
assert_eq!(*miner.get_ref_tasks()[0].get_path_front_coords(), (6, 3));
}