31 Commits

Author SHA1 Message Date
8d93fc528e print input 2026-03-27 10:30:51 -07:00
f406f126ee adam optimizer 2026-03-27 10:24:52 -07:00
ec73f62f89 drastically reduce hidden layers 2026-03-27 10:23:25 -07:00
25bee24e4c graphics bodge 2 2026-03-27 10:07:01 -07:00
93c01910cb include world offset 2026-03-27 09:54:15 -07:00
293daf3ab2 fix inputs 2026-03-26 13:23:39 -07:00
8283759d47 tweak stride 2026-03-26 13:13:03 -07:00
d31868f033 swap angles xy 2026-03-26 13:09:45 -07:00
da20ad0464 partial_cmp 2026-03-26 13:09:38 -07:00
8ac19cb5fe print whats going on 2026-03-26 12:17:37 -07:00
6a4c222c90 comments 2026-03-26 11:57:50 -07:00
989bc37dc4 allow configure gpu 2026-03-26 11:44:24 -07:00
9e441c1d95 print number of parameters 2026-03-26 11:36:44 -07:00
be265fae24 use bodged version 2026-03-26 11:29:26 -07:00
07c9535cb1 ignore test files 2026-03-26 11:19:37 -07:00
4c551ed1a8 comments 2026-03-26 11:18:28 -07:00
46a4f25e55 remove 1 hidden layer 2026-03-26 11:05:39 -07:00
5724cabba1 remove sigmoid 2026-03-26 11:05:39 -07:00
02cfd6c052 fix stupid 2026-03-26 10:59:02 -07:00
4d3811a590 fix shape 2026-03-26 10:58:08 -07:00
4f51665372 fix 2026-03-26 10:55:07 -07:00
669b6f7614 create gradually decreasing layers 2026-03-26 10:53:47 -07:00
dd26c463f0 fix input size 2026-03-26 10:44:56 -07:00
6775f647cc print once 2026-03-26 10:41:22 -07:00
1cb5c01bb0 copy inside scope 2026-03-26 10:37:05 -07:00
ba406eb2dd tweaks 2026-03-26 10:36:02 -07:00
1f0475ef46 t 2026-03-26 10:28:33 -07:00
dc72a8d5e3 klfawmklklmwe 2026-03-26 10:20:31 -07:00
3ce6ad84a3 write things 2026-03-26 10:11:18 -07:00
48eefba747 up 2026-03-26 09:40:07 -07:00
842949b6d6 wip generate frames 2026-03-26 09:22:57 -07:00
3 changed files with 153 additions and 414 deletions

27
Cargo.lock generated
View File

