1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
//! Transfinite number-valued games carried by their surreal value.
use crate::games::Game;
use crate::scalar::{Ordinal, Scalar, SignExpansion, Surreal};
use std::cmp::Ordering;
use std::fmt;
/// A transfinite **number-valued** game, carried by its surreal value rather than
/// a (necessarily infinite) option tree. Numbers are a transfinite class needing no
/// materialized options (see also [`NimberGame`](crate::games::NimberGame) for the
/// characteristic-2 impartial mirror): value, birthday, and the group/order
/// operations all come from [`Surreal`]. The finite [`Game`] engine is untouched
/// — `NumberGame` is a parallel *view*, not a `Game`, the numbers-only honoring
/// of "games of transfinite birthday" (`ω = {0,1,2,...|}` is a number).
#[derive(Clone, Debug, PartialEq)]
pub struct NumberGame {
value: Surreal,
}
impl NumberGame {
/// The number-game of a surreal value (always succeeds — no options built).
pub fn from_surreal(s: &Surreal) -> NumberGame {
NumberGame { value: s.clone() }
}
/// The exact surreal value.
pub fn value(&self) -> &Surreal {
&self.value
}
/// The birthday as an [`Ordinal`], via [`Surreal::birthday_ordinal`]. `None`
/// when the value is outside the representable sign-expansion subclass (e.g.
/// `sqrt(omega)`).
pub fn birthday(&self) -> Option<Ordinal> {
self.value.birthday_ordinal()
}
/// The **sign expansion** — the canonical ±-path from `0` to this number in
/// the surreal tree, run-length-encoded (its length is the birthday). This is
/// the finite encoding of the game's (transfinitely deep) `{Left | Right}`
/// tree: a transfinite number like `ω = {0,1,2,…|}` has an *infinite* option
/// set that cannot be listed, but its sign expansion `+^ω` is finite data.
/// `None` outside the representable subclass.
pub fn sign_expansion(&self) -> Option<SignExpansion> {
self.value.transfinite_sign_expansion()
}
/// Reconstruct a number-game from a sign expansion — the inverse of
/// [`sign_expansion`](Self::sign_expansion), closing the transfinite
/// surreal↔game round trip *through* the canonical birthday path rather than a
/// stored value: `from_sign_expansion(g.sign_expansion()?) == Some(g)`. This is
/// the transfinite analogue of the dyadic [`Game::from_surreal`] /
/// [`Game::number_value`] bridge. `None` outside the representable subclass.
pub fn from_sign_expansion(se: &SignExpansion) -> Option<NumberGame> {
Surreal::from_transfinite_sign_expansion(se).map(|value| NumberGame { value })
}
/// Negation (additive inverse) — surreal negation.
pub fn neg(&self) -> NumberGame {
NumberGame {
value: self.value.neg(),
}
}
/// Disjunctive sum: for numbers this is exactly surreal addition (no options
/// materialized).
pub fn add(&self, other: &NumberGame) -> NumberGame {
NumberGame {
value: self.value.add(&other.value),
}
}
/// The game order = the surreal order on values.
// Inherent value-order, deliberately kept off `std::cmp::Ord` to mirror
// `Surreal::cmp` and the partial `Game` order (see AGENTS.md).
#[allow(clippy::should_implement_trait)]
pub fn cmp(&self, other: &NumberGame) -> Ordering {
self.value.cmp(&other.value)
}
/// Bridge to the finite engine: `Some(short Game)` iff the value is dyadic;
/// `None` for genuinely transfinite numbers (`omega`, `epsilon`, ...), which
/// have no finite option tree. On dyadics this agrees with
/// [`Game::from_surreal`]/[`Game::number_value`].
pub fn to_finite_game(&self) -> Option<Game> {
Game::from_surreal(&self.value)
}
}
impl fmt::Display for NumberGame {
/// Renders as the surreal value's display (e.g. `ω`, `3/4`, `-1`).
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.value)
}
}
impl std::ops::Add for NumberGame {
type Output = NumberGame;
fn add(self, rhs: NumberGame) -> NumberGame {
NumberGame::add(&self, &rhs)
}
}
impl std::ops::Neg for NumberGame {
type Output = NumberGame;
fn neg(self) -> NumberGame {
NumberGame::neg(&self)
}
}
impl std::ops::Mul for NumberGame {
type Output = NumberGame;
fn mul(self, rhs: NumberGame) -> NumberGame {
NumberGame {
value: <Surreal as Scalar>::mul(&self.value, &rhs.value),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scalar::{Rational, Surreal};
#[test]
fn transfinite_bridge() {
let w = Surreal::omega();
let ng = NumberGame::from_surreal(&w);
assert_eq!(ng.value(), &w);
assert_eq!(ng.birthday(), Some(Ordinal::omega()));
assert!(ng.to_finite_game().is_none());
let one = NumberGame::from_surreal(&Surreal::from_int(1));
assert_eq!(ng.add(&one).value(), &w.add(&Surreal::from_int(1)));
assert_eq!(
ng.cmp(&NumberGame::from_surreal(&Surreal::from_int(1_000_000))),
Ordering::Greater
);
assert_eq!(ng.neg().value(), &w.neg());
let d = Surreal::from_rational(Rational::new(3, 4));
let ngd = NumberGame::from_surreal(&d);
let fin = Game::from_surreal(&d).unwrap();
assert_eq!(ngd.birthday().unwrap().as_finite(), Some(fin.birthday()));
assert!(ngd.to_finite_game().is_some());
}
#[test]
fn operator_traits_delegate_to_surreal_arithmetic() {
let two = NumberGame::from_surreal(&Surreal::from_int(2));
let three = NumberGame::from_surreal(&Surreal::from_int(3));
assert_eq!((two.clone() + three.clone()).value(), &Surreal::from_int(5));
assert_eq!((-two.clone()).value(), &Surreal::from_int(-2));
assert_eq!((two * three).value(), &Surreal::from_int(6));
}
#[test]
fn sign_expansion_round_trip_through_the_game_tree() {
// The full transfinite round trip: serialize each number-game to its
// canonical birthday ±-path and reconstruct it, value-for-value — going
// *through* the sign expansion, not a stored value. Spans dyadic, ordinal,
// negative-ordinal, and the infinitesimal ε.
let cases = [
Surreal::from_int(0),
Surreal::from_rational(Rational::new(3, 4)),
Surreal::from_rational(Rational::new(-5, 8)),
Surreal::omega(), // ω = {0,1,2,…|}
Surreal::omega().add(&Surreal::from_int(1)), // ω+1
Surreal::omega_pow(Surreal::omega()), // ω^ω
Surreal::omega().neg(), // −ω
Surreal::epsilon(), // ε
];
for v in &cases {
let g = NumberGame::from_surreal(v);
let se = g.sign_expansion().expect("representable");
let back = NumberGame::from_sign_expansion(&se).expect("reconstructible");
assert_eq!(back, g, "game sign-expansion round trip failed: {v:?}");
assert_eq!(back.value(), v);
}
// sqrt(ω) is outside the representable subclass — honestly None, no crash.
let root_omega = Surreal::omega_pow(Surreal::from_rational(Rational::new(1, 2)));
assert!(NumberGame::from_surreal(&root_omega)
.sign_expansion()
.is_none());
}
}