1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
//! # Assembly Mechs: Beyond WasmDome SDK
//!
//! _The year is 2020 and our containerized civilization is falling apart.
//! A cruel and villainous DevOps demon named **Boylur Plait** has descended from the cloud to Earth to challenge mankind to a tournament_.
//!
//! To win this tournament, _Assembly Mechs_ must compete in an absurdly over-dramatized contest.
//! These mechs will challenge their creator's ability to write code that will outlast and defeat everything that
//! the demon and its nearly infinite hordes pit against us. Humanity's only hope is to master a technology called **WebAssembly**,
//! win the tournament, and prove to the cloud nemesis that this world is well protected.
//!
//! ## How to Play
//! The game is played by your mech declaring a `handler` function. During each turn, the mech's handler will be invoked
//! and it will be responsible for returning a list of commands. These commands can include requests to move, fire a weapon,
//! perform a radar scan, etc. Commands cost _action points_ and you need to take care that you do not exceed the maximum number
//! of action points per turn (currently **4**).
//!
//! Your mech will have to make clever use of the limited resources and information available to it to devise a strategy for
//! winning the match.
//!
//! The mech interacts with its environment exclusively through the use of the [MechInstruments](trait.MechInstruments.html) trait.
//!
//! ## Possible Mech Actions
//!
//! The following is a list of actions a mech can take by using the appropriate methods in the _Assembly Mech SDK_:
//!
//!
//!| Action | AP Cost | Description |
//!| -------- | -------- | -------- |
//!| [move_mech](trait.MechInstruments.html#tymethod.move_mech)     | 1     | Moves the mech one grid unit in a given direction.|
//!| [fire_primary](trait.MechInstruments.html#tymethod.fire_primary)| 2 | Fires the mech's primary weapon in a given direction. Primary weapons fire a single small projectile that will damage the first thing it encounters. Primary weapon range is available via sensor interrogation. |
//!| [fire_secondary](trait.MechInstruments.html#tymethod.fire_secondary)| 4 | Fires the mech's secondary weapon in a given direction. Secondary weapons fire an explosive projectile that damages the first thing it encounters, as well as producing splash damage that radiates out from the point of impact. Secondary weapon range is available via sensor interrogation.  |
//!| [radar_scan](trait.MechInstruments.html#tymethod.radar_scan) | 1 | Performs a full radar scan of the mech's surroundings, reporting on detected enemies and obstacles. The mech will receive the results of the scan at the beginning of the next turn.|
//!
//!The default, unaffected power of a mech is **4** units, meaning that within a single turn a mech may fire its secondary weapon once,
//! move 4 times, or perform some other combination of actions. Accessing sensor values does not cost you anything.
//!
//! ## Warnings
//! Take care not to exceed the maximum number of action points consumed in a given turn. At best, commands exceeding your [power](trait.MechInstruments.html#tymethod.power)
//! will fail, at worst (depending on the match rules) your mech might be penalized for the attempt
//!
//! Collision damage is real, and your mech's hull will lose structural integrity when colliding with other mechs and with walls
//!
//! # Example
//! ```
//! extern crate wasmdome_mech_sdk as mech;
//!
//! use mech::*;
//!
//! mech_handler!(handler);
//!
//! // Respond to a request to take a turn
//! pub fn handler(mech: impl MechInstruments) -> Vec<MechCommand> {
//!     // Respond with up to 4 action points worth of actions
//!     vec![
//!         mech.request_radar(),
//!         mech.move_mech(GridDirection::North),
//!         mech.fire_primary(GridDirection::South),
//!     ]
//! }
//!
//! ```

pub extern crate wascc_actor;
pub extern crate wasmdome_protocol as protocol;

use wasmdome_domain as domain;

use domain::state::MechState;
pub use domain::{
    commands::MechCommand, GameBoard, GridDirection, Point, RegisterOperation, RegisterValue, EAX,
    EBX, ECX,
};

use wascc_actor::prelude::*;

