nmarshall commited on
Commit
e0c5881
·
1 Parent(s): 826c333

gameplay: port source ranking sort

Browse files
crates/urt-gameplay/src/bin/urt-lifecycle.rs CHANGED
@@ -1,4 +1,18 @@
1
  use urt_gameplay::protected::{ProtectedState, update};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  fn main() {
3
  if let Err(e) = run() {
4
  eprintln!("{e}");
@@ -314,6 +328,23 @@ fn run() -> Result<(), String> {
314
  second.cached_ping as u32
315
  );
316
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  "rank-compare" if w.len() == 21 => {
318
  let client = |v: &[u32]| urt_gameplay::ranking::Client {
319
  spectator_state: v[0] as i32,
 
1
  use urt_gameplay::protected::{ProtectedState, update};
2
+ fn ranking_client(v: &[u32]) -> urt_gameplay::ranking::Client {
3
+ urt_gameplay::ranking::Client {
4
+ spectator_state: v[0] as i32,
5
+ follow_index: v[1] as i32,
6
+ connection: v[2] as i32,
7
+ team: v[3] as i32,
8
+ spectator_time: v[4] as i32,
9
+ time: v[5] as i32,
10
+ class: v[6] as i32,
11
+ score: v[7] as i32,
12
+ deaths: v[8] as i32,
13
+ awards: v[9] as i32,
14
+ }
15
+ }
16
  fn main() {
17
  if let Err(e) = run() {
18
  eprintln!("{e}");
 
328
  second.cached_ping as u32
329
  );
330
  }
331
+ "rank-sort" if w.len() >= 2 && w[1] <= 64 => {
332
+ let n = w[1] as usize;
333
+ if w.len() != 2 + 11 * n {
334
+ return Err("invalid rank sort words".into());
335
+ }
336
+ let mut ids: Vec<_> = w[2..2 + n].iter().map(|v| *v as usize).collect();
337
+ let clients: Vec<_> = w[2 + n..].chunks_exact(10).map(ranking_client).collect();
338
+ urt_gameplay::rank_sort::sort(w[0] as i32, &clients, &mut ids)
339
+ .map_err(|e| format!("unsupported input: {e:?}"))?;
340
+ println!(
341
+ "{{\"order\":[{}]}}",
342
+ ids.iter()
343
+ .map(|v| v.to_string())
344
+ .collect::<Vec<_>>()
345
+ .join(",")
346
+ );
347
+ }
348
  "rank-compare" if w.len() == 21 => {
349
  let client = |v: &[u32]| urt_gameplay::ranking::Client {
350
  spectator_state: v[0] as i32,
crates/urt-gameplay/src/lib.rs CHANGED
@@ -157,3 +157,5 @@ pub mod recipient_wire;
157
  pub mod record_clear;
158
  pub mod score;
159
  pub mod stat_totals;
 
 
 
157
  pub mod record_clear;
158
  pub mod score;
159
  pub mod stat_totals;
160
+
161
+ pub mod rank_sort;
crates/urt-gameplay/src/rank_sort.rs ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! QVM35095 aligned four-byte ranking sort. No Rust Ord assumption.
2
+ use crate::ranking::{Client, RankError, compare};
3
+
4
+ fn median(a: &[usize], c: &[Client], mode: i32, x: usize, y: usize, z: usize) -> usize {
5
+ let cmp = |i: usize, j: usize| compare(mode, c[a[i]], c[a[j]]);
6
+ if cmp(x, y) < 0 {
7
+ if cmp(y, z) < 0 {
8
+ y
9
+ } else if cmp(x, z) < 0 {
10
+ z
11
+ } else {
12
+ x
13
+ }
14
+ } else if cmp(y, z) > 0 {
15
+ y
16
+ } else if cmp(x, z) < 0 {
17
+ x
18
+ } else {
19
+ z
20
+ }
21
+ }
22
+ fn insertion(a: &mut [usize], clients: &[Client], mode: i32) {
23
+ for i in 1..a.len() {
24
+ let mut j = i;
25
+ while j > 0 && compare(mode, clients[a[j - 1]], clients[a[j]]) > 0 {
26
+ a.swap(j - 1, j);
27
+ j -= 1;
28
+ }
29
+ }
30
+ }
31
+ fn block_swap(a: &mut [usize], x: usize, y: usize, n: usize) {
32
+ for i in 0..n {
33
+ a.swap(x + i, y + i);
34
+ }
35
+ }
36
+ pub(crate) fn source_sort(mut a: &mut [usize], c: &[Client], mode: i32) {
37
+ loop {
38
+ let n = a.len();
39
+ if n < 7 {
40
+ insertion(a, c, mode);
41
+ return;
42
+ }
43
+ let mut mid = n / 2;
44
+ if n > 7 {
45
+ let (mut first, mut last) = (0, n - 1);
46
+ if n > 40 {
47
+ let d = n / 8;
48
+ first = median(a, c, mode, 0, d, 2 * d);
49
+ mid = median(a, c, mode, mid - d, mid, mid + d);
50
+ last = median(a, c, mode, last - 2 * d, last - d, last);
51
+ }
52
+ mid = median(a, c, mode, first, mid, last);
53
+ }
54
+ a.swap(0, mid);
55
+ let (mut pa, mut pb) = (1usize, 1usize);
56
+ let (mut pc, mut pd) = ((n - 1) as isize, (n - 1) as isize);
57
+ let mut swapped = false;
58
+ loop {
59
+ while (pb as isize) <= pc {
60
+ let r = compare(mode, c[a[pb]], c[a[0]]);
61
+ if r > 0 {
62
+ break;
63
+ }
64
+ if r == 0 {
65
+ a.swap(pa, pb);
66
+ pa += 1;
67
+ swapped = true;
68
+ }
69
+ pb += 1;
70
+ }
71
+ while (pb as isize) <= pc {
72
+ let r = compare(mode, c[a[pc as usize]], c[a[0]]);
73
+ if r < 0 {
74
+ break;
75
+ }
76
+ if r == 0 {
77
+ a.swap(pc as usize, pd as usize);
78
+ pd -= 1;
79
+ swapped = true;
80
+ }
81
+ pc -= 1;
82
+ }
83
+ if (pb as isize) > pc {
84
+ break;
85
+ }
86
+ a.swap(pb, pc as usize);
87
+ pb += 1;
88
+ pc -= 1;
89
+ swapped = true;
90
+ }
91
+ if !swapped {
92
+ insertion(a, c, mode);
93
+ return;
94
+ }
95
+ let r = pa.min(pb - pa);
96
+ block_swap(a, 0, pb - r, r);
97
+ let right = (pd - pc) as usize;
98
+ let r = right.min(n - 1 - pd as usize);
99
+ block_swap(a, pb, n - r, r);
100
+ let left = pb - pa;
101
+ if left > 1 {
102
+ source_sort(&mut a[..left], c, mode);
103
+ }
104
+ if right <= 1 {
105
+ return;
106
+ }
107
+ a = &mut a[n - right..];
108
+ }
109
+ }
110
+ /// Eligible connected non-spectators only. The order must be a complete
111
+ /// permutation of at most64 clients. Equal-key ordering follows source swaps.
112
+ pub fn sort(mode: i32, clients: &[Client], order: &mut [usize]) -> Result<(), RankError> {
113
+ if clients.len() > 64 || order.len() != clients.len() {
114
+ return Err(RankError::InvalidOrder);
115
+ }
116
+ if clients
117
+ .iter()
118
+ .any(|c| c.connection != 2 || c.team == 3 || c.spectator_state == 3 || c.follow_index < 0)
119
+ {
120
+ return Err(RankError::UnsupportedParticipant);
121
+ }
122
+ let mut seen = vec![false; clients.len()];
123
+ for &id in order.iter() {
124
+ if id >= clients.len() || seen[id] {
125
+ return Err(RankError::InvalidOrder);
126
+ }
127
+ seen[id] = true;
128
+ }
129
+ source_sort(order, clients, mode);
130
+ Ok(())
131
+ }
132
+ #[cfg(test)]
133
+ mod tests {
134
+ use super::*;
135
+ fn client(score: i32) -> Client {
136
+ Client {
137
+ spectator_state: 0,
138
+ follow_index: 0,
139
+ connection: 2,
140
+ team: 1,
141
+ spectator_time: 0,
142
+ time: score,
143
+ class: score,
144
+ score,
145
+ deaths: 0,
146
+ awards: 0,
147
+ }
148
+ }
149
+ #[test]
150
+ fn signed_extremes_and_partition_sizes() {
151
+ for n in [0, 1, 6, 7, 8, 40, 41, 64] {
152
+ let mut c: Vec<_> = (0..n).map(|i| client(i as i32 % 5)).collect();
153
+ if n > 1 {
154
+ c[0].score = i32::MIN;
155
+ c[1].score = i32::MAX;
156
+ }
157
+ let mut ids: Vec<_> = (0..n).rev().collect();
158
+ sort(0, &c, &mut ids).unwrap();
159
+ assert!(ids.windows(2).all(|w| c[w[0]].score >= c[w[1]].score));
160
+ let mut permutation = ids;
161
+ permutation.sort_unstable();
162
+ assert_eq!(permutation, (0..n).collect::<Vec<_>>());
163
+ }
164
+ }
165
+ #[test]
166
+ fn equal_keys_keep_source_unstable_pivot_swap() {
167
+ let c = vec![client(0); 7];
168
+ let mut ids: Vec<_> = (0..7).collect();
169
+ sort(0, &c, &mut ids).unwrap();
170
+ assert_eq!(ids, [3, 1, 2, 0, 4, 5, 6]);
171
+ }
172
+ #[test]
173
+ fn rejects_without_mutating_invalid_or_unsupported_input() {
174
+ let c = vec![client(0); 2];
175
+ let mut ids = [0, 0];
176
+ assert_eq!(sort(0, &c, &mut ids), Err(RankError::InvalidOrder));
177
+ assert_eq!(ids, [0, 0]);
178
+ let mut c = c;
179
+ c[0].connection = 1;
180
+ let mut ids = [1, 0];
181
+ assert_eq!(
182
+ sort(0, &c, &mut ids),
183
+ Err(RankError::UnsupportedParticipant)
184
+ );
185
+ assert_eq!(ids, [1, 0]);
186
+ }
187
+ }
docs/FINDINGS.md CHANGED
@@ -1379,3 +1379,6 @@ RE-780–789 exposes and bounds legacy resumer timing/byte-state flaws, migrates
1379
 
1380
 
1381
  **RE-793:** Ranking count globals follow connection/team/bot gates, but deferred records can move active clients out of the positional prefix that receives rank words. [Evidence](research/QVM_RANKING_POPULATION.md).
 
 
 
 
1379
 
1380
 
1381
  **RE-793:** Ranking count globals follow connection/team/bot gates, but deferred records can move active clients out of the positional prefix that receives rank words. [Evidence](research/QVM_RANKING_POPULATION.md).
1382
+
1383
+
1384
+ **RE-794:** Rust now executes the recovered insertion/partition/median/ninther sort without assuming comparator Ord; source parity includes unstable tie permutations. [Evidence](research/RUST_RANK_SORT.md).
docs/PROVISIONAL_TODO.md CHANGED
@@ -1383,3 +1383,6 @@ RE-792: 5864 now composes actual35095 sorting and rank writes over80 larger elig
1383
 
1384
 
1385
  RE-793: Ranking count globals follow connection/team/bot gates, but deferred records can move active clients out of the positional prefix that receives rank words. Full runtime completion remains open. [Evidence](research/QVM_RANKING_POPULATION.md).
 
 
 
 
1383
 
1384
 
1385
  RE-793: Ranking count globals follow connection/team/bot gates, but deferred records can move active clients out of the positional prefix that receives rank words. Full runtime completion remains open. [Evidence](research/QVM_RANKING_POPULATION.md).
1386
+
1387
+
1388
+ RE-794: Rust now executes the recovered insertion/partition/median/ninther sort without assuming comparator Ord; source parity includes unstable tie permutations. Full runtime completion remains open. [Evidence](research/RUST_RANK_SORT.md).
docs/RESEARCH_LEDGER.md CHANGED
@@ -1629,3 +1629,8 @@ Both resumers can synchronize new and changed child words into byte lanes withou
1629
  ## 2026-10-26 — RE-793 Mixed ranking populations and positional rank-prefix behavior
1630
 
1631
  Ranking count globals follow connection/team/bot gates, but deferred records can move active clients out of the positional prefix that receives rank words. Focused `cargo xtask test-group ranking-population` passes (13 Python tests); selected reports regenerate twice deterministically, with storage, documentation links and whitespace guards. Full suite deferred to task ten. Original assets unchanged; source fixtures do not establish complete native/runtime parity.
 
 
 
 
 
 
1629
  ## 2026-10-26 — RE-793 Mixed ranking populations and positional rank-prefix behavior
1630
 
1631
  Ranking count globals follow connection/team/bot gates, but deferred records can move active clients out of the positional prefix that receives rank words. Focused `cargo xtask test-group ranking-population` passes (13 Python tests); selected reports regenerate twice deterministically, with storage, documentation links and whitespace guards. Full suite deferred to task ten. Original assets unchanged; source fixtures do not establish complete native/runtime parity.
1632
+
1633
+
1634
+ ## 2026-10-26 — RE-794 Rust source-shaped aligned ranking sort with exact tie order
1635
+
1636
+ Rust now executes the recovered insertion/partition/median/ninther sort without assuming comparator Ord; source parity includes unstable tie permutations. Focused `cargo xtask test-group rust-rank-sort` passes (9 Python tests); selected reports regenerate twice deterministically, with storage, documentation links and whitespace guards. Full suite deferred to task ten. Original assets unchanged; source fixtures do not establish complete native/runtime parity.
docs/REVERSE_ENGINEERING_TODO.md CHANGED
@@ -1429,3 +1429,6 @@ Purpose: turn the supplied installation into an evidence-backed specification of
1429
 
1430
 
1431
  - [x] RE-793 Mixed ranking populations and positional rank-prefix behavior ([evidence](research/QVM_RANKING_POPULATION.md)).
 
 
 
 
1429
 
1430
 
1431
  - [x] RE-793 Mixed ranking populations and positional rank-prefix behavior ([evidence](research/QVM_RANKING_POPULATION.md)).
1432
+
1433
+
1434
+ - [x] RE-794 Rust source-shaped aligned ranking sort with exact tie order ([evidence](research/RUST_RANK_SORT.md)).
docs/research/README.md CHANGED
@@ -969,3 +969,6 @@ The extension collector adds five focused tests for native tail calls, command-r
969
 
970
 
971
  [RE-793: Mixed ranking populations and positional rank-prefix behavior](QVM_RANKING_POPULATION.md).
 
 
 
 
969
 
970
 
971
  [RE-793: Mixed ranking populations and positional rank-prefix behavior](QVM_RANKING_POPULATION.md).
972
+
973
+
974
+ [RE-794: Rust source-shaped aligned ranking sort with exact tie order](RUST_RANK_SORT.md).
docs/research/RUST_RANK_SORT.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Rust source-shaped aligned ranking sort with exact tie order
2
+
3
+ RE-794. Reproduce with `python3 scripts/analyze_rust_rank_sort.py`; focused validation: `cargo xtask test-group rust-rank-sort`. [Machine report](rust-rank-sort.json).
4
+
5
+ The Rust implementation follows source pivot selection, equal-key partition swaps, insertion fallback, left recursion and right tail iteration.140 original-QVM fixtures check exact resulting order rather than merely equivalent sorted keys. Unit tests cover partition boundaries, signed score extremes, original equal-key pivot permutation and rejection without mutation. Public inputs remain eligible complete permutations with at most64 clients. Mixed raw comparator behavior is reserved for a separate audited composition.
6
+
7
+ Original executable/native runtime and complete Linux/macOS Rust/Bevy/Solari acceptance remain unverified.
docs/research/rust-health-impulse.json CHANGED
@@ -2985,7 +2985,7 @@
2985
  "exit_code": 2
2986
  }
2987
  ],
2988
- "rust_unit_tests": 54,
2989
  "source_report": "docs/research/qvm-health-impulse.json",
2990
  "source_report_sha256": "625fd8064004ea6dbdf398d51d0af844d9c635ff04c0a0790886dddb6ab81e71"
2991
  }
 
2985
  "exit_code": 2
2986
  }
2987
  ],
2988
+ "rust_unit_tests": 57,
2989
  "source_report": "docs/research/qvm-health-impulse.json",
2990
  "source_report_sha256": "625fd8064004ea6dbdf398d51d0af844d9c635ff04c0a0790886dddb6ab81e71"
2991
  }
