Files
games/crates/apps/game-snake-poc-wasm/src/runtime.rs
2026-09-20 21:30:12 +02:00

331 lines
12 KiB
Rust

// file: crates/apps/game-snake-poc-wasm/src/runtime.rs
// version: 3
const FIXED_STEP_MILLIS: u64 = 16;
/// WebAssembly-owned Snake game state consumed by the direct browser host.
#[wasm_bindgen::prelude::wasm_bindgen]
pub struct SnakeWasmGame {
pending_input: engine_v1_common::InputState,
provenance: engine_v1_platform_api::RuntimeProvenance,
runner: engine_v1_common::FixedStepRunner,
state: game_snake_poc::SnakeState,
}
#[wasm_bindgen::prelude::wasm_bindgen]
impl SnakeWasmGame {
/// Creates a fresh Snake POC session using the shared gameplay crate.
#[wasm_bindgen::prelude::wasm_bindgen(constructor)]
pub fn new() -> Self {
return Self {
pending_input: engine_v1_common::InputState::none(),
provenance: engine_v1_platform_api::RuntimeProvenance::new(
engine_v1_platform_api::DeviceClass::Unknown,
engine_v1_platform_api::ExecutionModel::Wasm,
engine_v1_platform_api::InputProfile::Unknown,
engine_v1_platform_api::PlatformFamily::Web,
engine_v1_platform_api::RuntimeHost::Browser,
),
runner: engine_v1_common::FixedStepRunner::new(std::time::Duration::from_millis(FIXED_STEP_MILLIS)),
state: game_snake_poc::SnakeState::new(),
};
}
/// Updates host-observed runtime provenance while preserving WebAssembly execution.
pub fn configure_runtime_provenance(&mut self, platform_family: &str, runtime_host: &str, device_class: &str, input_profile: &str) -> bool {
let platform_family = match parse_platform_family(platform_family) {
Some(value) => value,
None => return false,
};
let runtime_host = match parse_runtime_host(runtime_host) {
Some(value) => value,
None => return false,
};
let device_class = match parse_device_class(device_class) {
Some(value) => value,
None => return false,
};
let input_profile = match parse_input_profile(input_profile) {
Some(value) => value,
None => return false,
};
self.provenance = engine_v1_platform_api::RuntimeProvenance::new(
device_class,
engine_v1_platform_api::ExecutionModel::Wasm,
input_profile,
platform_family,
runtime_host,
);
return true;
}
/// Returns the current physical device class label.
pub fn provenance_device_class(&self) -> String {
return device_class_label(self.provenance.device_class()).to_string();
}
/// Returns the current execution model label.
pub fn provenance_execution_model(&self) -> String {
return execution_model_label(self.provenance.execution_model()).to_string();
}
/// Returns the current primary input profile label.
pub fn provenance_input_profile(&self) -> String {
return input_profile_label(self.provenance.input_profile()).to_string();
}
/// Returns the current platform family label.
pub fn provenance_platform_family(&self) -> String {
return platform_family_label(self.provenance.platform_family()).to_string();
}
/// Returns the current runtime host label.
pub fn provenance_runtime_host(&self) -> String {
return runtime_host_label(self.provenance.runtime_host()).to_string();
}
/// Queues a logical left direction for the next deterministic update.
pub fn left(&mut self) {
self.queue_direction(engine_v1_common::GameAction::Left);
return;
}
/// Queues a logical right direction for the next deterministic update.
pub fn right(&mut self) {
self.queue_direction(engine_v1_common::GameAction::Right);
return;
}
/// Queues a logical upward direction for the next deterministic update.
pub fn up(&mut self) {
self.queue_direction(engine_v1_common::GameAction::Up);
return;
}
/// Queues a logical downward direction for the next deterministic update.
pub fn down(&mut self) {
self.queue_direction(engine_v1_common::GameAction::Down);
return;
}
/// Advances one deterministic update and consumes the queued direction.
pub fn tick(&mut self) {
let input = self.pending_input;
self.pending_input = engine_v1_common::InputState::none();
self.runner.tick(&mut self.state, input);
return;
}
/// Returns the current gameplay score.
pub fn score(&self) -> u32 {
return self.state.score().min(u32::MAX as u64) as u32;
}
/// Returns the current Snake length.
pub fn length(&self) -> u32 {
return self.state.length().min(u32::MAX as usize) as u32;
}
/// Returns the current scene background red channel.
pub fn background_red(&self) -> u8 {
return self.scene().background().red();
}
/// Returns the current scene background green channel.
pub fn background_green(&self) -> u8 {
return self.scene().background().green();
}
/// Returns the current scene background blue channel.
pub fn background_blue(&self) -> u8 {
return self.scene().background().blue();
}
/// Returns the number of occupied rectangle slots in the current scene.
pub fn rectangle_count(&self) -> u32 {
let scene = self.scene();
let mut count = 0_u32;
for slot in scene.rectangles() {
if slot.is_some() {
count = count.saturating_add(1);
}
}
return count;
}
/// Returns one rectangle's normalized left coordinate or `-1.0` if absent.
pub fn rectangle_x(&self, index: u32) -> f32 {
return match self.rectangle(index) {
Some(rectangle) => rectangle.rect().x(),
None => -1.0,
};
}
/// Returns one rectangle's normalized top coordinate or `-1.0` if absent.
pub fn rectangle_y(&self, index: u32) -> f32 {
return match self.rectangle(index) {
Some(rectangle) => rectangle.rect().y(),
None => -1.0,
};
}
/// Returns one rectangle's normalized width or `0.0` if absent.
pub fn rectangle_width(&self, index: u32) -> f32 {
return match self.rectangle(index) {
Some(rectangle) => rectangle.rect().width(),
None => 0.0,
};
}
/// Returns one rectangle's normalized height or `0.0` if absent.
pub fn rectangle_height(&self, index: u32) -> f32 {
return match self.rectangle(index) {
Some(rectangle) => rectangle.rect().height(),
None => 0.0,
};
}
/// Returns one rectangle's red channel or zero if absent.
pub fn rectangle_red(&self, index: u32) -> u8 {
return match self.rectangle(index) {
Some(rectangle) => rectangle.color().red(),
None => 0,
};
}
/// Returns one rectangle's green channel or zero if absent.
pub fn rectangle_green(&self, index: u32) -> u8 {
return match self.rectangle(index) {
Some(rectangle) => rectangle.color().green(),
None => 0,
};
}
/// Returns one rectangle's blue channel or zero if absent.
pub fn rectangle_blue(&self, index: u32) -> u8 {
return match self.rectangle(index) {
Some(rectangle) => rectangle.color().blue(),
None => 0,
};
}
}
impl Default for SnakeWasmGame {
fn default() -> Self {
return Self::new();
}
}
impl SnakeWasmGame {
fn queue_direction(&mut self, action: engine_v1_common::GameAction) {
self.pending_input = engine_v1_common::InputState::none().with_action(action, true);
return;
}
fn scene(&self) -> engine_v1_common::EngineScene {
return engine_v1_common::EngineGame::scene(&self.state);
}
fn rectangle(&self, requested_index: u32) -> Option<engine_v1_common::RenderRect> {
let scene = self.scene();
let mut occupied_index = 0_u32;
for slot in scene.rectangles() {
match slot {
Some(rectangle) => {
if occupied_index == requested_index {
return Some(*rectangle);
}
occupied_index = occupied_index.saturating_add(1);
},
None => {},
}
}
return None;
}
}
fn parse_platform_family(label: &str) -> Option<engine_v1_platform_api::PlatformFamily> {
return match label {
"android" => Some(engine_v1_platform_api::PlatformFamily::Android),
"desktop" => Some(engine_v1_platform_api::PlatformFamily::Desktop),
"web" => Some(engine_v1_platform_api::PlatformFamily::Web),
_ => None,
};
}
fn parse_runtime_host(label: &str) -> Option<engine_v1_platform_api::RuntimeHost> {
return match label {
"browser" => Some(engine_v1_platform_api::RuntimeHost::Browser),
"native" => Some(engine_v1_platform_api::RuntimeHost::Native),
"tauri-webview" => Some(engine_v1_platform_api::RuntimeHost::TauriWebView),
_ => None,
};
}
fn parse_device_class(label: &str) -> Option<engine_v1_platform_api::DeviceClass> {
return match label {
"desktop" => Some(engine_v1_platform_api::DeviceClass::Desktop),
"phone" => Some(engine_v1_platform_api::DeviceClass::Phone),
"tablet" => Some(engine_v1_platform_api::DeviceClass::Tablet),
"unknown" => Some(engine_v1_platform_api::DeviceClass::Unknown),
_ => None,
};
}
fn parse_input_profile(label: &str) -> Option<engine_v1_platform_api::InputProfile> {
return match label {
"gamepad" => Some(engine_v1_platform_api::InputProfile::Gamepad),
"keyboard-mouse" => Some(engine_v1_platform_api::InputProfile::KeyboardMouse),
"mixed" => Some(engine_v1_platform_api::InputProfile::Mixed),
"touch" => Some(engine_v1_platform_api::InputProfile::Touch),
"unknown" => Some(engine_v1_platform_api::InputProfile::Unknown),
_ => None,
};
}
fn device_class_label(value: engine_v1_platform_api::DeviceClass) -> &'static str {
return match value {
engine_v1_platform_api::DeviceClass::Desktop => "desktop",
engine_v1_platform_api::DeviceClass::Phone => "phone",
engine_v1_platform_api::DeviceClass::Tablet => "tablet",
engine_v1_platform_api::DeviceClass::Unknown => "unknown",
};
}
fn execution_model_label(value: engine_v1_platform_api::ExecutionModel) -> &'static str {
return match value {
engine_v1_platform_api::ExecutionModel::Native => "native",
engine_v1_platform_api::ExecutionModel::Wasm => "wasm",
};
}
fn input_profile_label(value: engine_v1_platform_api::InputProfile) -> &'static str {
return match value {
engine_v1_platform_api::InputProfile::Gamepad => "gamepad",
engine_v1_platform_api::InputProfile::KeyboardMouse => "keyboard-mouse",
engine_v1_platform_api::InputProfile::Mixed => "mixed",
engine_v1_platform_api::InputProfile::Touch => "touch",
engine_v1_platform_api::InputProfile::Unknown => "unknown",
};
}
fn platform_family_label(value: engine_v1_platform_api::PlatformFamily) -> &'static str {
return match value {
engine_v1_platform_api::PlatformFamily::Android => "android",
engine_v1_platform_api::PlatformFamily::Desktop => "desktop",
engine_v1_platform_api::PlatformFamily::Web => "web",
};
}
fn runtime_host_label(value: engine_v1_platform_api::RuntimeHost) -> &'static str {
return match value {
engine_v1_platform_api::RuntimeHost::Browser => "browser",
engine_v1_platform_api::RuntimeHost::Native => "native",
engine_v1_platform_api::RuntimeHost::TauriWebView => "tauri-webview",
};
}
#[cfg(test)]
#[path = "../unit_tests/runtime.rs"]
mod tests;