/// Declares the function to be used as the mech's turn handler
#[macro_export]
macro_rules! mech_handler {
    ($user_handler:ident) => {
        use protocol::commands::{TakeTurn, TakeTurnResponse};
        use protocol::events::MatchEvent;
        use protocol::OP_TAKE_TURN;
        use $crate::wascc_actor::prelude::*;

        actor_handlers!{ OP_TAKE_TURN => handle_take_turn, codec::core::OP_HEALTH_REQUEST => health }

        fn health(_req: codec::core::HealthRequest) -> HandlerResult<()> {
            Ok(())
        }

        fn handle_take_turn(
            take_turn: protocol::commands::TakeTurn,
        ) -> HandlerResult<TakeTurnResponse> {
            let mech =
                $crate::WasmdomeMechInstruments::new(take_turn.clone(), take_turn.actor.clone());
            let mut vec = if mech.is_alive() {
                $user_handler(mech)
            } else {
                Vec::new()
            };
            vec.push(MechCommand::FinishTurn {
                mech: take_turn.actor.clone(),
                turn: take_turn.turn,
            });
            let response = TakeTurnResponse {
                commands: vec,
            };
            Ok(response)
        }
    };
}

/// The interface through which a mech interacts with the arena. Functions on the mech instruments panel are divided into two categories:
/// * Sensor access - These functions are synchronous and the results are accessed immediately within your code. There is no power/action point cost to using these functions.
/// * Commands - The mech's `handler` function must return a vector of commands. The instruments panel has shortcut functions that can be used to generate these commands. Each command has an action point cost associated with it, so take care not to exceed your turn budget
///
/// By cleverly and carefully combining the mech sensor functions with the commands it can issue to the arena, your goal is to build
/// a mech that can outsmart, outgun, and outmaneuver its opponents in the [wasmdome](https://wasmdome.dev).
pub trait MechInstruments {
    /// Obtains the current position of the mech
    fn position(&self) -> Point;
    /// Queries the hull integrity of the mech in remaining hit / damage points
    fn hull_integrity(&self) -> u32;
    /// Returns the number of action points this mech can consume per turn. While this may default to **4**, your code should use this value as a maximum in case matches started with different rules include more or less maximum APs
    fn power(&self) -> u32;
    /// Returns the range (in whole grid units) of the primary weapon. This defaults to **3** but your code should use this if you need to perform calculations based on range
    fn primary_range(&self) -> u32;
    /// Returns the range (in whole grid units) of the secondary weapon, which defaults to **6** but your code should use this if you need to perform calculations based on range
    fn secondary_range(&self) -> u32;
    /// Accesses the last radar scan (if any) performed by your mech. If on turn **x** your mech has a radar request in the command list, then on turn **x+1** that scan's results will be available
    fn last_radar_scan(&self) -> Option<Vec<RadarPing>>;
    /// A handy function that performs the Euclidean calculation for you in order to determine the direction between your mech and a target point
    fn direction_to(&self, target: &Point) -> GridDirection;
    /// Generate a random number between the min and max values (inclusive)
    fn random_number(&self, min: u32, max: u32) -> u32;
    /// Obtains the dimensions of the arena in which the mech resides
    fn world_size(&self) -> GameBoard;

    //- Registers

    /// Accumulates the value stored in the given register. If the register has not been initialized, the accumulator value supplied will be stored in the register (e.g. the value will be added to **0**)
    fn register_acc(&self, reg: u32, val: u64) -> MechCommand;
    /// Safely decrements (with a floor of **0**) the value in the given register
    fn register_dec(&self, reg: u32, val: u64) -> MechCommand;
    /// Sets the value in the given register. This will overwrite any previously existing value
    fn register_set(&self, reg: u32, val: RegisterValue) -> MechCommand;
    /// Queries the value (if any) stored in the given register
    fn register_get(&self, reg: u32) -> Option<&RegisterValue>;

    //- Generate commands

    /// Generates a radar request command to be processed by the game engine at the end of this turn
    fn request_radar(&self) -> MechCommand;
    /// Generates a request to fire the primary weapon
    fn fire_primary(&self, dir: GridDirection) -> MechCommand;
    /// Generates a request to fire the secondary weapon
    fn fire_secondary(&self, dir: GridDirection) -> MechCommand;
    /// Generates a request to move the mech
    fn move_mech(&self, dir: GridDirection) -> MechCommand;
}

