summaryrefslogtreecommitdiff
path: root/src/game.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/game.rs')
-rw-r--r--src/game.rs779
1 files changed, 0 insertions, 779 deletions
diff --git a/src/game.rs b/src/game.rs
deleted file mode 100644
index 00289a0..0000000
--- a/src/game.rs
+++ /dev/null
@@ -1,779 +0,0 @@
-use crate::command::{Action, Command};
-use crate::constants::*;
-use crate::geometry::*;
-use crate::json;
-
-mod player;
-use player::*;
-
-mod powerup;
-use powerup::*;
-
-pub mod map;
-use map::*;
-
-use arrayvec::ArrayVec;
-
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct GameBoard {
- pub round: u16,
- pub max_rounds: u16,
- pub players: [Player; 2],
- pub powerups: ArrayVec<[Powerup; 2]>,
- pub map: Map,
- pub occupied_cells: ArrayVec<[Point2d; 6]>,
- pub outcome: SimulationOutcome,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum SimulationOutcome {
- PlayerWon(usize),
- Draw,
- Continue,
-}
-
-impl GameBoard {
- pub fn new(json: json::State) -> GameBoard {
- let player = Player {
- active_worm: json.active_worm_index().unwrap(),
- moves_score: json.my_player.score - json.my_player.health_score(),
- select_moves: json.my_player.remaining_worm_selections,
- worms: json
- .my_player
- .worms
- .iter()
- .map(|w| Worm {
- id: w.id,
- health: w.health,
- position: Point2d::new(w.position.x, w.position.y),
- rounds_until_unfrozen: w.rounds_until_unfrozen,
- bombs: w.banana_bombs.as_ref().map(|b| b.count).unwrap_or(0),
- snowballs: w.snowballs.as_ref().map(|b| b.count).unwrap_or(0),
- })
- .collect(),
- };
-
- let opponent = Player {
- active_worm: 0,
- moves_score: json.opponents[0].score - json.opponents[0].health_score(),
- select_moves: json.opponents[0].remaining_worm_selections,
- worms: json
- .opponents
- .iter()
- .flat_map(|o| &o.worms)
- .map(|w| Worm {
- id: w.id,
- health: w.health,
- position: Point2d::new(w.position.x, w.position.y),
- rounds_until_unfrozen: w.rounds_until_unfrozen,
- bombs: if w.profession == json::WormType::Agent {
- STARTING_BOMBS
- } else {
- 0
- },
- snowballs: if w.profession == json::WormType::Technologist {
- STARTING_SNOWBALLS
- } else {
- 0
- },
- })
- .collect(),
- };
-
- let mut map = Map::default();
- for cell in json.map.iter().flatten() {
- if cell.cell_type == json::CellType::Dirt {
- map.set(Point2d::new(cell.x, cell.y))
- }
- }
-
- let players = [player, opponent];
- let occupied_cells = players
- .iter()
- .flat_map(|p| p.worms.iter())
- .map(|w| w.position)
- .collect();
-
- GameBoard {
- round: json.current_round,
- max_rounds: json.max_rounds,
- players,
- powerups: json
- .map
- .iter()
- .flatten()
- .filter_map(|c| {
- c.powerup.as_ref().map(|_p| Powerup {
- position: Point2d::new(c.x, c.y),
- })
- })
- .collect(),
- map,
- occupied_cells,
- outcome: SimulationOutcome::Continue,
- }
- }
-
- pub fn update(&mut self, json: json::State) {
- for w in &json.my_player.worms {
- if let Some(worm) = self.players[0].find_worm_mut(w.id) {
- worm.health = w.health;
- worm.position = Point2d::new(w.position.x, w.position.y);
- worm.bombs = w.banana_bombs.as_ref().map(|b| b.count).unwrap_or(0);
- worm.snowballs = w.snowballs.as_ref().map(|b| b.count).unwrap_or(0);
- }
- }
- for w in json.opponents.iter().flat_map(|o| &o.worms) {
- if let Some(worm) = self.players[1].find_worm_mut(w.id) {
- worm.health = w.health;
- worm.position = Point2d::new(w.position.x, w.position.y);
- }
- }
-
- if !self.players[1].active_worm_is_frozen() {
- if json
- .opponents
- .iter()
- .any(|o| o.previous_command.starts_with("banana"))
- {
- for worm in &mut self.players[1].worms {
- worm.bombs = worm.bombs.saturating_sub(1);
- }
- }
- if json
- .opponents
- .iter()
- .any(|o| o.previous_command.starts_with("snowball"))
- {
- for worm in &mut self.players[1].worms {
- worm.snowballs = worm.snowballs.saturating_sub(1);
- }
- }
- }
-
- self.players[0].moves_score = json.my_player.score - json.my_player.health_score();
- self.players[1].moves_score = json.opponents[0].score - json.opponents[0].health_score();
-
- self.players[0].select_moves = json.my_player.remaining_worm_selections;
- self.players[1].select_moves = json.opponents[0].remaining_worm_selections;
-
- self.powerups = json
- .map
- .iter()
- .flatten()
- .filter_map(|c| {
- c.powerup.as_ref().map(|_p| Powerup {
- position: Point2d::new(c.x, c.y),
- })
- })
- .collect();
-
- for cell in json.map.iter().flatten() {
- let point = Point2d::new(cell.x, cell.y);
-
- if cfg!(debug_assertions) {
- // This checks if my lava map is the same as the game
- // engine's lava map. It's off by one because the game
- // engine will update its one at the beginning of
- // processing the round.
- let lava = LAVA_MAP[self.round as usize];
-
- let lava_at = lava.at(point);
- // NB: Map hasn't been updated yet, so it can be used to tell previous state.
- match (&cell.cell_type, self.map.at(point)) {
- (json::CellType::Air, Some(false)) => assert!(
- lava_at == Some(false),
- "Lava at {:?} expected Some(false), but found {:?}",
- point,
- lava_at
- ),
- (json::CellType::Air, _) => assert!(
- lava_at.is_some(),
- "Lava at {:?} expected Some(_), but found {:?}",
- point,
- lava_at
- ),
- (json::CellType::Lava, _) => assert!(
- lava_at == Some(true),
- "Lava at {:?} expected Some(true), but found {:?}",
- point,
- lava_at
- ),
- (json::CellType::DeepSpace, _) => assert!(
- lava_at == None,
- "Lava at {:?} expected None, but found {:?}",
- point,
- lava_at
- ),
- (json::CellType::Dirt, _) => assert!(
- lava_at.is_some(),
- "Lava at {:?} expected Some(_), but found {:?}",
- point,
- lava_at
- ),
- };
- }
-
- if cell.cell_type == json::CellType::Air {
- self.map.clear(point)
- }
- }
-
- self.clear_dead_worms();
- self.players[0].active_worm = json.active_worm_index().unwrap_or(0);
- self.players[1].active_worm = json.opponent_active_worm_index().unwrap_or(0);
-
- self.round += 1;
- debug_assert_eq!(json.current_round, self.round);
- }
-
- pub fn simulate(&mut self, moves: [Command; 2]) {
- self.simulate_worms_on_lava();
- self.simulate_tick_frozen_timers();
-
- self.simulate_select(moves);
-
- let actions = self.identify_actions(moves);
-
- self.simulate_moves(actions);
- self.simulate_digs(actions);
- self.simulate_bombs(actions);
- self.simulate_shoots(actions);
- self.simulate_snowballs(actions);
-
- self.clear_dead_worms();
-
- for player in &mut self.players {
- player.next_active_worm();
- }
-
- self.round += 1;
-
- self.outcome = match (
- self.players[0].worms.len(),
- self.players[1].worms.len(),
- self.round > self.max_rounds,
- ) {
- (0, 0, _) => SimulationOutcome::Draw,
- (_, 0, _) => SimulationOutcome::PlayerWon(0),
- (0, _, _) => SimulationOutcome::PlayerWon(1),
- (_, _, true) => SimulationOutcome::Draw,
- _ => SimulationOutcome::Continue,
- };
- }
-
- fn simulate_worms_on_lava(&mut self) {
- let lava_map = LAVA_MAP[self.round as usize];
- self.players
- .iter_mut()
- .flat_map(|p| p.worms.iter_mut())
- .filter(|w| lava_map.at(w.position) == Some(true))
- .for_each(|ref mut w| w.health -= LAVA_DAMAGE);
- }
-
- fn simulate_tick_frozen_timers(&mut self) {
- self.players
- .iter_mut()
- .flat_map(|p| p.worms.iter_mut())
- .filter(|w| w.health > 0)
- .for_each(|ref mut w| {
- w.rounds_until_unfrozen = w.rounds_until_unfrozen.saturating_sub(1)
- });
- }
-
- fn identify_actions(&self, moves: [Command; 2]) -> [Action; 2] {
- let mut it = self.players.iter().zip(moves.iter()).map(|(p, m)| {
- if p.active_worm_is_frozen() {
- Action::DoNothing
- } else {
- m.action
- }
- });
- [it.next().unwrap(), it.next().unwrap()]
- }
-
- fn simulate_select(&mut self, moves: [Command; 2]) {
- moves
- .iter()
- .zip(self.players.iter_mut())
- .filter(|(_m, player)| !player.active_worm_is_frozen())
- .for_each(|(m, player)| {
- if let Some(worm) = m.worm {
- debug_assert!(
- player.select_moves > 0,
- "Could not make select move, out of select tokens"
- );
- player.select_moves = player.select_moves.saturating_sub(1);
- player.active_worm = player.find_worm_position(worm).unwrap_or(0);
- }
- });
- }
-
- fn simulate_moves(&mut self, actions: [Action; 2]) {
- match actions {
- [Action::Move(p1), Action::Move(p2)] if p1.x == p2.x && p1.y == p2.y => {
- let damage = COLLISION_DAMAGE;
-
- debug_assert_eq!(
- Some(false),
- self.map.at(Point2d::new(p1.x, p1.y)),
- "Movement target wasn't empty, ({}, {})",
- p1.x,
- p1.y
- );
- // Worms have a 50% chance of swapping places
- // here. I'm treating that as an edge case that I
- // don't need to handle for now.
- for player in &mut self.players {
- if let Some(worm) = player.active_worm_mut() {
- worm.health -= damage;
- }
- // You might expect damage score too here, but nope
- player.moves_score += MOVE_SCORE;
- }
- }
- _ => {
- for player_index in 0..actions.len() {
- if let Action::Move(p) = actions[player_index] {
- debug_assert_eq!(
- Some(false),
- self.map.at(p),
- "Movement target wasn't empty, ({}, {})",
- p.x,
- p.y
- );
-
- self.players[player_index].moves_score += MOVE_SCORE;
-
- if let Some(worm) = self.players[player_index].active_worm_mut() {
- debug_assert!(
- (worm.position.x - p.x).abs() <= 1
- && (worm.position.y - p.y).abs() <= 1,
- "Tried to move too far away, ({}, {})",
- p.x,
- p.y
- );
-
- worm.position = p;
-
- self.powerups.retain(|power| {
- if power.position == worm.position {
- worm.health += HEALTH_PACK_VALUE;
- false
- } else {
- true
- }
- });
- }
- }
- }
- }
- }
- }
-
- fn simulate_digs(&mut self, actions: [Action; 2]) {
- for player_index in 0..actions.len() {
- if let Action::Dig(p) = actions[player_index] {
- debug_assert!(
- Some(true) == self.map.at(p)
- || (player_index == 1 && actions[0] == Action::Dig(p)),
- "Tried to dig through air, ({}, {})",
- p.x,
- p.y
- );
- debug_assert! {
- (self.players[player_index].active_worm().unwrap().position.x - p.x).abs() <= 1 &&
- (self.players[player_index].active_worm().unwrap().position.y - p.y).abs() <= 1,
- "Tried to dig too far away, ({}, {})", p.x, p.y
- };
-
- self.players[player_index].moves_score += DIG_SCORE;
-
- self.map.clear(p);
- }
- }
- }
-
- fn simulate_bombs(&mut self, actions: [Action; 2]) {
- // NB: Damage radius has the cell distance rounded UP, throwing range has the cell distance rounded DOWN
-
- let map_clone: Map = self.map;
-
- for player_index in 0..actions.len() {
- if let Action::Bomb(p) = actions[player_index] {
- if self.map.at(p).is_some() {
- if let Some(worm) = self.players[player_index].active_worm_mut() {
- debug_assert!(worm.bombs > 0, "Worm is throwing a bomb it doesn't have");
- debug_assert!((worm.position - p).magnitude_squared() < 6 * 6); // max range is 5, but it's 5 after rounding down
-
- worm.bombs = worm.bombs.saturating_sub(1);
-
- for &(damage_offset, weapon_damage) in BOMB_DAMAGES.iter() {
- let target = p + damage_offset;
-
- if map_clone.at(target) == Some(true) {
- self.map.clear(target);
- self.players[player_index].moves_score += DIG_SCORE;
- }
- self.powerups.retain(|powerup| powerup.position != target);
-
- let target_own_worm: Option<&mut Worm> = self.players[player_index]
- .worms
- .iter_mut()
- .find(|w| w.position == target);
-
- if let Some(target_worm) = target_own_worm {
- target_worm.health -= weapon_damage;
- self.players[player_index].moves_score -=
- weapon_damage * ATTACK_SCORE_MULTIPLIER;
- if target_worm.health <= 0 {
- self.players[player_index].moves_score -= KILL_SCORE;
- }
- }
-
- let target_opponent_worm: Option<&mut Worm> = self.players
- [GameBoard::opponent(player_index)]
- .worms
- .iter_mut()
- .find(|w| w.position == target);
- if let Some(target_worm) = target_opponent_worm {
- target_worm.health -= weapon_damage;
- self.players[player_index].moves_score +=
- weapon_damage * ATTACK_SCORE_MULTIPLIER;
- if target_worm.health <= 0 {
- self.players[player_index].moves_score += KILL_SCORE;
- }
- }
- }
- }
- }
- }
- }
- }
-
- pub fn simulate_snowballs(&mut self, actions: [Action; 2]) {
- for player_index in 0..actions.len() {
- if let Action::Snowball(p) = actions[player_index] {
- if self.map.at(p).is_some() {
- if let Some(worm) = self.players[player_index].active_worm_mut() {
- debug_assert!(
- worm.snowballs > 0,
- "Worm is throwing a snowball it doesn't have"
- );
- debug_assert!((worm.position - p).magnitude_squared() < 6 * 6); // max range is 5, but it's 5 after rounding down
-
- worm.snowballs = worm.snowballs.saturating_sub(1);
-
- for &freeze_offset in SNOWBALL_FREEZES.iter() {
- let target = p + freeze_offset;
-
- let target_own_worm: Option<&mut Worm> = self.players[player_index]
- .worms
- .iter_mut()
- .find(|w| w.position == target);
-
- if let Some(target_worm) = target_own_worm {
- target_worm.rounds_until_unfrozen = FREEZE_DURATION;
- self.players[player_index].moves_score -= FREEZE_SCORE;
- }
-
- let target_opponent_worm: Option<&mut Worm> = self.players
- [GameBoard::opponent(player_index)]
- .worms
- .iter_mut()
- .find(|w| w.position == target);
- if let Some(target_worm) = target_opponent_worm {
- target_worm.rounds_until_unfrozen = FREEZE_DURATION;
- self.players[player_index].moves_score += FREEZE_SCORE;
- }
- }
- }
- }
- }
- }
- }
-
- fn simulate_shoots(&mut self, actions: [Action; 2]) {
- 'players_loop: for player_index in 0..actions.len() {
- if let Action::Shoot(dir) = actions[player_index] {
- if let Some(worm) = self.players[player_index].active_worm() {
- let center = worm.position;
- let diff = dir.as_vec();
-
- let range = if dir.is_diagonal() {
- SHOOT_RANGE_DIAGONAL
- } else {
- SHOOT_RANGE
- };
-
- for distance in 1..=range {
- let target = center + diff * distance;
- match self.map.at(target) {
- Some(false) => {
- let target_own_worm: Option<&mut Worm> = self.players[player_index]
- .worms
- .iter_mut()
- .find(|w| w.position == target);
-
- if let Some(target_worm) = target_own_worm {
- target_worm.health -= SHOOT_DAMAGE;
- self.players[player_index].moves_score -=
- SHOOT_DAMAGE * ATTACK_SCORE_MULTIPLIER;
- if target_worm.health <= 0 {
- self.players[player_index].moves_score -= KILL_SCORE;
- }
- continue 'players_loop;
- }
-
- let target_opponent_worm: Option<&mut Worm> = self.players
- [GameBoard::opponent(player_index)]
- .worms
- .iter_mut()
- .find(|w| w.position == target);
-
- if let Some(target_worm) = target_opponent_worm {
- target_worm.health -= SHOOT_DAMAGE;
- self.players[player_index].moves_score +=
- SHOOT_DAMAGE * ATTACK_SCORE_MULTIPLIER;
- if target_worm.health <= 0 {
- self.players[player_index].moves_score += KILL_SCORE;
- }
-
- continue 'players_loop;
- }
- }
- _ => break,
- }
- }
-
- // You get here if the shot missed. Hits are an early return.
- self.players[player_index].moves_score += MISSED_ATTACK_SCORE;
- }
- }
- }
- }
-
- fn clear_dead_worms(&mut self) {
- for player in &mut self.players {
- player.clear_dead_worms();
- }
-
- self.occupied_cells = self
- .players
- .iter()
- .flat_map(|p| p.worms.iter())
- .map(|w| w.position)
- .collect();
- }
-
- pub fn opponent(player_index: usize) -> usize {
- (player_index + 1) % 2
- }
-
- fn selects_iter(&self, player_index: usize) -> impl Iterator<Item = (Option<i32>, &Worm)> {
- let no_select = self.players[player_index]
- .active_worm()
- .into_iter()
- .map(|w| (None, w));
-
- let has_select_moves = self.players[player_index].select_moves > 0;
- let active_worm_index = self.players[player_index].active_worm;
- let selects = self.players[player_index]
- .worms
- .iter()
- .enumerate()
- .filter(move |(p, _w)| has_select_moves && active_worm_index != *p)
- .map(|(_p, w)| (Some(w.id), w));
-
- no_select.chain(selects)
- }
-
- fn pruned_valid_move_commands(&self, player_index: usize) -> ArrayVec<[Command; 8]> {
- self.players[player_index]
- .active_worm()
- .into_iter()
- .flat_map(|worm| {
- // TODO: If you aren't on lava, don't step onto the lava
- Direction::ALL
- .iter()
- .map(Direction::as_vec)
- .map(move |d| worm.position + d)
- .filter(|p| !self.occupied_cells.contains(p))
- .filter_map(|p| match self.map.at(p) {
- Some(false) => Some(Action::Move(p)),
- Some(true) => Some(Action::Dig(p)),
- _ => None,
- })
- .map(Command::new)
- })
- .collect()
- }
-
- fn pruned_valid_bomb_commands(&self, player_index: usize) -> Vec<Command> {
- self.selects_iter(player_index)
- .filter(|(_, worm)| worm.bombs > 0)
- .flat_map(|(select, worm)| {
- let mut result = Vec::with_capacity((BOMB_RANGE * 2 + 1).pow(2) as usize - 12);
- let own_worm_positions: ArrayVec<[Point2d; 3]> = self.players[player_index]
- .worms
- .iter()
- .map(|w| w.position)
- .collect();
- let opponent_worm_positions: ArrayVec<[Point2d; 3]> = self.players
- [GameBoard::opponent(player_index)]
- .worms
- .iter()
- .map(|w| w.position)
- .collect();
-
- for y in worm.position.y - BOMB_RANGE..=worm.position.y + BOMB_RANGE {
- for x in worm.position.x - BOMB_RANGE..=worm.position.x + BOMB_RANGE {
- let target = Point2d::new(x, y);
- if self.map.at(target).is_some()
- && (worm.position - target).magnitude_squared()
- < (BOMB_RANGE + 1).pow(2)
- {
- let own_affected_worms = own_worm_positions.iter().any(|p| {
- (target - *p).magnitude_squared()
- <= BOMB_DAMAGE_RANGE * BOMB_DAMAGE_RANGE
- });
- let opponent_affected_worms = opponent_worm_positions.iter().any(|p| {
- (target - *p).magnitude_squared()
- <= BOMB_DAMAGE_RANGE * BOMB_DAMAGE_RANGE
- });
-
- if !own_affected_worms && opponent_affected_worms {
- result.push(Command {
- worm: select,
- action: Action::Bomb(target),
- });
- }
- }
- }
- }
-
- result
- })
- .collect()
- }
-
- fn pruned_valid_snowball_commands(&self, player_index: usize) -> Vec<Command> {
- self.selects_iter(player_index)
- .filter(|(_, worm)| worm.snowballs > 0)
- .flat_map(|(select, worm)| {
- let mut result = Vec::with_capacity((SNOWBALL_RANGE * 2 + 1).pow(2) as usize - 12);
- let own_worm_positions: ArrayVec<[Point2d; 3]> = self.players[player_index]
- .worms
- .iter()
- .map(|w| w.position)
- .collect();
- let opponent_worm_positions: ArrayVec<[Point2d; 3]> = self.players
- [GameBoard::opponent(player_index)]
- .worms
- .iter()
- .map(|w| w.position)
- .collect();
-
- for y in worm.position.y - SNOWBALL_RANGE..=worm.position.y + SNOWBALL_RANGE {
- for x in worm.position.x - SNOWBALL_RANGE..=worm.position.x + SNOWBALL_RANGE {
- let target = Point2d::new(x, y);
- if self.map.at(target).is_some()
- && (worm.position - target).magnitude_squared()
- < (SNOWBALL_RANGE + 1).pow(2)
- {
- let own_affected_worms = own_worm_positions.iter().any(|p| {
- (target - *p).magnitude_squared()
- <= SNOWBALL_FREEZE_RANGE * SNOWBALL_FREEZE_RANGE
- });
- let opponent_affected_worms = opponent_worm_positions.iter().any(|p| {
- (target - *p).magnitude_squared()
- <= SNOWBALL_FREEZE_RANGE * SNOWBALL_FREEZE_RANGE
- });
-
- if !own_affected_worms && opponent_affected_worms {
- result.push(Command {
- worm: select,
- action: Action::Snowball(target),
- });
- }
- }
- }
- }
-
- result
- })
- .collect()
- }
-
- fn pruned_valid_shoot_commands(&self, player_index: usize) -> Vec<Command> {
- self.selects_iter(player_index)
- .flat_map(|(select, worm)| {
- self.players[GameBoard::opponent(player_index)]
- .worms
- .iter()
- .filter_map(move |w| {
- let diff = w.position - worm.position;
- if diff.x == 0 && diff.y.abs() <= SHOOT_RANGE {
- if diff.y > 0 {
- Some((Direction::South, diff.y))
- } else {
- Some((Direction::North, -diff.y))
- }
- } else if diff.y == 0 && diff.x.abs() <= SHOOT_RANGE {
- if diff.x > 0 {
- Some((Direction::East, diff.x))
- } else {
- Some((Direction::West, -diff.x))
- }
- } else if diff.x.abs() == diff.y.abs()
- && diff.x.abs() <= SHOOT_RANGE_DIAGONAL
- {
- match (diff.x > 0, diff.y > 0) {
- (true, true) => Some((Direction::SouthEast, diff.x)),
- (false, true) => Some((Direction::SouthWest, -diff.x)),
- (true, false) => Some((Direction::NorthEast, diff.x)),
- (false, false) => Some((Direction::NorthWest, -diff.x)),
- }
- } else {
- None
- }
- })
- .filter(move |(dir, range)| {
- let diff = dir.as_vec();
- // NB: This is up to range EXCLUSIVE, so if there's
- // anything in the way, even another good shot, skip.
- !(1..*range).any(|distance| {
- self.map.at(worm.position + diff * distance) != Some(false)
- && !self
- .players
- .iter()
- .flat_map(|p| p.worms.iter())
- .any(|w| w.position == worm.position + diff * distance)
- })
- })
- .map(move |(dir, _range)| Command {
- worm: select,
- action: Action::Shoot(dir),
- })
- })
- .collect()
- }
-
- pub fn pruned_valid_moves(&self, player_index: usize) -> Vec<Command> {
- if self.players[player_index].active_worm_is_frozen_after_tick() {
- vec![Command::new(Action::DoNothing)]
- } else {
- self.pruned_valid_shoot_commands(player_index)
- .iter()
- .chain(self.pruned_valid_move_commands(player_index).iter())
- .chain(self.pruned_valid_bomb_commands(player_index).iter())
- .chain(self.pruned_valid_snowball_commands(player_index).iter())
- .chain([Command::new(Action::DoNothing)].iter())
- .cloned()
- .collect()
- }
- }
-}
-
-#[cfg(test)]
-mod test {}