docs/research/rust-rank-sort.json ADDED
The diff for this file is too large to render. See raw diff
 
scripts/analyze_rust_rank_sort.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import hashlib,json
3
+ from pathlib import Path
4
+ from analyze_rust_ranking import words
5
+ from rust_lifecycle_support import binary,result,invoke
6
+ from analyze_qvm import write_json
7
+
8
+ def main():
9
+ b=binary();rows=[];sources={}
10
+ for filename in ['qvm-rank-small-sort','qvm-rank-large-sort']:
11
+ path=Path('docs/research/'+filename+'.json');data=path.read_bytes();sources[str(path)]=hashlib.sha256(data).hexdigest()
12
+ for p in json.loads(data)['probes']:
13
+ c=p['clients'];order=p['initial'];r=result(b,'rank-sort',[p['mode'],len(order),*order,*[v for row in c[:len(order)] for v in words(row)]]) if len(c)==len(order) else None
14
+ # Small fixtures keep six seed records even for shorter arrays.
15
+ if r is None:
16
+ used=sorted(set(order));remap={v:j for j,v in enumerate(used)};r=result(b,'rank-sort',[p['mode'],len(used),*[remap[v] for v in order],*[v for j in used for v in words(c[j])]]);r['order']=[used[j] for j in r['order']]
17
+ rows.append({'mode':p['mode'],'initial':order,'source':p['final'],'rust':r,'clients':c})
18
+ bad=invoke(b,'rank-sort',[0,1,0,*words({'468':1})],check=False);assert bad.returncode!=0
19
+ write_json(Path('docs/research/rust-rank-sort.json'),{'source_reports':sources,'probes':rows,'rejections':[bad.returncode],'limits':'Rust source-shaped35095 word-index sort, exact source order including unstable ties across140 fixtures. Eligible complete permutations only; mixed raw prefilters, native ABI and full ranking publication remain outside this API.'})
20
+ if __name__=='__main__':main()
scripts/test_rust_rank_sort.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib,json,unittest
2
+ from pathlib import Path
3
+ class RustRankSortTests(unittest.TestCase):
4
+ @classmethod
5
+ def setUpClass(cls):cls.r=json.loads(Path('docs/research/rust-rank-sort.json').read_text())
6
+ def test_exact_source_order_including_tie_permutations(self):
7
+ self.assertEqual(len(self.r['probes']),140)
8
+ for p in self.r['probes']:self.assertEqual(p['rust']['order'],p['source'],(p['mode'],p['initial']))
9
+ def test_source_identity_and_domain_rejection(self):
10
+ for path,sha in self.r['source_reports'].items():self.assertEqual(hashlib.sha256(Path(path).read_bytes()).hexdigest(),sha)
11
+ self.assertTrue(all(v!=0 for v in self.r['rejections']))
12
+ if __name__=='__main__':unittest.main()
xtask/checks.tsv CHANGED
@@ -1792,3 +1792,18 @@ group ranking-population scripts/analyze_qvm_ranking_population.py
1792
  edge scripts/analyze_qvm_ranking_population.py scripts/analyze_qvm_resumer_audit.py