@@ -1080,15 +1080,6 @@ dependencies = [
"rand_core 0.10.0",
]
[[package]]
name = "chrono"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"num-traits",
]
[[package]]
name = "cipher"
version = "0.4.4"
@@ -1118,7 +1109,7 @@ checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
dependencies = [
"serde",
"termcolor",
"unicode-width 0.1.14",
"unicode-width 0.2.0",
]
[[package]]
@@ -1129,7 +1120,7 @@ checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
dependencies = [
"serde",
"termcolor",
"unicode-width 0.1.14",
"unicode-width 0.2.0",
]
[[package]]
@@ -1144,7 +1135,7 @@ version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -2241,7 +2232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -2928,7 +2919,7 @@ dependencies = [
"log",
"presser",
"thiserror 2.0.18",
"windows 0.58.0",
"windows 0.61.3",
]
[[package]]
@@ -5063,7 +5054,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -5460,8 +5451,6 @@ name = "strafe-ai"
version = "0.1.0"
dependencies = [
"burn",
"chrono",
"glam",
"pollster",
"strafesnet_common",
"strafesnet_graphics",
@@ -5740,7 +5729,7 @@ dependencies = [
"getrandom 0.4.2",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -6999,7 +6988,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]

View File

@@ -5,8 +5,6 @@ edition = "2024"
[dependencies]
burn = { version = "0.20.1", features = ["cuda", "autodiff"] }
glam = "0.32.1"
pollster = "0.4.0"
wgpu = "29.0.0"
strafesnet_common = { version = "0.9.0", registry = "strafesnet" }
@@ -15,4 +13,4 @@ strafesnet_physics = { version = "=0.0.2-surf", registry = "strafesnet" }
strafesnet_roblox_bot_file = { version = "0.9.4", registry = "strafesnet" }
strafesnet_roblox_bot_player = { version = "=0.6.2-depth2", registry = "strafesnet" }
strafesnet_snf = { version = "0.4.0", registry = "strafesnet" }
chrono = { version = "0.4.44", default-features = false, features = ["now"] }
pollster = "0.4.0"

View File

@@ -1,21 +1,23 @@
use burn::backend::Autodiff;
use burn::nn::loss::{MseLoss, Reduction};
use burn::nn::{Dropout, DropoutConfig, Linear, LinearConfig, Relu};
use burn::optim::{AdamConfig, GradientsParams, Optimizer};
use burn::nn::{Linear, LinearConfig, Relu};
use burn::optim::{GradientsParams, Optimizer, AdamConfig};
use burn::prelude::*;
type InferenceBackend = burn::backend::Cuda<f32>;
type TrainingBackend = Autodiff<InferenceBackend>;
const LIMITS: wgpu::Limits = wgpu::Limits::defaults();
const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
use strafesnet_graphics::setup;
use strafesnet_roblox_bot_file::v0;
const SIZE: glam::UVec2 = glam::uvec2(64, 36);
const POSITION_HISTORY: usize = 4;
const INPUT: usize = (SIZE.x * SIZE.y) as usize + POSITION_HISTORY * 3;
const HIDDEN: [usize; 2] = [INPUT >> 3, INPUT >> 7];
const SIZE_X: usize = 64;
const SIZE_Y: usize = 36;
const INPUT: usize = SIZE_X * SIZE_Y;
const HIDDEN: [usize; 2] = [
INPUT >> 3,
INPUT >> 7,
];
// MoveForward
// MoveLeft
// MoveBack
@@ -25,13 +27,9 @@ const HIDDEN: [usize; 2] = [INPUT >> 3, INPUT >> 7];
// mouse_dy
const OUTPUT: usize = 7;
// bytes_per_row needs to be a multiple of 256.
const STRIDE_SIZE: u32 = (SIZE.x * size_of::<f32>() as u32).next_multiple_of(256);
#[derive(Module, Debug)]
struct Net<B: Backend> {
input: Linear<B>,
dropout: Dropout,
hidden: [Linear<B>; HIDDEN.len() - 1],
output: Linear<B>,
activation: Relu,
@@ -48,10 +46,8 @@ impl<B: Backend> Net<B> {
layer
});
let output = LinearConfig::new(last_size, OUTPUT).init(device);
let dropout = DropoutConfig::new(0.1).init();
Self {
input,
dropout,
hidden,
output,
activation: Relu::new(),
@@ -59,7 +55,6 @@ impl<B: Backend> Net<B> {
}
fn forward(&self, input: Tensor<B, 2>) -> Tensor<B, 2> {
let x = self.input.forward(input);
let x = self.dropout.forward(x);
let mut x = self.activation.forward(x);
for layer in &self.hidden {
x = layer.forward(x);
@@ -69,164 +64,6 @@ impl<B: Backend> Net<B> {
}
}
struct GraphicsState {
device: wgpu::Device,
queue: wgpu::Queue,
graphics: strafesnet_roblox_bot_player::graphics::Graphics,
graphics_texture_view: wgpu::TextureView,
output_staging_buffer: wgpu::Buffer,
texture_data: Vec<u8>,
position_history: Vec<glam::Vec3>,
}
impl GraphicsState {
fn new(map: &strafesnet_common::map::CompleteMap) -> Self {
let desc = wgpu::InstanceDescriptor::new_without_display_handle_from_env();
let instance = wgpu::Instance::new(desc);
let (device, queue) = pollster::block_on(async {
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
})
.await
.unwrap();
setup::step4::request_device(&adapter, LIMITS)
.await
.unwrap()
});
let mut graphics = strafesnet_roblox_bot_player::graphics::Graphics::new(
&device, &queue, SIZE, FORMAT, LIMITS,
);
graphics.change_map(&device, &queue, map).unwrap();
let graphics_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("RGB texture"),
format: FORMAT,
size: wgpu::Extent3d {
width: SIZE.x,
height: SIZE.y,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let graphics_texture_view = graphics_texture.create_view(&wgpu::TextureViewDescriptor {
label: Some("RGB texture view"),
aspect: wgpu::TextureAspect::All,
usage: Some(
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
),
..Default::default()
});
let texture_data = Vec::<u8>::with_capacity((STRIDE_SIZE * SIZE.y) as usize);
let output_staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Output staging buffer"),
size: texture_data.capacity() as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let position_history = Vec::with_capacity(POSITION_HISTORY);
Self {
device,
queue,
graphics,
graphics_texture_view,
output_staging_buffer,
texture_data,
position_history,
}
}
fn generate_inputs(&mut self, pos: glam::Vec3, angles: glam::Vec2, inputs: &mut Vec<f32>) {
// write position history to model inputs
if !self.position_history.is_empty() {
let camera = strafesnet_graphics::graphics::view_inv(pos, angles).inverse();
for &pos in self.position_history.iter().rev() {
let relative_pos = camera.transform_vector3(pos);
inputs.extend_from_slice(&relative_pos.to_array());
}
}
// fill remaining history with zeroes
for _ in self.position_history.len()..POSITION_HISTORY {
inputs.extend_from_slice(&[0.0, 0.0, 0.0]);
}
// track position history
if self.position_history.len() < POSITION_HISTORY {
self.position_history.push(pos);
} else {
self.position_history.rotate_left(1);
*self.position_history.last_mut().unwrap() = pos;
}
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("wgpu encoder"),
});
// render!
self.graphics
.encode_commands(&mut encoder, &self.graphics_texture_view, pos, angles);
// copy the depth texture into ram
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: self.graphics.depth_texture(),
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &self.output_staging_buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
// This needs to be a multiple of 256.
bytes_per_row: Some(STRIDE_SIZE),
rows_per_image: Some(SIZE.y),
},
},
wgpu::Extent3d {
width: SIZE.x,
height: SIZE.y,
depth_or_array_layers: 1,
},
);
self.queue.submit([encoder.finish()]);
// map buffer
let buffer_slice = self.output_staging_buffer.slice(..);
let (sender, receiver) = std::sync::mpsc::channel();
buffer_slice.map_async(wgpu::MapMode::Read, move |r| sender.send(r).unwrap());
self.device
.poll(wgpu::PollType::wait_indefinitely())
.unwrap();
receiver.recv().unwrap().unwrap();
// copy texture inside a scope so the mapped view gets dropped
{
let view = buffer_slice.get_mapped_range();
self.texture_data.extend_from_slice(&view[..]);
}
self.output_staging_buffer.unmap();
// discombolulate stride
for y in 0..SIZE.y {
inputs.extend(
self.texture_data[(STRIDE_SIZE * y) as usize
..(STRIDE_SIZE * y + SIZE.x * size_of::<f32>() as u32) as usize]
.chunks_exact(4)
.map(|b| 1.0 - 2.0 * f32::from_le_bytes(b.try_into().unwrap())),
)
}
self.texture_data.clear();
}
}
fn training() {
let gpu_id: usize = std::env::args()
.skip(1)
@@ -251,16 +88,68 @@ fn training() {
let world_offset = bot.world_offset();
let timelines = bot.timelines();
// setup graphics
let desc = wgpu::InstanceDescriptor::new_without_display_handle_from_env();
let instance = wgpu::Instance::new(desc);
let (device, queue) = pollster::block_on(async {
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
})
.await
.unwrap();
setup::step4::request_device(&adapter, LIMITS)
.await
.unwrap()
});
const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
let size = [SIZE_X as u32, SIZE_Y as u32].into();
let mut graphics = strafesnet_roblox_bot_player::graphics::Graphics::new(
&device, &queue, size, FORMAT, LIMITS,
);
graphics.change_map(&device, &queue, &map).unwrap();
// setup simulation
// run progressively longer segments of the map, starting very close to the end of the run and working the starting time backwards until the ai can run the whole map
// set up graphics
let mut g = GraphicsState::new(&map);
// set up textures
let graphics_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("RGB texture"),
format: FORMAT,
size: wgpu::Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let graphics_texture_view = graphics_texture.create_view(&wgpu::TextureViewDescriptor {
label: Some("RGB texture view"),
aspect: wgpu::TextureAspect::All,
usage: Some(wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING),
..Default::default()
});
// bytes_per_row needs to be a multiple of 256.
let stride_size = (size.x * size_of::<f32>() as u32).next_multiple_of(256);
let mut texture_data = Vec::<u8>::with_capacity((stride_size * size.y) as usize);
let output_staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Output staging buffer"),
size: texture_data.capacity() as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
// training data
let training_samples = timelines.input_events.len() - 1;
let input_size = INPUT * size_of::<f32>();
let input_size = (size.x * size.y) as usize * size_of::<f32>();
let mut inputs = Vec::with_capacity(input_size * training_samples);
let mut targets = Vec::with_capacity(OUTPUT * training_samples);
@@ -338,17 +227,82 @@ fn training() {
.unwrap(),
};
fn vec3(v: v0::Vector3) -> glam::Vec3 {
glam::vec3(v.x, v.y, v.z)
fn p(v: v0::Vector3) -> [f32; 3] {
[v.x, v.y, v.z]
}
fn angles(a: v0::Vector3) -> glam::Vec2 {
glam::vec2(a.y, a.x)
fn a(a: v0::Vector3) -> [f32; 2] {
[a.y, a.x]
}
fn sub<T: core::ops::Sub>(lhs: T, rhs: T) -> T::Output {
lhs - rhs
}
let pos = vec3(output_event.event.position) - world_offset;
let angles = angles(output_event.event.angles);
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("wgpu encoder"),
});
g.generate_inputs(pos, angles, &mut inputs);
// render!
graphics.encode_commands(
&mut encoder,
&graphics_texture_view,
sub(p(output_event.event.position).into(), world_offset),
a(output_event.event.angles).into(),
);
// copy the depth texture into ram
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: graphics.depth_texture(),
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &output_staging_buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
// This needs to be a multiple of 256.
bytes_per_row: Some(stride_size as u32),
rows_per_image: Some(size.y),
},
},
wgpu::Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
},
);
queue.submit([encoder.finish()]);
// map buffer
let buffer_slice = output_staging_buffer.slice(..);
let (sender, receiver) = std::sync::mpsc::channel();
buffer_slice.map_async(wgpu::MapMode::Read, move |r| sender.send(r).unwrap());
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
receiver.recv().unwrap().unwrap();
// copy texture inside a scope so the mapped view gets dropped
{
let view = buffer_slice.get_mapped_range();
texture_data.extend_from_slice(&view[..]);
}
output_staging_buffer.unmap();
let inputs_start = inputs.len();
// discombolulate stride
for y in 0..size.y {
inputs.extend(
texture_data[(stride_size * y) as usize
..(stride_size * y + size.x * size_of::<f32>() as u32) as usize]
.chunks_exact(4)
.map(|b| f32::from_le_bytes(b.try_into().unwrap())),
)
}
let inputs_end = inputs.len();
println!("inputs = {:?}", &inputs[inputs_start..inputs_end]);
texture_data.clear();
}
let device = burn::backend::cuda::CudaDevice::new(gpu_id);
@@ -359,7 +313,10 @@ fn training() {
let mut optim = AdamConfig::new().init();
let inputs = Tensor::from_data(
TensorData::new(inputs, Shape::new([training_samples, INPUT])),
TensorData::new(
inputs,
Shape::new([training_samples, (size.x * size.y) as usize]),
),
&device,
);
let targets = Tensor::from_data(
@@ -367,11 +324,8 @@ fn training() {
&device,
);
const LEARNING_RATE: f64 = 0.001;
const EPOCHS: usize = 100000;
let mut best_model = model.clone();
let mut best_loss = f32::INFINITY;
const LEARNING_RATE: f64 = 0.5;
const EPOCHS: usize = 10000;
for epoch in 0..EPOCHS {
let predictions = model.forward(inputs.clone());
@@ -388,12 +342,6 @@ fn training() {
let grads = loss.backward();
let grads = GradientsParams::from_grads(grads, &model);
// get the best model
if loss_scalar < best_loss {
best_loss = loss_scalar;
best_model = model.clone();
}
model = optim.step(LEARNING_RATE, model, grads);
if epoch % (EPOCHS >> 4) == 0 || epoch == EPOCHS - 1 {
@@ -401,211 +349,15 @@ fn training() {
println!(" epoch {:>5} | loss = {:.8}", epoch, loss_scalar);
}
}
let date_string = format!("{}_{}.model", chrono::Utc::now(), best_loss);
best_model
.save_file(
date_string,
&burn::record::BinFileRecorder::<burn::record::FullPrecisionSettings>::new(),
)
.unwrap();
}
use strafesnet_common::instruction::TimedInstruction;
use strafesnet_common::mouse::MouseState;
use strafesnet_common::physics::{
Instruction as PhysicsInputInstruction, MiscInstruction, ModeInstruction, MouseInstruction,
SetControlInstruction, Time as PhysicsTime,
};
use strafesnet_physics::physics::{PhysicsContext, PhysicsData, PhysicsState};
pub struct Recording {
instructions: Vec<TimedInstruction<PhysicsInputInstruction, PhysicsTime>>,
}
struct FrameState {
trajectory: strafesnet_physics::physics::Trajectory,
camera: strafesnet_physics::physics::PhysicsCamera,
}
impl FrameState {
fn pos(&self, time: PhysicsTime) -> glam::Vec3 {
self.trajectory
.extrapolated_position(time)
.map(Into::<f32>::into)
.to_array()
.into()
}
fn angles(&self) -> glam::Vec2 {
self.camera.simulate_move_angles(glam::IVec2::ZERO)
}
}
struct Session {
geometry_shared: PhysicsData,
simulation: PhysicsState,
recording: Recording,
}
impl Session {
fn get_frame_state(&self) -> FrameState {
FrameState {
trajectory: self.simulation.camera_trajectory(&self.geometry_shared),
camera: self.simulation.camera(),
}
}
fn run(&mut self, time: PhysicsTime, instruction: PhysicsInputInstruction) {
let instruction = TimedInstruction { time, instruction };
self.recording.instructions.push(instruction.clone());
PhysicsContext::run_input_instruction(
&mut self.simulation,
&self.geometry_shared,
instruction,
);
}
}
fn inference() {
let mut args = std::env::args().skip(1);
// pick device
let gpu_id: usize = args
.next()
.map(|id| id.parse().unwrap())
.unwrap_or_default();
let device = burn::backend::cuda::CudaDevice::new(gpu_id);
// load model
let path: std::path::PathBuf = args.next().unwrap().parse().unwrap();
let mut model: Net<InferenceBackend> = Net::init(&device);
model = model
.load_file(
path,
&burn::record::BinFileRecorder::<burn::record::FullPrecisionSettings>::new(),
&device,
)
.unwrap();
// load map
let map_file = include_bytes!("../files/bhop_marble_5692093612.snfm");
let map = strafesnet_snf::read_map(std::io::Cursor::new(map_file))
.unwrap()
.into_complete_map()
.unwrap();
let modes = map.modes.clone().denormalize();
let mode = modes
.get_mode(strafesnet_common::gameplay_modes::ModeId::MAIN)
.unwrap();
let start_zone = map.models.get(mode.get_start().get() as usize).unwrap();
let start_offset = glam::Vec3::from_array(
start_zone
.transform
.translation
.map(|f| f.into())
.to_array(),
);
// setup graphics
let mut g = GraphicsState::new(&map);
// setup simulation
let mut session = Session {
geometry_shared: PhysicsData::new(&map),
simulation: PhysicsState::default(),
recording: Recording {
instructions: Vec::new(),
},
};
let mut time = PhysicsTime::ZERO;
// reset to start zone
session.run(time, PhysicsInputInstruction::Mode(ModeInstruction::Reset));
// session.run(
// time,
// PhysicsInputInstruction::Misc(MiscInstruction::SetSensitivity(?)),
// );
session.run(
time,
PhysicsInputInstruction::Mode(ModeInstruction::Restart(
strafesnet_common::gameplay_modes::ModeId::MAIN,
)),
);
// TEMP: turn mouse left
let mut mouse_pos = glam::ivec2(-5300, 0);
const STEP: PhysicsTime = PhysicsTime::from_millis(10);
let mut input_floats = Vec::new();
// setup agent-simulation feedback loop
for _ in 0..20 * 100 {
// generate inputs
let frame_state = session.get_frame_state();
g.generate_inputs(
frame_state.pos(time) - start_offset,
frame_state.angles(),
&mut input_floats,
);
// inference
let inputs = Tensor::from_data(
TensorData::new(input_floats.clone(), Shape::new([1, INPUT])),
&device,
);
let outputs = model.forward(inputs).into_data().into_vec::<f32>().unwrap();
let &[
move_forward,
move_left,
move_back,
move_right,
jump,
mouse_dx,
mouse_dy,
] = outputs.as_slice()
else {
panic!()
};
macro_rules! set_control {
($control:ident,$output:expr) => {
session.run(
time,
PhysicsInputInstruction::SetControl(SetControlInstruction::$control(
0.5 < $output,
)),
);
};
}
set_control!(SetMoveForward, move_forward);
set_control!(SetMoveLeft, move_left);
set_control!(SetMoveBack, move_back);
set_control!(SetMoveRight, move_right);
set_control!(SetJump, jump);
mouse_pos += glam::vec2(mouse_dx, mouse_dy).round().as_ivec2();
let next_time = time + STEP;
session.run(
time,
PhysicsInputInstruction::Mouse(MouseInstruction::SetNextMouse(MouseState {
pos: mouse_pos,
time: next_time,
})),
);
time = next_time;
// clear
input_floats.clear();
}
let date_string = format!("{}.snfb", chrono::Utc::now());
let file = std::fs::File::create(date_string).unwrap();
strafesnet_snf::bot::write_bot(
std::io::BufWriter::new(file),
strafesnet_physics::VERSION.get(),
core::mem::take(&mut session.recording.instructions),
)
.unwrap();
// go!
}
fn main() {
// training();
inference();
training();
}