up_or_down 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,186 @@
1
+ # This file is autogenerated by maturin v1.13.3
2
+ # To update, run
3
+ #
4
+ # maturin generate-ci github
5
+ #
6
+ name: CI
7
+
8
+ on:
9
+ push:
10
+ branches:
11
+ - main
12
+ - master
13
+ tags:
14
+ - '*'
15
+ pull_request:
16
+ workflow_dispatch:
17
+
18
+ permissions:
19
+ contents: read
20
+
21
+ jobs:
22
+ linux:
23
+ runs-on: ${{ matrix.platform.runner }}
24
+ strategy:
25
+ matrix:
26
+ platform:
27
+ - runner: ubuntu-22.04
28
+ target: x86_64
29
+ - runner: ubuntu-22.04
30
+ target: x86
31
+ - runner: ubuntu-22.04
32
+ target: aarch64
33
+ - runner: ubuntu-22.04
34
+ target: armv7
35
+ - runner: ubuntu-22.04
36
+ target: s390x
37
+ - runner: ubuntu-22.04
38
+ target: ppc64le
39
+ steps:
40
+ - uses: actions/checkout@v6
41
+ - uses: actions/setup-python@v6
42
+ with:
43
+ python-version: "3.x"
44
+ - name: Build wheels
45
+ uses: PyO3/maturin-action@v1
46
+ with:
47
+ target: ${{ matrix.platform.target }}
48
+ args: --release --out dist --find-interpreter
49
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
50
+ manylinux: auto
51
+ - name: Upload wheels
52
+ uses: actions/upload-artifact@v6
53
+ with:
54
+ name: wheels-linux-${{ matrix.platform.target }}
55
+ path: dist
56
+
57
+ musllinux:
58
+ runs-on: ${{ matrix.platform.runner }}
59
+ strategy:
60
+ matrix:
61
+ platform:
62
+ - runner: ubuntu-22.04
63
+ target: x86_64
64
+ - runner: ubuntu-22.04
65
+ target: x86
66
+ - runner: ubuntu-22.04
67
+ target: aarch64
68
+ - runner: ubuntu-22.04
69
+ target: armv7
70
+ steps:
71
+ - uses: actions/checkout@v6
72
+ - uses: actions/setup-python@v6
73
+ with:
74
+ python-version: "3.x"
75
+ - name: Build wheels
76
+ uses: PyO3/maturin-action@v1
77
+ with:
78
+ target: ${{ matrix.platform.target }}
79
+ args: --release --out dist --find-interpreter
80
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
81
+ manylinux: musllinux_1_2
82
+ - name: Upload wheels
83
+ uses: actions/upload-artifact@v6
84
+ with:
85
+ name: wheels-musllinux-${{ matrix.platform.target }}
86
+ path: dist
87
+
88
+ windows:
89
+ runs-on: ${{ matrix.platform.runner }}
90
+ strategy:
91
+ matrix:
92
+ platform:
93
+ - runner: windows-latest
94
+ target: x64
95
+ python_arch: x64
96
+ - runner: windows-latest
97
+ target: x86
98
+ python_arch: x86
99
+ - runner: windows-11-arm
100
+ target: aarch64
101
+ python_arch: arm64
102
+ steps:
103
+ - uses: actions/checkout@v6
104
+ - uses: actions/setup-python@v6
105
+ with:
106
+ python-version: "3.13"
107
+ architecture: ${{ matrix.platform.python_arch }}
108
+ - name: Build wheels
109
+ uses: PyO3/maturin-action@v1
110
+ with:
111
+ target: ${{ matrix.platform.target }}
112
+ args: --release --out dist --find-interpreter
113
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
114
+ - name: Upload wheels
115
+ uses: actions/upload-artifact@v6
116
+ with:
117
+ name: wheels-windows-${{ matrix.platform.target }}
118
+ path: dist
119
+
120
+ macos:
121
+ runs-on: ${{ matrix.platform.runner }}
122
+ strategy:
123
+ matrix:
124
+ platform:
125
+ - runner: macos-15-intel
126
+ target: x86_64
127
+ - runner: macos-latest
128
+ target: aarch64
129
+ steps:
130
+ - uses: actions/checkout@v6
131
+ - uses: actions/setup-python@v6
132
+ with:
133
+ python-version: "3.x"
134
+ - name: Build wheels
135
+ uses: PyO3/maturin-action@v1
136
+ with:
137
+ target: ${{ matrix.platform.target }}
138
+ args: --release --out dist --find-interpreter
139
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
140
+ - name: Upload wheels
141
+ uses: actions/upload-artifact@v6
142
+ with:
143
+ name: wheels-macos-${{ matrix.platform.target }}
144
+ path: dist
145
+
146
+ sdist:
147
+ runs-on: ubuntu-latest
148
+ steps:
149
+ - uses: actions/checkout@v6
150
+ - name: Build sdist
151
+ uses: PyO3/maturin-action@v1
152
+ with:
153
+ command: sdist
154
+ args: --out dist
155
+ - name: Upload sdist
156
+ uses: actions/upload-artifact@v6
157
+ with:
158
+ name: wheels-sdist
159
+ path: dist
160
+
161
+ release:
162
+ name: Release
163
+ runs-on: ubuntu-latest
164
+ if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}
165
+ needs: [linux, musllinux, windows, macos, sdist]
166
+ permissions:
167
+ # Use to sign the release artifacts
168
+ id-token: write
169
+ # Used to upload release artifacts
170
+ contents: write
171
+ # Used to generate artifact attestation
172
+ attestations: write
173
+ steps:
174
+ - uses: actions/download-artifact@v7
175
+ - name: Generate artifact attestation
176
+ uses: actions/attest@v4
177
+ with:
178
+ subject-path: 'wheels-*/*'
179
+ - name: Install uv
180
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
181
+ uses: astral-sh/setup-uv@v7
182
+ - name: Publish to PyPI
183
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
184
+ run: uv publish 'wheels-*/*'
185
+ env:
186
+ UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,21 @@
1
+ /target
2
+
3
+ *.so
4
+ *.pyd
5
+ *.dylib
6
+ *.pdb
7
+
8
+ __pycache__/
9
+ *.py[cod]
10
+
11
+ /dist
12
+ /wheels
13
+ *.egg-info/
14
+
15
+ .venv/
16
+ .env
17
+ .python-version
18
+
19
+ .mypy_cache/
20
+ .ruff_cache/
21
+ .pytest_cache/
@@ -0,0 +1,195 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "cfg-if"
7
+ version = "1.0.4"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
10
+
11
+ [[package]]
12
+ name = "chacha20"
13
+ version = "0.10.2"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
16
+ dependencies = [
17
+ "cfg-if",
18
+ "cpufeatures",
19
+ "rand_core",
20
+ ]
21
+
22
+ [[package]]
23
+ name = "cpufeatures"
24
+ version = "0.3.1"
25
+ source = "registry+https://github.com/rust-lang/crates.io-index"
26
+ checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
27
+ dependencies = [
28
+ "libc",
29
+ ]
30
+
31
+ [[package]]
32
+ name = "getrandom"
33
+ version = "0.4.3"
34
+ source = "registry+https://github.com/rust-lang/crates.io-index"
35
+ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
36
+ dependencies = [
37
+ "cfg-if",
38
+ "libc",
39
+ "r-efi",
40
+ "rand_core",
41
+ ]
42
+
43
+ [[package]]
44
+ name = "heck"
45
+ version = "0.5.0"
46
+ source = "registry+https://github.com/rust-lang/crates.io-index"
47
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
48
+
49
+ [[package]]
50
+ name = "libc"
51
+ version = "0.2.189"
52
+ source = "registry+https://github.com/rust-lang/crates.io-index"
53
+ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
54
+
55
+ [[package]]
56
+ name = "once_cell"
57
+ version = "1.21.4"
58
+ source = "registry+https://github.com/rust-lang/crates.io-index"
59
+ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
60
+
61
+ [[package]]
62
+ name = "portable-atomic"
63
+ version = "1.15.0"
64
+ source = "registry+https://github.com/rust-lang/crates.io-index"
65
+ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
66
+
67
+ [[package]]
68
+ name = "proc-macro2"
69
+ version = "1.0.107"
70
+ source = "registry+https://github.com/rust-lang/crates.io-index"
71
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
72
+ dependencies = [
73
+ "unicode-ident",
74
+ ]
75
+
76
+ [[package]]
77
+ name = "pyo3"
78
+ version = "0.28.3"
79
+ source = "registry+https://github.com/rust-lang/crates.io-index"
80
+ checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12"
81
+ dependencies = [
82
+ "libc",
83
+ "once_cell",
84
+ "portable-atomic",
85
+ "pyo3-build-config",
86
+ "pyo3-ffi",
87
+ "pyo3-macros",
88
+ ]
89
+
90
+ [[package]]
91
+ name = "pyo3-build-config"
92
+ version = "0.28.3"
93
+ source = "registry+https://github.com/rust-lang/crates.io-index"
94
+ checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e"
95
+ dependencies = [
96
+ "target-lexicon",
97
+ ]
98
+
99
+ [[package]]
100
+ name = "pyo3-ffi"
101
+ version = "0.28.3"
102
+ source = "registry+https://github.com/rust-lang/crates.io-index"
103
+ checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e"
104
+ dependencies = [
105
+ "libc",
106
+ "pyo3-build-config",
107
+ ]
108
+
109
+ [[package]]
110
+ name = "pyo3-macros"
111
+ version = "0.28.3"
112
+ source = "registry+https://github.com/rust-lang/crates.io-index"
113
+ checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813"
114
+ dependencies = [
115
+ "proc-macro2",
116
+ "pyo3-macros-backend",
117
+ "quote",
118
+ "syn",
119
+ ]
120
+
121
+ [[package]]
122
+ name = "pyo3-macros-backend"
123
+ version = "0.28.3"
124
+ source = "registry+https://github.com/rust-lang/crates.io-index"
125
+ checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb"
126
+ dependencies = [
127
+ "heck",
128
+ "proc-macro2",
129
+ "pyo3-build-config",
130
+ "quote",
131
+ "syn",
132
+ ]
133
+
134
+ [[package]]
135
+ name = "quote"
136
+ version = "1.0.47"
137
+ source = "registry+https://github.com/rust-lang/crates.io-index"
138
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
139
+ dependencies = [
140
+ "proc-macro2",
141
+ ]
142
+
143
+ [[package]]
144
+ name = "r-efi"
145
+ version = "6.0.0"
146
+ source = "registry+https://github.com/rust-lang/crates.io-index"
147
+ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
148
+
149
+ [[package]]
150
+ name = "rand"
151
+ version = "0.10.2"
152
+ source = "registry+https://github.com/rust-lang/crates.io-index"
153
+ checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
154
+ dependencies = [
155
+ "chacha20",
156
+ "getrandom",
157
+ "rand_core",
158
+ ]
159
+
160
+ [[package]]
161
+ name = "rand_core"
162
+ version = "0.10.1"
163
+ source = "registry+https://github.com/rust-lang/crates.io-index"
164
+ checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
165
+
166
+ [[package]]
167
+ name = "syn"
168
+ version = "2.0.119"
169
+ source = "registry+https://github.com/rust-lang/crates.io-index"
170
+ checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
171
+ dependencies = [
172
+ "proc-macro2",
173
+ "quote",
174
+ "unicode-ident",
175
+ ]
176
+
177
+ [[package]]
178
+ name = "target-lexicon"
179
+ version = "0.13.5"
180
+ source = "registry+https://github.com/rust-lang/crates.io-index"
181
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
182
+
183
+ [[package]]
184
+ name = "unicode-ident"
185
+ version = "1.0.24"
186
+ source = "registry+https://github.com/rust-lang/crates.io-index"
187
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
188
+
189
+ [[package]]
190
+ name = "up_or_down"
191
+ version = "0.2.0"
192
+ dependencies = [
193
+ "pyo3",
194
+ "rand",
195
+ ]
@@ -0,0 +1,16 @@
1
+ [package]
2
+ name = "up_or_down"
3
+ version = "0.2.0"
4
+ edition = "2024"
5
+ description = "A simple number guessing game in Rust."
6
+ license = "MIT"
7
+ repository = "https://github.com/not-lain/up_or_down"
8
+
9
+ # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
10
+ [lib]
11
+ name = "up_or_down"
12
+ crate-type = ["cdylib"]
13
+
14
+ [dependencies]
15
+ rand = "0.10.2"
16
+ pyo3 = "0.28.3"
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: up_or_down
3
+ Version: 0.2.0
4
+ Classifier: Programming Language :: Rust
5
+ Classifier: Programming Language :: Python :: Implementation :: CPython
6
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
7
+ Requires-Python: >=3.8
@@ -0,0 +1,17 @@
1
+ [build-system]
2
+ requires = ["maturin>=1.13,<2.0"]
3
+ build-backend = "maturin"
4
+
5
+ [project]
6
+ name = "up_or_down"
7
+ requires-python = ">=3.8"
8
+ classifiers = [
9
+ "Programming Language :: Rust",
10
+ "Programming Language :: Python :: Implementation :: CPython",
11
+ "Programming Language :: Python :: Implementation :: PyPy",
12
+ ]
13
+ dynamic = ["version"]
14
+
15
+ [tool.maturin]
16
+ python-source = "python"
17
+ module-name = "up_or_down._up_or_down"
@@ -0,0 +1,40 @@
1
+ """A simple number guessing game in Rust."""
2
+
3
+ from ._up_or_down import HIGH, LOW, MAX_TRIES, Game
4
+
5
+ __all__ = ["HIGH", "LOW", "MAX_TRIES", "Game", "play"]
6
+
7
+
8
+ def play() -> None:
9
+ """Play a round of the guessing game on stdin/stdout."""
10
+ game = Game()
11
+
12
+ print(f"guess the number between {game.low} and {game.high}!")
13
+ print(f"you have {game.max_tries} tries.")
14
+
15
+ while not game.is_over:
16
+ try:
17
+ answer = input(f"your guess ({game.tries_left} tries left): ")
18
+ except EOFError:
19
+ print(f"\nNo more input. The number was {game.secret}.")
20
+ return
21
+
22
+ try:
23
+ guess = int(answer)
24
+ except ValueError:
25
+ print("that is not a number.")
26
+ continue
27
+
28
+ try:
29
+ hint = game.guess(guess)
30
+ except ValueError as exc:
31
+ print(exc)
32
+ continue
33
+
34
+ if hint == "correct":
35
+ print(f"correct! you got it in {game.tries_used} tries.")
36
+ return
37
+
38
+ print("the secret number is bigger!" if hint == "higher" else "the secret number is smaller!")
39
+
40
+ print(f"out of tries! the number was {game.secret}.")
@@ -0,0 +1,5 @@
1
+ """Play the game: python -m up_or_down"""
2
+
3
+ from up_or_down import play
4
+
5
+ play()
@@ -0,0 +1,66 @@
1
+ """A number guessing game implemented in Rust."""
2
+
3
+ from typing import Literal
4
+
5
+ __all__ = ["HIGH", "LOW", "MAX_TRIES", "Game"]
6
+
7
+ MAX_TRIES: int
8
+ LOW: int
9
+ HIGH: int
10
+
11
+ class Game:
12
+ """One round of the game: the secret number plus the tries spent on it."""
13
+
14
+ def __init__(
15
+ self,
16
+ low: int = ...,
17
+ high: int = ...,
18
+ max_tries: int = ...,
19
+ secret: int | None = None,
20
+ ) -> None:
21
+ """Start a game with a secret drawn from ``low..=high``.
22
+
23
+ Pass ``secret`` to pick the number yourself (handy for tests).
24
+ """
25
+
26
+ def guess(self, guess: int) -> Literal["higher", "lower", "correct"]:
27
+ """Spend one try on ``guess``.
28
+
29
+ Returns ``"higher"`` or ``"lower"`` to say where the secret is, or
30
+ ``"correct"``. Guesses outside ``low..=high`` raise ``ValueError`` and
31
+ do not cost a try.
32
+ """
33
+
34
+ @property
35
+ def secret(self) -> int:
36
+ """The number to find."""
37
+
38
+ @property
39
+ def low(self) -> int:
40
+ """Lowest number the secret can be."""
41
+
42
+ @property
43
+ def high(self) -> int:
44
+ """Highest number the secret can be."""
45
+
46
+ @property
47
+ def max_tries(self) -> int:
48
+ """How many guesses this game allows."""
49
+
50
+ @property
51
+ def tries_used(self) -> int:
52
+ """How many guesses have been spent."""
53
+
54
+ @property
55
+ def tries_left(self) -> int:
56
+ """How many guesses are left."""
57
+
58
+ @property
59
+ def won(self) -> bool:
60
+ """Whether the secret has been guessed."""
61
+
62
+ @property
63
+ def is_over(self) -> bool:
64
+ """Whether the game is won or out of tries."""
65
+
66
+ def __repr__(self) -> str: ...
File without changes
@@ -0,0 +1,221 @@
1
+ use pyo3::prelude::*;
2
+
3
+ #[pymodule]
4
+ mod _up_or_down {
5
+ use std::cmp::Ordering;
6
+
7
+ use pyo3::exceptions::{PyRuntimeError, PyValueError};
8
+ use pyo3::prelude::*;
9
+ use rand::RngExt;
10
+
11
+ const MAX_TRIES: u32 = 10;
12
+ const LOW: u32 = 1;
13
+ const HIGH: u32 = 100;
14
+
15
+ #[pyclass]
16
+ struct Game {
17
+ secret: u32,
18
+ low: u32,
19
+ high: u32,
20
+ max_tries: u32,
21
+ tries_used: u32,
22
+ won: bool,
23
+ }
24
+
25
+ #[pymethods]
26
+ impl Game {
27
+ #[new]
28
+ #[pyo3(signature = (low = LOW, high = HIGH, max_tries = MAX_TRIES, secret = None))]
29
+ fn new(low: u32, high: u32, max_tries: u32, secret: Option<u32>) -> PyResult<Self> {
30
+ if low > high {
31
+ return Err(PyValueError::new_err(format!(
32
+ "low ({low}) must not be greater than high ({high})"
33
+ )));
34
+ }
35
+ if max_tries == 0 {
36
+ return Err(PyValueError::new_err("max_tries must be at least 1"));
37
+ }
38
+
39
+ let secret = match secret {
40
+ Some(secret) if !(low..=high).contains(&secret) => {
41
+ return Err(PyValueError::new_err(format!(
42
+ "secret ({secret}) is outside {low} to {high}"
43
+ )));
44
+ }
45
+ Some(secret) => secret,
46
+ None => rand::rng().random_range(low..=high),
47
+ };
48
+
49
+ Ok(Self {
50
+ secret,
51
+ low,
52
+ high,
53
+ max_tries,
54
+ tries_used: 0,
55
+ won: false,
56
+ })
57
+ }
58
+
59
+ fn guess(&mut self, guess: u32) -> PyResult<&'static str> {
60
+ if self.is_over() {
61
+ return Err(PyRuntimeError::new_err("the game is already over"));
62
+ }
63
+ if !(self.low..=self.high).contains(&guess) {
64
+ return Err(PyValueError::new_err(format!(
65
+ "that is outside {} to {}.",
66
+ self.low, self.high
67
+ )));
68
+ }
69
+
70
+ self.tries_used += 1;
71
+
72
+ Ok(match guess.cmp(&self.secret) {
73
+ Ordering::Less => "higher",
74
+ Ordering::Greater => "lower",
75
+ Ordering::Equal => {
76
+ self.won = true;
77
+ "correct"
78
+ }
79
+ })
80
+ }
81
+
82
+ #[getter]
83
+ fn secret(&self) -> u32 {
84
+ self.secret
85
+ }
86
+
87
+ #[getter]
88
+ fn low(&self) -> u32 {
89
+ self.low
90
+ }
91
+
92
+ #[getter]
93
+ fn high(&self) -> u32 {
94
+ self.high
95
+ }
96
+
97
+ #[getter]
98
+ fn max_tries(&self) -> u32 {
99
+ self.max_tries
100
+ }
101
+
102
+ #[getter]
103
+ fn tries_used(&self) -> u32 {
104
+ self.tries_used
105
+ }
106
+
107
+ #[getter]
108
+ fn tries_left(&self) -> u32 {
109
+ self.max_tries - self.tries_used
110
+ }
111
+
112
+ #[getter]
113
+ fn won(&self) -> bool {
114
+ self.won
115
+ }
116
+
117
+ #[getter]
118
+ fn is_over(&self) -> bool {
119
+ self.won || self.tries_used >= self.max_tries
120
+ }
121
+
122
+ fn __repr__(&self) -> String {
123
+ format!(
124
+ "Game(low={}, high={}, max_tries={}, tries_used={}, won={})",
125
+ self.low,
126
+ self.high,
127
+ self.max_tries,
128
+ self.tries_used,
129
+ if self.won { "True" } else { "False" }
130
+ )
131
+ }
132
+ }
133
+
134
+ #[pymodule_init]
135
+ fn init(m: &Bound<'_, PyModule>) -> PyResult<()> {
136
+ m.add("MAX_TRIES", MAX_TRIES)?;
137
+ m.add("LOW", LOW)?;
138
+ m.add("HIGH", HIGH)
139
+ }
140
+
141
+ #[cfg(test)]
142
+ mod tests {
143
+ use super::*;
144
+
145
+ fn game(secret: u32) -> Game {
146
+ Game::new(LOW, HIGH, MAX_TRIES, Some(secret)).unwrap()
147
+ }
148
+
149
+ #[test]
150
+ fn random_secret_stays_in_range() {
151
+ for _ in 0..1_000 {
152
+ let game = Game::new(LOW, HIGH, MAX_TRIES, None).unwrap();
153
+ assert!((LOW..=HIGH).contains(&game.secret));
154
+ }
155
+ }
156
+
157
+ #[test]
158
+ fn hints_point_at_the_secret() {
159
+ let mut game = game(42);
160
+ assert_eq!(game.guess(10).unwrap(), "higher");
161
+ assert_eq!(game.guess(90).unwrap(), "lower");
162
+ assert_eq!(game.tries_used, 2);
163
+ assert_eq!(game.tries_left(), 8);
164
+ assert!(!game.is_over());
165
+
166
+ assert_eq!(game.guess(42).unwrap(), "correct");
167
+ assert!(game.won);
168
+ assert!(game.is_over());
169
+ }
170
+
171
+ #[test]
172
+ fn running_out_of_tries_ends_the_game() {
173
+ let mut game = Game::new(LOW, HIGH, 2, Some(5)).unwrap();
174
+ game.guess(1).unwrap();
175
+ game.guess(2).unwrap();
176
+ assert!(game.is_over());
177
+ assert!(!game.won);
178
+ assert_eq!(game.tries_left(), 0);
179
+ }
180
+
181
+ #[test]
182
+ fn out_of_range_guess_costs_nothing() {
183
+ Python::initialize();
184
+ Python::attach(|py| {
185
+ let mut game = game(42);
186
+ for out_of_range in [LOW - 1, HIGH + 1] {
187
+ let err = game.guess(out_of_range).unwrap_err();
188
+ assert!(err.is_instance_of::<PyValueError>(py));
189
+ }
190
+ assert_eq!(game.tries_used, 0);
191
+ });
192
+ }
193
+
194
+ #[test]
195
+ fn guessing_after_the_end_fails() {
196
+ Python::initialize();
197
+ Python::attach(|py| {
198
+ let mut game = game(42);
199
+ game.guess(42).unwrap();
200
+ let err = game.guess(42).unwrap_err();
201
+ assert!(err.is_instance_of::<PyRuntimeError>(py));
202
+ });
203
+ }
204
+
205
+ #[test]
206
+ fn invalid_bounds_are_rejected() {
207
+ Python::initialize();
208
+ Python::attach(|py| {
209
+ let invalid = [
210
+ (5, 1, MAX_TRIES, None),
211
+ (LOW, HIGH, 0, None),
212
+ (LOW, HIGH, MAX_TRIES, Some(HIGH + 1)),
213
+ ];
214
+ for (low, high, max_tries, secret) in invalid {
215
+ let err = Game::new(low, high, max_tries, secret).err().unwrap();
216
+ assert!(err.is_instance_of::<PyValueError>(py));
217
+ }
218
+ });
219
+ }
220
+ }
221
+ }
@@ -0,0 +1,7 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.8"
4
+
5
+ [[package]]
6
+ name = "up-or-down"
7
+ source = { editable = "." }