Skip to content

Commit f6aa311

Browse files
authored
Merge pull request #14 from Steake/tournament-mii-plus-system
feat: Implement MII+ tournament system with adaptive strategy
2 parents 590fa57 + f63e952 commit f6aa311

15 files changed

Lines changed: 2000 additions & 35 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ members = [
1111
"crates/bitcell-network",
1212
"crates/bitcell-node",
1313
"crates/bitcell-admin",
14+
"crates/bitcell-simulation",
1415
"crates/bitcell-wallet",
1516
]
1617
resolver = "2"
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
//! Block API endpoints
2+
3+
use axum::{
4+
extract::{Path, State},
5+
http::StatusCode,
6+
Json,
7+
};
8+
use serde::Serialize;
9+
use std::sync::Arc;
10+
11+
use crate::AppState;
12+
use bitcell_ca::{Battle, BattleOutcome, Glider, GliderPattern, Position};
13+
14+
#[derive(Debug, Serialize)]
15+
pub struct BlockInfo {
16+
pub height: u64,
17+
pub hash: String,
18+
pub timestamp: u64,
19+
pub proposer: String,
20+
pub transaction_count: usize,
21+
pub battle_count: usize,
22+
}
23+
24+
#[derive(Debug, Serialize)]
25+
pub struct BlockListResponse {
26+
pub blocks: Vec<BlockInfo>,
27+
pub total: usize,
28+
}
29+
30+
#[derive(Debug, Serialize)]
31+
pub struct BlockDetailResponse {
32+
pub height: u64,
33+
pub hash: String,
34+
pub timestamp: u64,
35+
pub proposer: String,
36+
pub prev_hash: String,
37+
pub tx_root: String,
38+
pub state_root: String,
39+
pub transactions: Vec<TransactionInfo>,
40+
pub battle_count: usize,
41+
}
42+
43+
#[derive(Debug, Serialize)]
44+
pub struct TransactionInfo {
45+
pub hash: String,
46+
pub from: String,
47+
pub to: String,
48+
pub amount: u64,
49+
}
50+
51+
#[derive(Debug, Serialize)]
52+
pub struct BlockBattleFrame {
53+
pub step: usize,
54+
pub grid: Vec<Vec<u8>>,
55+
pub energy_a: u64,
56+
pub energy_b: u64,
57+
}
58+
59+
#[derive(Debug, Serialize)]
60+
pub struct BlockBattleVisualization {
61+
pub block_height: u64,
62+
pub battle_index: usize,
63+
pub glider_a_pattern: String,
64+
pub glider_b_pattern: String,
65+
pub winner: String,
66+
pub steps: usize,
67+
pub frames: Vec<BlockBattleFrame>,
68+
}
69+
70+
/// List recent blocks
71+
pub async fn list_blocks(
72+
State(state): State<Arc<AppState>>,
73+
) -> Result<Json<BlockListResponse>, (StatusCode, Json<String>)> {
74+
// Get all registered nodes
75+
let nodes = state.process.list_nodes();
76+
77+
if nodes.is_empty() {
78+
return Err((
79+
StatusCode::SERVICE_UNAVAILABLE,
80+
Json("No nodes available. Please deploy nodes first.".to_string()),
81+
));
82+
}
83+
84+
// Try to fetch blocks from the first running node
85+
// In a real implementation, this would query the blockchain via RPC
86+
// For now, we'll return mock data based on chain height from metrics
87+
88+
let endpoints: Vec<(String, String)> = nodes
89+
.iter()
90+
.map(|n| {
91+
let metrics_port = n.port + 1;
92+
(n.id.clone(), format!("http://127.0.0.1:{}/metrics", metrics_port))
93+
})
94+
.collect();
95+
96+
if endpoints.is_empty() {
97+
return Err((
98+
StatusCode::SERVICE_UNAVAILABLE,
99+
Json("No running nodes found.".to_string()),
100+
));
101+
}
102+
103+
// Fetch current chain height
104+
let aggregated = state.metrics_client.aggregate_metrics(&endpoints)
105+
.await
106+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(e)))?;
107+
108+
let chain_height = aggregated.chain_height;
109+
110+
// Generate mock block list (most recent 10 blocks)
111+
let start_height = chain_height.saturating_sub(9);
112+
let mut blocks = Vec::new();
113+
114+
for height in start_height..=chain_height {
115+
blocks.push(BlockInfo {
116+
height,
117+
hash: format!("0x{:016x}", height * 12345),
118+
timestamp: 1700000000 + (height * 600), // 10 min blocks
119+
proposer: format!("miner-{}", height % 3),
120+
transaction_count: (height % 5) as usize,
121+
battle_count: 1, // Each block has 1 battle in simplified model
122+
});
123+
}
124+
125+
// Reverse to show newest first
126+
blocks.reverse();
127+
128+
Ok(Json(BlockListResponse {
129+
total: blocks.len(),
130+
blocks,
131+
}))
132+
}
133+
134+
/// Get block details by height
135+
pub async fn get_block(
136+
State(state): State<Arc<AppState>>,
137+
Path(height): Path<u64>,
138+
) -> Result<Json<BlockDetailResponse>, (StatusCode, Json<String>)> {
139+
// In a real implementation, this would fetch the actual block from the blockchain
140+
// For now, return mock data
141+
142+
let nodes = state.process.list_nodes();
143+
if nodes.is_empty() {
144+
return Err((
145+
StatusCode::SERVICE_UNAVAILABLE,
146+
Json("No nodes available.".to_string()),
147+
));
148+
}
149+
150+
// Handle edge case of height == 0 to prevent underflow
151+
if height == 0 {
152+
return Err((
153+
StatusCode::BAD_REQUEST,
154+
Json("Invalid block height: cannot be 0".to_string()),
155+
));
156+
}
157+
158+
Ok(Json(BlockDetailResponse {
159+
height,
160+
hash: format!("0x{:016x}", height * 12345),
161+
timestamp: 1700000000 + (height * 600),
162+
proposer: format!("miner-{}", height % 3),
163+
prev_hash: format!("0x{:016x}", (height - 1) * 12345),
164+
tx_root: format!("0x{:016x}", height * 54321),
165+
state_root: format!("0x{:016x}", height * 98765),
166+
transactions: vec![],
167+
battle_count: 1,
168+
}))
169+
}
170+
171+
/// Get battle visualization for a specific block
172+
pub async fn get_block_battles(
173+
State(_state): State<Arc<AppState>>,
174+
Path(height): Path<u64>,
175+
) -> Result<Json<Vec<BlockBattleVisualization>>, (StatusCode, Json<String>)> {
176+
tracing::info!("Fetching battle visualization for block {}", height);
177+
178+
// In a real implementation, we would:
179+
// 1. Fetch the block from the blockchain
180+
// 2. Extract the glider reveals from the tournament data
181+
// 3. Re-simulate the battles
182+
//
183+
// For now, we'll simulate a deterministic battle based on block height
184+
// to demonstrate the visualization
185+
186+
let battle_index = 0;
187+
188+
// Deterministically choose glider patterns based on block height
189+
let patterns = [
190+
GliderPattern::Standard,
191+
GliderPattern::Lightweight,
192+
GliderPattern::Middleweight,
193+
GliderPattern::Heavyweight,
194+
];
195+
196+
let pattern_a = patterns[(height % 4) as usize];
197+
let pattern_b = patterns[((height + 1) % 4) as usize];
198+
199+
// Create gliders
200+
let glider_a = Glider::new(pattern_a, Position::new(256, 512));
201+
let glider_b = Glider::new(pattern_b, Position::new(768, 512));
202+
203+
// Create battle with fewer steps for faster rendering
204+
let steps = 500;
205+
let frame_count = 20;
206+
let downsample_size = 128;
207+
208+
// Generate entropy seed from block height
209+
let mut entropy_seed = [0u8; 32];
210+
let height_bytes = height.to_le_bytes();
211+
// Fill entropy seed with deterministic but varied data based on height
212+
for i in 0..32 {
213+
entropy_seed[i] = height_bytes[i % 8].wrapping_mul((i as u8).wrapping_add(1));
214+
}
215+
216+
let battle = Battle::with_entropy(glider_a, glider_b, steps, entropy_seed);
217+
218+
// Calculate sample steps
219+
let sample_interval = steps / frame_count;
220+
let mut sample_steps: Vec<usize> = (0..frame_count)
221+
.map(|i| i * sample_interval)
222+
.collect();
223+
sample_steps.push(steps);
224+
225+
// Run simulation in blocking task
226+
let (outcome, frames) = tokio::task::spawn_blocking(move || {
227+
let outcome = battle.simulate();
228+
let grids = battle.grid_states(&sample_steps);
229+
230+
let mut frames = Vec::new();
231+
for (i, grid) in grids.iter().enumerate() {
232+
let step = sample_steps[i];
233+
let (energy_a, energy_b) = battle.measure_regional_energy(grid);
234+
let downsampled = grid.downsample(downsample_size);
235+
236+
frames.push(BlockBattleFrame {
237+
step,
238+
grid: downsampled,
239+
energy_a,
240+
energy_b,
241+
});
242+
}
243+
244+
(outcome, frames)
245+
})
246+
.await
247+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(format!("Task join error: {}", e))))?;
248+
249+
let winner = match outcome {
250+
BattleOutcome::AWins => "glider_a",
251+
BattleOutcome::BWins => "glider_b",
252+
BattleOutcome::Tie => "tie",
253+
};
254+
255+
let pattern_name = |p: GliderPattern| match p {
256+
GliderPattern::Standard => "Standard",
257+
GliderPattern::Lightweight => "Lightweight",
258+
GliderPattern::Middleweight => "Middleweight",
259+
GliderPattern::Heavyweight => "Heavyweight",
260+
};
261+
262+
let visualization = BlockBattleVisualization {
263+
block_height: height,
264+
battle_index,
265+
glider_a_pattern: pattern_name(pattern_a).to_string(),
266+
glider_b_pattern: pattern_name(pattern_b).to_string(),
267+
winner: winner.to_string(),
268+
steps,
269+
frames,
270+
};
271+
272+
Ok(Json(vec![visualization]))
273+
}

crates/bitcell-admin/src/api/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub mod deployment;
66
pub mod config;
77
pub mod test;
88
pub mod setup;
9+
pub mod blocks;
910

1011
use std::collections::HashMap;
1112
use std::sync::RwLock;

crates/bitcell-admin/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ impl AdminConsole {
112112
.route("/api/setup/data-dir", post(api::setup::set_data_dir))
113113
.route("/api/setup/complete", post(api::setup::complete_setup))
114114

115+
.route("/api/blocks", get(api::blocks::list_blocks))
116+
.route("/api/blocks/:height", get(api::blocks::get_block))
117+
.route("/api/blocks/:height/battles", get(api::blocks::get_block_battles))
118+
115119
// Static files
116120
.nest_service("/static", ServeDir::new("static"))
117121

0 commit comments

Comments
 (0)