Skip to content

Commit bd77cdf

Browse files
committed
Add real battle visualization and cypherpunk-neue UI to admin console
Features added: - Real process management for nodes (spawn actual bitcell-node processes) - Actual CA battle testing with real simulation (not mocked) - Battle visualization API with downsampled grid frames - Interactive battle playback with play/pause and frame scrubbing - Beautiful cypherpunk-neue aesthetic with: - Neon green (#00ffaa) color scheme - Scanline effects and grid backgrounds - Glowing text and borders with pulsing animations - Monospace fonts (Share Tech Mono, Orbitron) - Matrix-inspired dark theme Technical improvements: - Made Battle::measure_regional_energy public - Added Battle::grid_states() for capturing frames at intervals - Added Grid::downsample() for efficient visualization - Real-time CA simulation using tokio::spawn_blocking - Canvas-based rendering with color-coded regions - Unix signal handling for graceful node shutdown All 158 tests passing.
1 parent f35fd0f commit bd77cdf

10 files changed

Lines changed: 1000 additions & 183 deletions

File tree

crates/bitcell-admin/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,19 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
3434
# Time
3535
chrono = { version = "0.4", features = ["serde"] }
3636

37+
# Sync primitives
38+
parking_lot = "0.12"
39+
3740
# BitCell dependencies
3841
bitcell-node = { path = "../bitcell-node" }
3942
bitcell-consensus = { path = "../bitcell-consensus" }
4043
bitcell-state = { path = "../bitcell-state" }
4144
bitcell-network = { path = "../bitcell-network" }
4245
bitcell-crypto = { path = "../bitcell-crypto" }
46+
bitcell-ca = { path = "../bitcell-ca" }
47+
48+
# Unix process management
49+
[target.'cfg(unix)'.dependencies]
50+
libc = "0.2"
4351

4452
[dev-dependencies]

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

Lines changed: 16 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub struct StartNodeRequest {
3636
pub async fn list_nodes(
3737
State(state): State<Arc<AppState>>,
3838
) -> Result<Json<NodesResponse>, (StatusCode, Json<ErrorResponse>)> {
39-
let nodes = state.api.list_nodes();
39+
let nodes = state.process.list_nodes();
4040
let total = nodes.len();
4141

4242
Ok(Json(NodesResponse { nodes, total }))
@@ -47,7 +47,7 @@ pub async fn get_node(
4747
State(state): State<Arc<AppState>>,
4848
Path(id): Path<String>,
4949
) -> Result<Json<NodeResponse>, (StatusCode, Json<ErrorResponse>)> {
50-
match state.api.get_node(&id) {
50+
match state.process.get_node(&id) {
5151
Some(node) => Ok(Json(NodeResponse { node })),
5252
None => Err((
5353
StatusCode::NOT_FOUND,
@@ -64,29 +64,15 @@ pub async fn start_node(
6464
Path(id): Path<String>,
6565
Json(_req): Json<StartNodeRequest>,
6666
) -> Result<Json<NodeResponse>, (StatusCode, Json<ErrorResponse>)> {
67-
// Update status to starting
68-
if !state.api.update_node_status(&id, NodeStatus::Starting) {
69-
return Err((
70-
StatusCode::NOT_FOUND,
71-
Json(ErrorResponse {
72-
error: format!("Node '{}' not found", id),
73-
}),
74-
));
75-
}
76-
77-
// TODO: Actually start the node process
78-
// For now, simulate starting
79-
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
80-
81-
// Update to running
82-
state.api.update_node_status(&id, NodeStatus::Running);
83-
84-
match state.api.get_node(&id) {
85-
Some(node) => Ok(Json(NodeResponse { node })),
86-
None => Err((
67+
match state.process.start_node(&id) {
68+
Ok(node) => {
69+
tracing::info!("Started node '{}' successfully", id);
70+
Ok(Json(NodeResponse { node }))
71+
}
72+
Err(e) => Err((
8773
StatusCode::INTERNAL_SERVER_ERROR,
8874
Json(ErrorResponse {
89-
error: "Failed to retrieve node after starting".to_string(),
75+
error: format!("Failed to start node '{}': {}", id, e),
9076
}),
9177
)),
9278
}
@@ -97,29 +83,15 @@ pub async fn stop_node(
9783
State(state): State<Arc<AppState>>,
9884
Path(id): Path<String>,
9985
) -> Result<Json<NodeResponse>, (StatusCode, Json<ErrorResponse>)> {
100-
// Update status to stopping
101-
if !state.api.update_node_status(&id, NodeStatus::Stopping) {
102-
return Err((
103-
StatusCode::NOT_FOUND,
104-
Json(ErrorResponse {
105-
error: format!("Node '{}' not found", id),
106-
}),
107-
));
108-
}
109-
110-
// TODO: Actually stop the node process
111-
// For now, simulate stopping
112-
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
113-
114-
// Update to stopped
115-
state.api.update_node_status(&id, NodeStatus::Stopped);
116-
117-
match state.api.get_node(&id) {
118-
Some(node) => Ok(Json(NodeResponse { node })),
119-
None => Err((
86+
match state.process.stop_node(&id) {
87+
Ok(node) => {
88+
tracing::info!("Stopped node '{}' successfully", id);
89+
Ok(Json(NodeResponse { node }))
90+
}
91+
Err(e) => Err((
12092
StatusCode::INTERNAL_SERVER_ERROR,
12193
Json(ErrorResponse {
122-
error: "Failed to retrieve node after stopping".to_string(),
94+
error: format!("Failed to stop node '{}': {}", id, e),
12395
}),
12496
)),
12597
}

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

Lines changed: 203 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ use std::sync::Arc;
1010

1111
use crate::AppState;
1212

13+
// Import BitCell types
14+
use bitcell_ca::{Battle, Glider, GliderPattern, Position, BattleOutcome};
15+
1316
#[derive(Debug, Deserialize)]
1417
pub struct RunBattleTestRequest {
1518
pub glider_a: String,
@@ -27,6 +30,33 @@ pub struct BattleTestResponse {
2730
pub duration_ms: u64,
2831
}
2932

33+
#[derive(Debug, Deserialize)]
34+
pub struct BattleVisualizationRequest {
35+
pub glider_a: String,
36+
pub glider_b: String,
37+
pub steps: Option<usize>,
38+
pub frame_count: Option<usize>,
39+
pub downsample_size: Option<usize>,
40+
}
41+
42+
#[derive(Debug, Serialize)]
43+
pub struct BattleVisualizationResponse {
44+
pub test_id: String,
45+
pub winner: String,
46+
pub steps: usize,
47+
pub final_energy_a: u64,
48+
pub final_energy_b: u64,
49+
pub frames: Vec<BattleFrame>,
50+
}
51+
52+
#[derive(Debug, Serialize)]
53+
pub struct BattleFrame {
54+
pub step: usize,
55+
pub grid: Vec<Vec<u8>>,
56+
pub energy_a: u64,
57+
pub energy_b: u64,
58+
}
59+
3060
#[derive(Debug, Deserialize)]
3161
pub struct SendTestTransactionRequest {
3262
pub from: Option<String>,
@@ -41,23 +71,85 @@ pub struct TransactionTestResponse {
4171
pub message: String,
4272
}
4373

74+
fn parse_glider_pattern(name: &str) -> Result<GliderPattern, String> {
75+
match name.to_lowercase().as_str() {
76+
"standard" => Ok(GliderPattern::Standard),
77+
"lightweight" | "lwss" => Ok(GliderPattern::Lightweight),
78+
"middleweight" | "mwss" => Ok(GliderPattern::Middleweight),
79+
"heavyweight" | "hwss" => Ok(GliderPattern::Heavyweight),
80+
_ => Err(format!("Unknown glider pattern: {}", name)),
81+
}
82+
}
83+
4484
/// Run a battle test
4585
pub async fn run_battle_test(
4686
State(_state): State<Arc<AppState>>,
4787
Json(req): Json<RunBattleTestRequest>,
4888
) -> Result<Json<BattleTestResponse>, (StatusCode, Json<String>)> {
49-
// TODO: Actually run battle simulation
50-
// For now, return mock response
51-
5289
let test_id = format!("test-{}", chrono::Utc::now().timestamp());
5390

91+
tracing::info!("Running battle test: {} vs {}", req.glider_a, req.glider_b);
92+
93+
// Parse glider patterns
94+
let pattern_a = parse_glider_pattern(&req.glider_a)
95+
.map_err(|e| (StatusCode::BAD_REQUEST, Json(e)))?;
96+
97+
let pattern_b = parse_glider_pattern(&req.glider_b)
98+
.map_err(|e| (StatusCode::BAD_REQUEST, Json(e)))?;
99+
100+
// Create gliders
101+
let glider_a = Glider::new(pattern_a, Position::new(256, 512));
102+
let glider_b = Glider::new(pattern_b, Position::new(768, 512));
103+
104+
// Create battle
105+
let steps = req.steps.unwrap_or(1000);
106+
let battle = if steps != 1000 {
107+
Battle::with_steps(glider_a, glider_b, steps)
108+
} else {
109+
Battle::new(glider_a, glider_b)
110+
};
111+
112+
// Run battle simulation
113+
let start = std::time::Instant::now();
114+
115+
let (outcome, energy_a, energy_b) = tokio::task::spawn_blocking(move || {
116+
// Simulate the battle
117+
let outcome = battle.simulate()
118+
.map_err(|e| format!("Battle simulation error: {:?}", e))?;
119+
120+
// Get final grid to measure energies
121+
let final_grid = battle.final_grid();
122+
let (energy_a, energy_b) = battle.measure_regional_energy(&final_grid);
123+
124+
Ok::<_, String>((outcome, energy_a, energy_b))
125+
})
126+
.await
127+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(format!("Task join error: {}", e))))?
128+
.map_err(|e: String| (StatusCode::INTERNAL_SERVER_ERROR, Json(e)))?;
129+
130+
let duration = start.elapsed();
131+
132+
let winner = match outcome {
133+
BattleOutcome::AWins => "glider_a".to_string(),
134+
BattleOutcome::BWins => "glider_b".to_string(),
135+
BattleOutcome::Tie => "tie".to_string(),
136+
};
137+
138+
tracing::info!(
139+
"Battle test completed: winner={}, energy_a={}, energy_b={}, duration={}ms",
140+
winner,
141+
energy_a,
142+
energy_b,
143+
duration.as_millis()
144+
);
145+
54146
let response = BattleTestResponse {
55147
test_id,
56-
winner: "glider_a".to_string(),
57-
steps: req.steps.unwrap_or(1000),
58-
final_energy_a: 8500,
59-
final_energy_b: 7200,
60-
duration_ms: 235,
148+
winner,
149+
steps,
150+
final_energy_a: energy_a,
151+
final_energy_b: energy_b,
152+
duration_ms: duration.as_millis() as u64,
61153
};
62154

63155
Ok(Json(response))
@@ -68,19 +160,118 @@ pub async fn send_test_transaction(
68160
State(_state): State<Arc<AppState>>,
69161
Json(req): Json<SendTestTransactionRequest>,
70162
) -> Result<Json<TransactionTestResponse>, (StatusCode, Json<String>)> {
71-
// TODO: Actually send transaction
72-
// For now, return mock response
163+
// TODO: Actually send transaction to a running node
164+
// For now, return a formatted response
73165

74166
let tx_hash = format!("0x{:x}", chrono::Utc::now().timestamp());
75167

76168
let response = TransactionTestResponse {
77169
tx_hash,
78170
status: "pending".to_string(),
79-
message: format!("Test transaction sent: {} -> {}",
171+
message: format!(
172+
"Test transaction sent: {} -> {} ({} units)",
80173
req.from.unwrap_or_else(|| "genesis".to_string()),
81-
req.to
174+
req.to,
175+
req.amount
82176
),
83177
};
84178

179+
tracing::info!("Test transaction: {}", response.message);
180+
181+
Ok(Json(response))
182+
}
183+
184+
/// Run a battle with visualization frames
185+
pub async fn run_battle_visualization(
186+
State(_state): State<Arc<AppState>>,
187+
Json(req): Json<BattleVisualizationRequest>,
188+
) -> Result<Json<BattleVisualizationResponse>, (StatusCode, Json<String>)> {
189+
let test_id = format!("viz-{}", chrono::Utc::now().timestamp());
190+
191+
tracing::info!("Running battle visualization: {} vs {}", req.glider_a, req.glider_b);
192+
193+
// Parse glider patterns
194+
let pattern_a = parse_glider_pattern(&req.glider_a)
195+
.map_err(|e| (StatusCode::BAD_REQUEST, Json(e)))?;
196+
197+
let pattern_b = parse_glider_pattern(&req.glider_b)
198+
.map_err(|e| (StatusCode::BAD_REQUEST, Json(e)))?;
199+
200+
// Create gliders
201+
let glider_a = Glider::new(pattern_a, Position::new(256, 512));
202+
let glider_b = Glider::new(pattern_b, Position::new(768, 512));
203+
204+
// Create battle
205+
let steps = req.steps.unwrap_or(1000);
206+
let frame_count = req.frame_count.unwrap_or(20).min(100); // Max 100 frames
207+
let downsample_size = req.downsample_size.unwrap_or(128).min(512); // Max 512x512
208+
209+
let battle = if steps != 1000 {
210+
Battle::with_steps(glider_a, glider_b, steps)
211+
} else {
212+
Battle::new(glider_a, glider_b)
213+
};
214+
215+
// Calculate which steps to capture
216+
let sample_interval = steps / frame_count;
217+
let mut sample_steps: Vec<usize> = (0..frame_count)
218+
.map(|i| i * sample_interval)
219+
.collect();
220+
sample_steps.push(steps); // Always include final step
221+
222+
// Run simulation and capture frames
223+
let (outcome, frames) = tokio::task::spawn_blocking(move || {
224+
// Get outcome
225+
let outcome = battle.simulate()
226+
.map_err(|e| format!("Battle simulation error: {:?}", e))?;
227+
228+
// Get grid states at sample steps
229+
let grids = battle.grid_states(&sample_steps);
230+
231+
// Create frames with downsampled grids and energy measurements
232+
let mut frames = Vec::new();
233+
for (i, grid) in grids.iter().enumerate() {
234+
let step = sample_steps[i];
235+
let (energy_a, energy_b) = battle.measure_regional_energy(grid);
236+
let downsampled = grid.downsample(downsample_size);
237+
238+
frames.push(BattleFrame {
239+
step,
240+
grid: downsampled,
241+
energy_a,
242+
energy_b,
243+
});
244+
}
245+
246+
Ok::<_, String>((outcome, frames))
247+
})
248+
.await
249+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(format!("Task join error: {}", e))))?
250+
.map_err(|e: String| (StatusCode::INTERNAL_SERVER_ERROR, Json(e)))?;
251+
252+
let winner = match outcome {
253+
BattleOutcome::AWins => "glider_a".to_string(),
254+
BattleOutcome::BWins => "glider_b".to_string(),
255+
BattleOutcome::Tie => "tie".to_string(),
256+
};
257+
258+
let final_energy_a = frames.last().map(|f| f.energy_a).unwrap_or(0);
259+
let final_energy_b = frames.last().map(|f| f.energy_b).unwrap_or(0);
260+
261+
tracing::info!(
262+
"Battle visualization completed: winner={}, {} frames captured",
263+
winner,
264+
frames.len()
265+
);
266+
267+
let response = BattleVisualizationResponse {
268+
test_id,
269+
winner,
270+
steps,
271+
final_energy_a,
272+
final_energy_b,
273+
frames,
274+
};
275+
85276
Ok(Json(response))
86277
}

0 commit comments

Comments
 (0)