1793
 
1794
  group ranking-population scripts/analyze_qvm_rank_large_sort.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1792
  edge scripts/analyze_qvm_ranking_population.py scripts/analyze_qvm_resumer_audit.py
1793
 
1794
  group ranking-population scripts/analyze_qvm_rank_large_sort.py
1795
+ edge Cargo.toml scripts/analyze_rust_rank_sort.py
1796
+ edge Cargo.lock scripts/analyze_rust_rank_sort.py
1797
+ edge crates/urt-gameplay/Cargo.toml scripts/analyze_rust_rank_sort.py
1798
+ edge crates/urt-gameplay/src/lib.rs scripts/analyze_rust_rank_sort.py
1799
+ edge crates/urt-gameplay/src/bin/urt-lifecycle.rs scripts/analyze_rust_rank_sort.py
1800
+ edge crates/urt-gameplay/src/rank_sort.rs scripts/analyze_rust_rank_sort.py
1801
+ edge crates/urt-gameplay/src/ranking.rs scripts/analyze_rust_rank_sort.py
1802
+ edge crates/urt-gameplay/src/rank_sort.rs scripts/analyze_rust_health_impulse.py
1803
+ group gameplay scripts/analyze_rust_rank_sort.py
1804
+
1805
+ group rust-rank-sort scripts/analyze_rust_health_impulse.py
1806
+ report scripts/analyze_rust_rank_sort.py docs/research/rust-rank-sort.json
1807
+ edge docs/research/rust-rank-sort.json scripts/test_rust_rank_sort.py
1808
+ group rust-rank-sort scripts/analyze_rust_rank_sort.py
1809
+ edge scripts/analyze_rust_rank_sort.py scripts/analyze_qvm_resumer_audit.py
xtask/src/lib.rs CHANGED
@@ -1041,6 +1041,7 @@ mod integration_tests {
1041
  ("recent_awards", "recent_awards"),
1042
  ("recipient_payload", "recipient_payload"),
1043
  ("ranking", "ranking"),
 
1044
  ("leaderboard", "leaderboard"),
1045
  ("recipient_wire", "recipient_wire"),
1046
  ] {
 
1041
  ("recent_awards", "recent_awards"),
1042
  ("recipient_payload", "recipient_payload"),
1043
  ("ranking", "ranking"),
1044
+ ("rank_sort", "rank_sort"),
1045
  ("leaderboard", "leaderboard"),
1046
  ("recipient_wire", "recipient_wire"),
1047
  ] {