Wait and both kinds of until loops; blocks run directly
Script runs are resumable threads saved with the game; Rhai is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
95cf3ce81c
commit
d17668f3b2
13 files changed
+466
-686
No files matched your search
+353
-187
@@ -1,24 +1,23 @@
|
||||
// Mission scripts at run time: the block scripts of a mission (campaign_doc::MissionScripts)
|
||||
// become one Rhai program; the triggers are checked ten times a second inside the fixed
|
||||
// simulation step, so a co-op match runs them identically on every computer.
|
||||
//
|
||||
// The program never touches the game directly: reading functions (m.gold(), m.in_zone(..))
|
||||
// look at the game through a pointer that is valid while a script runs, and everything a
|
||||
// script does is collected as actions that are applied afterwards.
|
||||
// are run directly from their blocks inside the fixed simulation step, so a co-op match runs
|
||||
// them identically on every computer. Every run of a script is a small "thread" (where it is
|
||||
// in which list of blocks); "wait" parks a thread until its time has come, and threads are
|
||||
// saved with the game. Loops without a wait run up to a budget of steps per check and go on at
|
||||
// the next check, so a script can never hang the game.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use rhai::{Engine, Scope, AST};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::blocks::function_code;
|
||||
use crate::blocks::{Block, Place, Val};
|
||||
use crate::campaign_doc::{MissionScripts, TriggerKind, ZoneDoc};
|
||||
use crate::game::*;
|
||||
use crate::map::*;
|
||||
|
||||
const CHECK_STEPS: u64 = 5; // triggers are checked every 5 fixed steps (0.1 s)
|
||||
const MAX_LOG: usize = 60;
|
||||
const STEP_BUDGET: u32 = 2000; // blocks a thread may run per check before it yields
|
||||
const MAX_THREADS: usize = 64;
|
||||
|
||||
// pending events the simulation reports (the rest is found by comparing two checks)
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@@ -59,10 +58,40 @@ pub struct ScriptState {
|
||||
next_second: f32,
|
||||
rng: u32,
|
||||
pub error: Option<String>,
|
||||
#[serde(default)]
|
||||
threads: Vec<Thread>, // scripts that are waiting (or yielded in a long loop)
|
||||
#[serde(skip)]
|
||||
pub camera: Option<(f32, f32)>, // a block asked the camera to look here (taken by main)
|
||||
}
|
||||
|
||||
// which script a thread runs
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
|
||||
enum Src {
|
||||
Start,
|
||||
Victory,
|
||||
Defeat,
|
||||
Tick,
|
||||
Trigger(usize),
|
||||
}
|
||||
|
||||
// a list of blocks being worked through: its path (pairs of block index and branch below the
|
||||
// script's own list), the next block, and what kind of list it is
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct Frame {
|
||||
lp: Vec<usize>,
|
||||
idx: usize,
|
||||
left: i64, // repeat: runs still to come after this one (-1: not a repeat)
|
||||
loop_kind: u8, // 0 plain, 1 repeat, 2 repeat until (checked before), 3 repeat ... until (checked after)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct Thread {
|
||||
src: Src,
|
||||
frames: Vec<Frame>,
|
||||
wake: f32,
|
||||
ev: EventInfo,
|
||||
}
|
||||
|
||||
// what a running script asks for
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Act {
|
||||
@@ -83,44 +112,12 @@ enum Act {
|
||||
}
|
||||
|
||||
// the triggering unit (for "where the unit is" and "the unit is a ...")
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
struct EventInfo {
|
||||
kind: Option<EK>,
|
||||
pos: Option<(f32, f32)>,
|
||||
}
|
||||
|
||||
struct Ctx {
|
||||
game: *const Game,
|
||||
vars: BTreeMap<String, f64>,
|
||||
acts: Vec<Act>,
|
||||
event: EventInfo,
|
||||
rng: u32,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static CTX: RefCell<Ctx> = RefCell::new(Ctx { game: std::ptr::null(), vars: BTreeMap::new(), acts: Vec::new(), event: EventInfo::default(), rng: 1 });
|
||||
// the compiled program of the running mission (key: the program text)
|
||||
static PROGRAM: RefCell<Option<(String, Engine, AST)>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
// reads the game while a script runs (the pointer is set by run_fn around every call)
|
||||
fn with_game<R>(f: impl FnOnce(&Game) -> R, fallback: R) -> R {
|
||||
CTX.with(|c| {
|
||||
let p = c.borrow().game;
|
||||
if p.is_null() {
|
||||
fallback
|
||||
} else {
|
||||
// SAFETY: run_fn sets the pointer from a live &Game for the duration of the call
|
||||
// and clears it afterwards; scripts run on this thread only
|
||||
f(unsafe { &*p })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn act(a: Act) {
|
||||
CTX.with(|c| c.borrow_mut().acts.push(a));
|
||||
}
|
||||
|
||||
pub fn kind_by_name(name: &str) -> Option<EK> {
|
||||
let n = name.trim().to_lowercase();
|
||||
crate::blocks::KINDS.iter().position(|k| k.to_lowercase() == n).map(|i| match i {
|
||||
@@ -247,118 +244,41 @@ fn line_side(z: &ZoneDoc, x: f32, y: f32) -> i8 {
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ the program
|
||||
|
||||
// the Rhai program of a mission: one function per script and trigger
|
||||
pub fn program(s: &MissionScripts) -> String {
|
||||
let mut code = String::new();
|
||||
code.push_str(&function_code("s_start", &s.start));
|
||||
code.push_str(&function_code("s_victory", &s.victory));
|
||||
code.push_str(&function_code("s_defeat", &s.defeat));
|
||||
code.push_str(&function_code("s_tick", &s.tick));
|
||||
for (k, t) in s.triggers.iter().enumerate() {
|
||||
code.push_str(&function_code(&format!("t_{}", k), &t.script));
|
||||
}
|
||||
code
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct M;
|
||||
|
||||
fn engine() -> Engine {
|
||||
let mut e = Engine::new();
|
||||
e.set_max_operations(100_000);
|
||||
e.set_max_call_levels(16);
|
||||
e.register_type_with_name::<M>("mission");
|
||||
// reading
|
||||
e.register_fn("gold", |_: &mut M| with_game(|g| g.gold as f64, 0.0));
|
||||
e.register_fn("mana", |_: &mut M| with_game(|g| g.mana as f64, 0.0));
|
||||
e.register_fn("time", |_: &mut M| with_game(|g| g.time as f64, 0.0));
|
||||
e.register_fn("get", |_: &mut M, name: &str| CTX.with(|c| c.borrow().vars.get(name).copied().unwrap_or(0.0)));
|
||||
e.register_fn("set", |_: &mut M, name: &str, v: f64| {
|
||||
CTX.with(|c| {
|
||||
c.borrow_mut().vars.insert(name.to_string(), if v.is_finite() { v } else { 0.0 });
|
||||
})
|
||||
});
|
||||
e.register_fn("count", |_: &mut M, side: i64, kind: &str| {
|
||||
with_game(|g| g.ents.iter().filter(|e| e.alive && e.captive == 0 && !e.chained && matches(e.side(), e.kind, side as i32, kind)).count() as f64, 0.0)
|
||||
});
|
||||
e.register_fn("in_zone", |_: &mut M, zone: &str, side: i64, kind: &str| {
|
||||
with_game(
|
||||
|g| {
|
||||
let Some(z) = g.script.as_ref().and_then(|s| s.zones.iter().find(|z| z.name == zone)) else { return 0.0 };
|
||||
g.ents.iter().filter(|e| e.alive && !e.held && e.captive == 0 && matches(e.side(), e.kind, side as i32, kind) && zone_contains(z, e.x, e.y)).count() as f64
|
||||
},
|
||||
0.0,
|
||||
)
|
||||
});
|
||||
e.register_fn("random", |_: &mut M, a: f64, b: f64| {
|
||||
CTX.with(|c| {
|
||||
let mut c = c.borrow_mut();
|
||||
// xorshift: deterministic, part of the saved script state
|
||||
let mut x = c.rng.max(1);
|
||||
x ^= x << 13;
|
||||
x ^= x >> 17;
|
||||
x ^= x << 5;
|
||||
c.rng = x;
|
||||
let (lo, hi) = (a.min(b).round(), a.max(b).round());
|
||||
lo + (x as f64 % (hi - lo + 1.0)).floor()
|
||||
})
|
||||
});
|
||||
e.register_fn("div", |_: &mut M, a: f64, b: f64| if b == 0.0 { 0.0 } else { a / b });
|
||||
e.register_fn("times", |_: &mut M, n: f64| if n.is_finite() { (n.round() as i64).clamp(0, 100) } else { 0 });
|
||||
e.register_fn("event_is", |_: &mut M, kind: &str| CTX.with(|c| c.borrow().event.kind.is_some() && c.borrow().event.kind == kind_by_name(kind)));
|
||||
// doing
|
||||
e.register_fn("message", |_: &mut M, s: &str| act(Act::Message(s.to_string())));
|
||||
e.register_fn("objective", |_: &mut M, s: &str| act(Act::Objective(s.to_string())));
|
||||
e.register_fn("hint", |_: &mut M, s: &str| act(Act::Hint(s.to_string())));
|
||||
e.register_fn("spawn_unit", |_: &mut M, side: i64, kind: &str, count: f64, level: f64, place: &str, zone: &str, name: &str| {
|
||||
act(Act::Spawn { side: side as i32, kind: kind.to_string(), count: count.round() as i64, level: level.round() as i64, place: place.to_string(), zone: zone.to_string(), name: name.to_string() })
|
||||
});
|
||||
e.register_fn("wave", |_: &mut M, n: f64| act(Act::Wave(n.round() as i64)));
|
||||
e.register_fn("add_gold", |_: &mut M, n: f64| act(Act::Gold(n)));
|
||||
e.register_fn("add_mana", |_: &mut M, n: f64| act(Act::Mana(n)));
|
||||
e.register_fn("win", |_: &mut M| act(Act::Win));
|
||||
e.register_fn("lose", |_: &mut M| act(Act::Lose));
|
||||
e.register_fn("reveal", |_: &mut M, z: &str| act(Act::Reveal(z.to_string())));
|
||||
e.register_fn("camera", |_: &mut M, z: &str| act(Act::Camera(z.to_string())));
|
||||
e.register_fn("terrain", |_: &mut M, z: &str, k: &str| act(Act::Terrain(z.to_string(), k.to_string())));
|
||||
e.register_fn("kill", |_: &mut M, z: &str, side: i64| act(Act::Kill(z.to_string(), side as i32)));
|
||||
e.register_fn("trigger", |_: &mut M, name: &str, on: bool| act(Act::Trigger(name.to_string(), on)));
|
||||
e
|
||||
}
|
||||
|
||||
// runs one function of the mission's program; returns the actions or an error
|
||||
fn run_fn(g: &Game, code: &str, name: &str, vars: &mut BTreeMap<String, f64>, rng: &mut u32, event: EventInfo) -> Result<Vec<Act>, String> {
|
||||
PROGRAM.with(|p| {
|
||||
let mut p = p.borrow_mut();
|
||||
if p.as_ref().map_or(true, |(c, _, _)| c != code) {
|
||||
let e = engine();
|
||||
let ast = e.compile(code).map_err(|e| e.to_string())?;
|
||||
*p = Some((code.to_string(), e, ast));
|
||||
}
|
||||
let (_, e, ast) = p.as_ref().unwrap();
|
||||
CTX.with(|c| {
|
||||
let mut c = c.borrow_mut();
|
||||
c.game = g as *const Game;
|
||||
c.vars = std::mem::take(vars);
|
||||
c.acts.clear();
|
||||
c.event = event;
|
||||
c.rng = *rng;
|
||||
});
|
||||
let mut scope = Scope::new();
|
||||
let r = e.call_fn::<rhai::Dynamic>(&mut scope, ast, name, (M,));
|
||||
CTX.with(|c| {
|
||||
let mut c = c.borrow_mut();
|
||||
c.game = std::ptr::null();
|
||||
*vars = std::mem::take(&mut c.vars);
|
||||
*rng = c.rng;
|
||||
match r {
|
||||
Ok(_) => Ok(std::mem::take(&mut c.acts)),
|
||||
Err(err) => Err(err.to_string()),
|
||||
// the list of blocks at a path inside a script
|
||||
fn list_at<'a>(s: &'a MissionScripts, src: Src, lp: &[usize]) -> Option<&'a [Block]> {
|
||||
let mut cur: &[Block] = match src {
|
||||
Src::Start => &s.start,
|
||||
Src::Victory => &s.victory,
|
||||
Src::Defeat => &s.defeat,
|
||||
Src::Tick => &s.tick,
|
||||
Src::Trigger(k) => &s.triggers.get(k)?.script,
|
||||
};
|
||||
let mut k = 0;
|
||||
while k + 1 < lp.len() + 1 && k < lp.len() {
|
||||
let (i, branch) = (lp[k], *lp.get(k + 1)?);
|
||||
cur = match cur.get(i)? {
|
||||
Block::If { then, els, .. } => {
|
||||
if branch == 0 {
|
||||
then
|
||||
} else {
|
||||
els
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
Block::Repeat { body, .. } | Block::Until { body, .. } | Block::DoUntil { body, .. } => body,
|
||||
_ => return None,
|
||||
};
|
||||
k += 2;
|
||||
}
|
||||
Some(cur)
|
||||
}
|
||||
|
||||
fn xorshift(rng: &mut u32) -> u32 {
|
||||
let mut x = (*rng).max(1);
|
||||
x ^= x << 13;
|
||||
x ^= x >> 17;
|
||||
x ^= x << 5;
|
||||
*rng = x;
|
||||
x
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ the game side
|
||||
@@ -429,58 +349,240 @@ impl Game {
|
||||
return;
|
||||
}
|
||||
let mut s = self.script.take().unwrap();
|
||||
let code = program(&s.scripts);
|
||||
let mut fire: Vec<(String, EventInfo, Option<usize>)> = Vec::new(); // function, event, trigger
|
||||
let mut fire: Vec<(String, EventInfo, Option<usize>)> = Vec::new();
|
||||
let mut start: Vec<(Src, EventInfo)> = Vec::new();
|
||||
if !s.started {
|
||||
s.started = true;
|
||||
s.next_second = self.time + 1.0;
|
||||
self.remember(&mut s, true);
|
||||
fire.push(("s_start".into(), EventInfo::default(), None));
|
||||
start.push((Src::Start, EventInfo::default()));
|
||||
} else {
|
||||
self.check_triggers(&mut s, &mut fire);
|
||||
if self.time >= s.next_second {
|
||||
s.next_second += 1.0;
|
||||
for f in ["s_tick", "s_victory", "s_defeat"] {
|
||||
fire.push((f.into(), EventInfo::default(), None));
|
||||
// the checks run every second, but never twice at once (one may still be waiting)
|
||||
for src in [Src::Tick, Src::Victory, Src::Defeat] {
|
||||
if !s.threads.iter().any(|t| t.src == src) {
|
||||
start.push((src, EventInfo::default()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (f, ev, trig) in fire {
|
||||
if let Some(k) = trig {
|
||||
// a trigger fires: once-triggers only the first time, switched-off ones never
|
||||
if !s.enabled.get(k).copied().unwrap_or(false) || (s.scripts.triggers[k].once && s.done[k]) {
|
||||
continue;
|
||||
}
|
||||
s.done[k] = true;
|
||||
let name = s.scripts.triggers[k].name.clone();
|
||||
s.log(self.time, name);
|
||||
for (_, ev, trig) in fire {
|
||||
let Some(k) = trig else { continue };
|
||||
// a trigger fires: once-triggers only the first time, switched-off ones never
|
||||
if !s.enabled.get(k).copied().unwrap_or(false) || (s.scripts.triggers[k].once && s.done[k]) {
|
||||
continue;
|
||||
}
|
||||
let (mut vars, mut rng) = (std::mem::take(&mut s.vars), s.rng);
|
||||
let r = run_fn(self, &code, &f, &mut vars, &mut rng, ev.clone());
|
||||
s.vars = vars;
|
||||
s.rng = rng;
|
||||
match r {
|
||||
Ok(acts) => {
|
||||
for a in acts {
|
||||
self.apply_act(&mut s, a, &ev);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if s.error.is_none() {
|
||||
self.script_msg("msg.script_error", vec![e.chars().take(120).collect()]);
|
||||
}
|
||||
s.log(self.time, format!("! {}", e.chars().take(80).collect::<String>()));
|
||||
s.error = Some(e);
|
||||
}
|
||||
}
|
||||
if self.state != 0 {
|
||||
break;
|
||||
s.done[k] = true;
|
||||
let name = s.scripts.triggers[k].name.clone();
|
||||
s.log(self.time, name);
|
||||
start.push((Src::Trigger(k), ev));
|
||||
}
|
||||
let mut threads = std::mem::take(&mut s.threads);
|
||||
for (src, ev) in start {
|
||||
threads.push(Thread { src, frames: vec![Frame { lp: Vec::new(), idx: 0, left: -1, loop_kind: 0 }], wake: 0.0, ev });
|
||||
}
|
||||
// run every thread whose time has come; the waiting ones stay
|
||||
let scripts = std::mem::take(&mut s.scripts);
|
||||
let mut keep = Vec::new();
|
||||
for mut t in threads {
|
||||
if self.state == 0 && t.wake <= self.time && !self.run_thread(&mut s, &scripts, &mut t) {
|
||||
keep.push(t);
|
||||
} else if t.wake > self.time {
|
||||
keep.push(t);
|
||||
}
|
||||
}
|
||||
if keep.len() > MAX_THREADS {
|
||||
keep.drain(0..keep.len() - MAX_THREADS);
|
||||
}
|
||||
s.scripts = scripts;
|
||||
s.threads = keep;
|
||||
s.events.clear();
|
||||
self.script = Some(s);
|
||||
}
|
||||
|
||||
// runs a thread until it ends (true), waits or used up its steps (false)
|
||||
fn run_thread(&mut self, s: &mut ScriptState, scripts: &MissionScripts, t: &mut Thread) -> bool {
|
||||
let mut budget = STEP_BUDGET;
|
||||
loop {
|
||||
if self.state != 0 {
|
||||
return true;
|
||||
}
|
||||
if budget == 0 {
|
||||
t.wake = self.time; // go on at the next check
|
||||
return false;
|
||||
}
|
||||
budget -= 1;
|
||||
let Some(fr) = t.frames.last() else { return true };
|
||||
let Some(list) = list_at(scripts, t.src, &fr.lp) else { return true };
|
||||
let (idx, lp, left, loop_kind) = (fr.idx, fr.lp.clone(), fr.left, fr.loop_kind);
|
||||
if idx >= list.len() {
|
||||
// the end of a list: loop again, or back to the list around it
|
||||
let again = match loop_kind {
|
||||
1 => left > 0,
|
||||
2 | 3 => {
|
||||
// the loop's condition sits in its block in the list around it
|
||||
let outer = list_at(scripts, t.src, &lp[..lp.len() - 2]).and_then(|l| l.get(lp[lp.len() - 2]));
|
||||
match outer {
|
||||
Some(Block::Until { cond, .. }) | Some(Block::DoUntil { cond, .. }) => !self.cond(s, cond, &t.ev),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if again {
|
||||
let fr = t.frames.last_mut().unwrap();
|
||||
fr.idx = 0;
|
||||
if loop_kind == 1 {
|
||||
fr.left -= 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
t.frames.pop();
|
||||
match t.frames.last_mut() {
|
||||
Some(parent) => parent.idx += 1,
|
||||
None => return true,
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let inner = |branch: usize, left: i64, kind: u8| {
|
||||
let mut p = lp.clone();
|
||||
p.push(idx);
|
||||
p.push(branch);
|
||||
Frame { lp: p, idx: 0, left, loop_kind: kind }
|
||||
};
|
||||
match &list[idx] {
|
||||
Block::If { cond, .. } => {
|
||||
let c = self.cond(s, cond, &t.ev);
|
||||
t.frames.push(inner(if c { 0 } else { 1 }, -1, 0));
|
||||
}
|
||||
Block::Repeat { times, .. } => {
|
||||
let n = self.eval(s, times, &t.ev).round().clamp(0.0, 100.0) as i64;
|
||||
if n > 0 {
|
||||
t.frames.push(inner(0, n - 1, 1));
|
||||
} else {
|
||||
t.frames.last_mut().unwrap().idx += 1;
|
||||
}
|
||||
}
|
||||
Block::Until { cond, .. } => {
|
||||
// checked before each run: already true means the inside never runs
|
||||
if self.cond(s, cond, &t.ev) {
|
||||
t.frames.last_mut().unwrap().idx += 1;
|
||||
} else {
|
||||
t.frames.push(inner(0, -1, 2));
|
||||
}
|
||||
}
|
||||
Block::DoUntil { .. } => t.frames.push(inner(0, -1, 3)),
|
||||
Block::Wait { secs } => {
|
||||
let n = self.eval(s, secs, &t.ev).clamp(0.1, 36000.0) as f32;
|
||||
t.frames.last_mut().unwrap().idx += 1;
|
||||
t.wake = self.time + n;
|
||||
return false;
|
||||
}
|
||||
b => {
|
||||
let b = b.clone();
|
||||
t.frames.last_mut().unwrap().idx += 1;
|
||||
self.exec(s, &b, &t.ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a value
|
||||
fn eval(&self, s: &mut ScriptState, v: &Val, ev: &EventInfo) -> f64 {
|
||||
let r = match v {
|
||||
Val::Num { n } => *n,
|
||||
Val::Bool { b } => *b as i32 as f64,
|
||||
Val::Var { name } => s.vars.get(name).copied().unwrap_or(0.0),
|
||||
Val::Gold => self.gold as f64,
|
||||
Val::Mana => self.mana as f64,
|
||||
Val::Time => self.time as f64,
|
||||
Val::Creatures { side, kind } => self.ents.iter().filter(|e| e.alive && e.captive == 0 && !e.chained && matches(e.side(), e.kind, *side, kind)).count() as f64,
|
||||
Val::InZone { zone, side, kind } => match s.zone(zone) {
|
||||
Some(z) => self.ents.iter().filter(|e| e.alive && !e.held && e.captive == 0 && matches(e.side(), e.kind, *side, kind) && zone_contains(z, e.x, e.y)).count() as f64,
|
||||
None => 0.0,
|
||||
},
|
||||
Val::Random { a, b } => {
|
||||
let (a, b) = (self.eval(s, a, ev), self.eval(s, b, ev));
|
||||
let (lo, hi) = (a.min(b).round(), a.max(b).round());
|
||||
lo + (xorshift(&mut s.rng) as f64 % (hi - lo + 1.0)).floor()
|
||||
}
|
||||
Val::Math { op, a, b } => {
|
||||
let (a, b) = (self.eval(s, a, ev), self.eval(s, b, ev));
|
||||
match op.as_str() {
|
||||
"-" => a - b,
|
||||
"*" => a * b,
|
||||
"/" => {
|
||||
if b == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
a / b
|
||||
}
|
||||
}
|
||||
_ => a + b,
|
||||
}
|
||||
}
|
||||
Val::Cmp { op, a, b } => {
|
||||
let (a, b) = (self.eval(s, a, ev), self.eval(s, b, ev));
|
||||
cmp(op, a, b) as i32 as f64
|
||||
}
|
||||
Val::And { a, b } => (self.cond(s, a, ev) && self.cond(s, b, ev)) as i32 as f64,
|
||||
Val::Or { a, b } => (self.cond(s, a, ev) || self.cond(s, b, ev)) as i32 as f64,
|
||||
Val::Not { a } => (!self.cond(s, a, ev)) as i32 as f64,
|
||||
Val::EventKind { kind } => (ev.kind.is_some() && ev.kind == kind_by_name(kind)) as i32 as f64,
|
||||
};
|
||||
if r.is_finite() {
|
||||
r
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
fn cond(&self, s: &mut ScriptState, v: &Val, ev: &EventInfo) -> bool {
|
||||
self.eval(s, v, ev) != 0.0
|
||||
}
|
||||
|
||||
// one block that does something
|
||||
fn exec(&mut self, s: &mut ScriptState, b: &Block, ev: &EventInfo) {
|
||||
let act = match b {
|
||||
Block::Message { text } => Act::Message(text.clone()),
|
||||
Block::Objective { text } => Act::Objective(text.clone()),
|
||||
Block::Hint { text } => Act::Hint(text.clone()),
|
||||
Block::Spawn { side, kind, count, level, place, name } => {
|
||||
let (pk, pz) = match place {
|
||||
Place::Zone(z) => ("zone", z.clone()),
|
||||
Place::Event => ("event", String::new()),
|
||||
Place::Core => ("core", String::new()),
|
||||
};
|
||||
Act::Spawn { side: *side, kind: kind.clone(), count: self.eval(s, count, ev).round() as i64, level: self.eval(s, level, ev).round() as i64, place: pk.into(), zone: pz, name: name.clone() }
|
||||
}
|
||||
Block::Wave { count } => Act::Wave(self.eval(s, count, ev).round() as i64),
|
||||
Block::Gold { amount } => Act::Gold(self.eval(s, amount, ev)),
|
||||
Block::Mana { amount } => Act::Mana(self.eval(s, amount, ev)),
|
||||
Block::SetVar { name, value } => {
|
||||
let v = self.eval(s, value, ev);
|
||||
s.vars.insert(name.clone(), v);
|
||||
return;
|
||||
}
|
||||
Block::AddVar { name, value } => {
|
||||
let v = s.vars.get(name).copied().unwrap_or(0.0) + self.eval(s, value, ev);
|
||||
s.vars.insert(name.clone(), v);
|
||||
return;
|
||||
}
|
||||
Block::Win => Act::Win,
|
||||
Block::Lose => Act::Lose,
|
||||
Block::Reveal { zone } => Act::Reveal(zone.clone()),
|
||||
Block::Camera { zone } => Act::Camera(zone.clone()),
|
||||
Block::Terrain { zone, kind } => Act::Terrain(zone.clone(), kind.clone()),
|
||||
Block::Kill { zone, side } => Act::Kill(zone.clone(), *side),
|
||||
Block::Trigger { name, on } => Act::Trigger(name.clone(), *on),
|
||||
// the flow blocks are handled by run_thread
|
||||
Block::If { .. } | Block::Repeat { .. } | Block::Until { .. } | Block::DoUntil { .. } | Block::Wait { .. } => return,
|
||||
};
|
||||
self.apply_act(s, act, ev);
|
||||
}
|
||||
|
||||
// what the next check compares against: living units, towers, rooms, research, zone counts
|
||||
fn remember(&self, s: &mut ScriptState, first: bool) {
|
||||
s.alive = self.ents.iter().filter(|e| e.alive && e.captive == 0).map(|e| Seen { id: e.id, kind: e.kind, side: e.side(), x: e.x, y: e.y }).collect();
|
||||
@@ -903,6 +1005,70 @@ mod tests {
|
||||
assert_eq!(v.get("rich"), Some(&1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn waits_and_both_kinds_of_until_loops() {
|
||||
let mut g = Game::new(3, 64, 64);
|
||||
let cmp = |op: &str, a: Val, b: f64| Val::Cmp { op: op.into(), a: Box::new(a), b: Box::new(Val::num(b)) };
|
||||
let var = |n: &str| Val::Var { name: n.into() };
|
||||
let scripts = MissionScripts {
|
||||
start: vec![
|
||||
Block::SetVar { name: "a".into(), value: Val::num(0.0) },
|
||||
Block::SetVar { name: "pre".into(), value: Val::num(0.0) },
|
||||
Block::SetVar { name: "post".into(), value: Val::num(0.0) },
|
||||
// repeat until (checked before): already true, so the inside never runs
|
||||
Block::Until { cond: cmp(">=", var("a"), 0.0), body: vec![Block::AddVar { name: "pre".into(), value: Val::num(1.0) }] },
|
||||
// repeat ... until (checked after): runs once even though it is already true
|
||||
Block::DoUntil { cond: cmp(">=", var("a"), 0.0), body: vec![Block::AddVar { name: "post".into(), value: Val::num(1.0) }] },
|
||||
// count to three, one step every second
|
||||
Block::Until {
|
||||
cond: cmp(">=", var("a"), 3.0),
|
||||
body: vec![Block::Wait { secs: Val::num(1.0) }, Block::AddVar { name: "a".into(), value: Val::num(1.0) }],
|
||||
},
|
||||
Block::SetVar { name: "done".into(), value: Val::num(1.0) },
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
g.script = Some(ScriptState::new(scripts, vec![], 7));
|
||||
run(&mut g, 0.3);
|
||||
let v = |g: &Game, k: &str| g.script.as_ref().unwrap().vars.get(k).copied();
|
||||
assert_eq!(v(&g, "pre"), Some(0.0), "repeat until checks first");
|
||||
assert_eq!(v(&g, "post"), Some(1.0), "repeat ... until runs once");
|
||||
assert_eq!(v(&g, "a"), Some(0.0), "waiting");
|
||||
assert_eq!(v(&g, "done"), None);
|
||||
run(&mut g, 1.5);
|
||||
assert_eq!(v(&g, "a"), Some(1.0), "one step after a second");
|
||||
run(&mut g, 2.2);
|
||||
assert_eq!(v(&g, "a"), Some(3.0));
|
||||
assert_eq!(v(&g, "done"), Some(1.0), "the script went on after the loop");
|
||||
// a waiting script survives saving and loading
|
||||
let mut g2 = Game::new(3, 64, 64);
|
||||
g2.script = Some(ScriptState::new(
|
||||
MissionScripts { start: vec![Block::Wait { secs: Val::num(2.0) }, Block::SetVar { name: "late".into(), value: Val::num(1.0) }], ..Default::default() },
|
||||
vec![],
|
||||
1,
|
||||
));
|
||||
run(&mut g2, 0.5);
|
||||
let json = serde_json::to_string(&g2).unwrap();
|
||||
let mut g3: Game = serde_json::from_str(&json).unwrap();
|
||||
g3.map.activate();
|
||||
run(&mut g3, 2.0);
|
||||
assert_eq!(g3.script.as_ref().unwrap().vars.get("late"), Some(&1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_endless_loop_does_not_hang_the_game() {
|
||||
let mut g = Game::new(3, 64, 64);
|
||||
let scripts = MissionScripts {
|
||||
start: vec![Block::Until { cond: Val::Bool { b: false }, body: vec![Block::AddVar { name: "n".into(), value: Val::num(1.0) }] }],
|
||||
..Default::default()
|
||||
};
|
||||
g.script = Some(ScriptState::new(scripts, vec![], 7));
|
||||
run(&mut g, 1.0);
|
||||
let n = g.script.as_ref().unwrap().vars["n"];
|
||||
assert!(n > 100.0, "the loop runs: {}", n);
|
||||
assert!(g.time > 0.9, "and the game goes on");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zone_shapes() {
|
||||
let r = rect("r", (2.0, 2.0), (4.0, 3.0));
|
||||
|
||||
Reference in new issue
Block a user