scroll.server/crates/game-service/tests/deploy_birth_tick.rs
WiseDev a585b0c18d resolve the deploy birth tick and stop wedging the client's own commands
- a card lands one tick after its command fires, since the client
  hashes after LogicTime::increaseTick, not before
- answer RequestSectorState immediately except in the two ticks after
  a firing tick, where a full update would teleport the client past
  the command it just queued - the old blanket refusal left every
  card the player tapped silently deleted while it held
- carry both pending commands and queued deploys in the same delivery
  ledger so the quiet window covers what the player actually paid for
- add the regression tests for the birth tick and the deferred answer
2026-08-28 16:27:16 +03:00

148 lines
4.7 KiB
Rust

use game_service::battle::BattleBuilder;
use game_service::battle_session::{BattleSession, COMMAND_DELAY_TICKS, DEPLOY_BIRTH_TICKS};
use logic::battle::LogicVector2;
use logic::data::{table, LogicDataRef, LogicDataTables};
use logic::model::LogicClientAvatar;
use std::path::Path;
use std::sync::Arc;
use titan::LogicLong;
fn assets() -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets")
}
fn avatar(low: i32) -> LogicClientAvatar {
let account = LogicLong::new(0, low);
LogicClientAvatar {
avatar_id: account,
account_id: account,
home_id: account,
name_change_state: -1,
..LogicClientAvatar::default()
}
}
#[test]
fn a_card_lands_one_tick_after_the_command_fires() {
let root = assets();
let tables = LogicDataTables::load_from_dir(&root).expect("tables");
LogicDataTables::install(Arc::new(tables));
let builder = BattleBuilder::new(&root);
let mode = builder
.build(
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
LogicDataRef::None,
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
vec![avatar(5), avatar(0)],
[None, None],
1,
)
.expect("battle");
let mut session = BattleSession::new(mode, Vec::new());
let at_tick = 0;
let spell = LogicDataRef::spell("Knight");
let entries = builder.summon(&spell, LogicVector2::new(14500, 23500), 0, 0, 100);
assert!(!entries.is_empty(), "the knight spell summons nothing");
let before = session.object_count();
let born = session.queue(at_tick, LogicVector2::new(14500, 23500), 0, 3, 0, entries);
assert_eq!(
born,
at_tick + DEPLOY_BIRTH_TICKS,
"the birth tick must be one past the firing tick"
);
assert_eq!(
DEPLOY_BIRTH_TICKS,
COMMAND_DELAY_TICKS + 1,
"the command still fires at age twenty; only the label the troops are first hashed under moves"
);
session.advance_to(born - 1);
assert_eq!(
session.object_count(),
before,
"the troops appeared on the firing tick, a tick before the client hashes them"
);
session.advance_to(born);
assert!(
session.object_count() > before,
"the troops never arrived on the birth tick"
);
}
#[test]
fn a_state_request_waits_for_the_cards_in_the_air() {
let root = assets();
let tables = LogicDataTables::load_from_dir(&root).expect("tables");
LogicDataTables::install(Arc::new(tables));
let builder = BattleBuilder::new(&root);
let mode = builder
.build(
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
LogicDataRef::None,
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
vec![avatar(5), avatar(0)],
[None, None],
1,
)
.expect("battle");
let mut session = BattleSession::new(mode, Vec::new());
assert!(
session.state_on_request().is_some(),
"with nothing in the air a request must be answered at once"
);
let first = session.queue(
0,
LogicVector2::new(14500, 23500),
0,
3,
0,
builder.summon(
&LogicDataRef::spell("Archer"),
LogicVector2::new(14500, 23500),
0,
0,
100,
),
);
session.advance_to(15);
let second = session.queue(
15,
LogicVector2::new(14000, 23500),
0,
3,
1,
builder.summon(
&LogicDataRef::spell("Bomber"),
LogicVector2::new(14000, 23500),
0,
0,
100,
),
);
assert!(second > first, "the second card must land after the first");
let mut refused = Vec::new();
for tick in 16..(second + 4) {
if session.state_on_request().is_none() {
refused.push(session.tick());
}
session.advance_to(tick);
}
for tick in &refused {
let near_a_firing_tick = [first, second]
.iter()
.any(|fires_at| tick > fires_at && tick <= &(fires_at + 2));
assert!(
near_a_firing_tick,
"t={tick}: a state was refused away from any firing tick ({first}, {second}); while a full update is pending that silently eats the player's cards"
);
}
assert!(
!refused.is_empty(),
"the firing ticks were never protected at all"
);
session.advance_to(second + 1);
let mut paid = false;
for _ in 0..8 {
if session.take_outbound().into_iter().count() > 0 {
paid = true;
break;
}
session.advance_to(session.tick() + 1);
}
assert!(paid, "the owed state was never sent after the cards landed");
}