/// A single result from a radar scan. When a mech queries for the last radar scan and
/// one is available, those results will be a vector of these radar pings
pub struct RadarPing {
    /// A unique identifier for this target. This is an opaque string and you should assign no internal meaning to it other than to distinguish one target from another
    pub id: String,
    /// Indicates if the discovered target is a friend or foe
    pub foe: bool,
    /// The location from which the radar response originated
    pub location: Point,
    /// A rounded, whole number indicating the distance to the discovered target
    pub distance: usize,
}

#[doc(hidden)]
pub struct WasmdomeMechInstruments {
    actor: String,
    turn: protocol::commands::TakeTurn,
}

#[doc(hidden)]
impl WasmdomeMechInstruments {
    pub fn new(turn: protocol::commands::TakeTurn, actor: String) -> Self {
        WasmdomeMechInstruments { turn, actor }
    }
}

#[doc(hidden)]
impl WasmdomeMechInstruments {
    fn current_mech(&self) -> &MechState {
        &self.turn.state.mechs[&self.actor]
    }

    #[allow(dead_code)]
    pub fn is_alive(&self) -> bool {
        self.current_mech().alive
    }
}

#[doc(hidden)]
impl MechInstruments for WasmdomeMechInstruments {
    fn position(&self) -> Point {
        self.current_mech().position.clone()
    }
    fn hull_integrity(&self) -> u32 {
        self.current_mech().health
    }

    fn power(&self) -> u32 {
        self.turn.state.parameters.aps_per_turn
    }

    fn primary_range(&self) -> u32 {
        domain::state::PRIMARY_RANGE as u32
    }

    fn secondary_range(&self) -> u32 {
        domain::state::SECONDARY_RANGE as u32
    }

    fn direction_to(&self, target: &Point) -> GridDirection {
        self.current_mech().position.bearing(target)
    }

    fn random_number(&self, min: u32, max: u32) -> u32 {
        match extras::default().get_random(min, max) {
            Ok(r) => r,
            Err(_) => 0,
        }
    }

    fn world_size(&self) -> GameBoard {
        GameBoard {
            height: self.turn.state.parameters.height,
            width: self.turn.state.parameters.width,
        }
    }

    //- Registers
    fn register_acc(&self, reg: u32, val: u64) -> MechCommand {
        MechCommand::RegisterUpdate {
            mech: self.current_mech().id.to_string(),
            reg,
            op: RegisterOperation::Accumulate(val),
            turn: self.turn.turn,
        }
    }

    fn register_dec(&self, reg: u32, val: u64) -> MechCommand {
        MechCommand::RegisterUpdate {
            mech: self.current_mech().id.to_string(),
            reg,
            op: RegisterOperation::Decrement(val),
            turn: self.turn.turn,
        }
    }

    fn register_set(&self, reg: u32, val: RegisterValue) -> MechCommand {
        MechCommand::RegisterUpdate {
            mech: self.current_mech().id.to_string(),
            reg,
            op: RegisterOperation::Set(val),
            turn: self.turn.turn,
        }
    }

    fn register_get(&self, reg: u32) -> Option<&RegisterValue> {
        self.current_mech().registers.get(&reg)
    }

    //- Generate commands

    fn request_radar(&self) -> MechCommand {
        MechCommand::RequestRadarScan {
            turn: self.turn.turn,
            mech: self.actor.to_string(),
        }
    }

    fn fire_primary(&self, dir: GridDirection) -> MechCommand {
        MechCommand::FirePrimary {
            turn: self.turn.turn,
            mech: self.actor.to_string(),
            direction: dir,
        }
    }

    fn fire_secondary(&self, dir: GridDirection) -> MechCommand {
        MechCommand::FireSecondary {
            turn: self.turn.turn,
            mech: self.actor.to_string(),
            direction: dir,
        }
    }

    fn move_mech(&self, dir: GridDirection) -> MechCommand {
        MechCommand::Move {
            turn: self.turn.turn,
            mech: self.actor.to_string(),
            direction: dir,
        }
    }

    fn last_radar_scan(&self) -> Option<Vec<RadarPing>> {
        self.turn.state.radar_pings.get(&self.actor).map(|pings| {
            pings
                .iter()
                .map(|p| RadarPing {
                    id: p.name.to_string(),
                    distance: p.distance,
                    foe: p.foe,
                    location: p.location.clone(),
                })
                .collect()
        })
    }
}