215 lines
8.9 KiB
Markdown
215 lines
8.9 KiB
Markdown
# Starter Algo Rust
|
|
|
|
### File overview
|
|
|
|
starter-algo-rust
|
|
|
|
|
├--algo/src
|
|
| |
|
|
| ├--lib.rs
|
|
| ├--bounds.rs
|
|
| ├--gameloop.rs
|
|
| ├--grid.rs
|
|
| ├--io.rs
|
|
| ├--map.rs
|
|
| ├--messages.rs
|
|
| └--units.rs
|
|
|
|
|
└--starter-algo/src
|
|
|
|
|
└main.rs
|
|
|
|
### Creating an algo
|
|
|
|
For starters, simply modify the `starter-algo/src/main.rs` file. You will mostly be using
|
|
methods of `GameState` and `FrameData`.
|
|
|
|
The easiest way to test your algo locally is to run the `build_local.py` script with a Python
|
|
interpreter, then run the engine with the path `rust-algo/algo-target`. The `build_local.py`
|
|
script will build your algo based on the content in `algo.json`, then move the resultant
|
|
binary to the algo target directory.
|
|
|
|
Our servers will essentially do the same thing when you upload your algo.
|
|
To upload your algo, upload the entire rust-algo folder on the myalgos page of the terminal site.
|
|
Note that the max upload size is limited, be carful not to include large build artifacts.
|
|
|
|
The simplest way to create an algo is to create a data type which stores your algo state,
|
|
implement `GameLoop` on it, and create a main function which calls `run_game_loop` on that
|
|
type.
|
|
|
|
The are three callbacks for `GameLoop` which you must implement:
|
|
```rust
|
|
fn initialize(&mut self, config: Arc<Config>);
|
|
|
|
fn on_action_frame(&mut self, config: Arc<Config>, map: &GameState);
|
|
|
|
fn make_move(&mut self, config: Arc<Config>, map: &GameState);
|
|
```
|
|
The `initialize` callback is called once, at the beginning of the game, with the `Config` data
|
|
that this algo has received and deserialized from the game engine.
|
|
|
|
The `on_action_frame` callback is called every action frame, and is given a `MapState`, an immutable
|
|
and random-access representation of the state of the game that frame. The `MapState` also contains
|
|
the deserialized frame data for this frame, including player stats.
|
|
|
|
The `make_move` callback is called every turn frame, and is given mutable access to a
|
|
`MapState`. Here, the algo should mutate the `MapState` by making valid
|
|
moves, such as spawning and removing units. The `MapState` records each spawn command that is
|
|
used to mutate it, and when `make_move` returns, those spawn commands will be submitted to the engine.
|
|
|
|
To generate programming documentation, run cargo doc in rust-algo. Then open index.html in rust-algo/target/doc/starter_algo
|
|
|
|
**The standard output is used to communicate with the game engine, and must not be printed to.**
|
|
For this reason, debugging must be done through `eprintln!`, and never `println!`. The standard
|
|
error messages are available on the playground.
|
|
|
|
Some people may find the `stderrlog` logging backend crate useful for this purpose.
|
|
|
|
### Example algo
|
|
```rust
|
|
extern crate algo;
|
|
|
|
use algo::{
|
|
prelude::*,
|
|
pathfinding::StartedAtWall,
|
|
};
|
|
|
|
fn main() {
|
|
run_game_loop(ExampleAlgo);
|
|
}
|
|
|
|
/// An example algo which showcases the key features provided in this repository
|
|
struct ExampleAlgo;
|
|
|
|
impl GameLoop for ExampleAlgo {
|
|
fn on_turn(&mut self, _: Arc<Config>, map: &MapState) {
|
|
// callback to make a move in the game
|
|
|
|
// try to place as many of four walls as possible
|
|
for &wall_coord in &[
|
|
xy(12, 5),
|
|
xy(13, 5),
|
|
xy(14, 5),
|
|
xy(15, 5),
|
|
] {
|
|
map[wall_coord].try_spawn(StructureUnitType::Wall);
|
|
}
|
|
|
|
// try to atomically place four scouts in two locations
|
|
let scout_coord_1 = xy(6, 7);
|
|
let scout_coord_2 = xy(21, 7);
|
|
|
|
if map[scout_coord_1].can_spawn(MobileUnitType::Scout, 2).yes() &&
|
|
map[scout_coord_2].can_spawn(MobileUnitType::Scout, 2).yes()
|
|
{
|
|
for _ in 0..2 {
|
|
map[scout_coord_1].spawn(MobileUnitType::Scout)
|
|
.expect("Unexpected spawn failure");
|
|
map[scout_coord_2].spawn(MobileUnitType::Scout)
|
|
.expect("Unexpected spawn failure");
|
|
}
|
|
}
|
|
|
|
// if our cores are low, try to delete a structure
|
|
if map.frame_data().p1_stats.cores < 5.0 &&
|
|
map[xy(5, 5)].can_remove_structure().yes()
|
|
{
|
|
map[xy(5, 5)].remove_structure().unwrap();
|
|
}
|
|
|
|
// print the path that an enemy unit would take if spawned at a particular location
|
|
let move_from = xy(13, 27);
|
|
match map.pathfind(move_from, MapEdge::BottomRight) {
|
|
Ok(path) => {
|
|
eprintln!("Path from [13, 27] = {:?}", path)
|
|
},
|
|
Err(StartedAtWall(_, unit)) => {
|
|
eprintln!("Enemy slot [13, 27] is blocked by {:?}", unit);
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Running locally basics
|
|
|
|
1. Install rust. You can use this [link](https://www.rust-lang.org/tools/install) or search 'Install rust' on a search engine like google and follow the most up to date instructions to install rust on your machine.
|
|
2. Compile your algo. Move to the rust-algo working directory, run cargo build. Note that you need to repeat this step to recompile after making changes.
|
|
3. Run ```python ../scripts/run_match.py rust-algo/algo-target```
|
|
|
|
See the README in the scripts folder for more detailed information on running locally.
|
|
|
|
### Project structure
|
|
|
|
Rust starter algo is organized as a [Cargo workspace](https://doc.rust-lang.org/book/second-edition/ch14-03-cargo-workspaces.html).
|
|
This workspace starts with 4 packages:
|
|
|
|
- `algo`: a library that contains everything you need to create an algo, in terms of interacting with the game engine.
|
|
- `starter-algo`: the rust implementation of the starter algo
|
|
- `example`: the algo code in the readme
|
|
- `pathtest`: an executable that we use to verify that algos' pathfinding matches the engine pathfinding when we develop new algos
|
|
|
|
To create your own algo, you can either create your own package, or modify the starter-algo code. If you create your own package,
|
|
you will have to point to it in your `algo.json` metadata file.
|
|
|
|
Rust's `algo.json` file can have these fields:
|
|
|
|
```rust
|
|
{
|
|
"language": "rust",
|
|
"rust-specific": {
|
|
"release": false,
|
|
"package": "starter-algo",
|
|
"toolchain": "stable",
|
|
"compile-target": "algo-target"
|
|
}
|
|
}
|
|
```
|
|
|
|
- release: whether to run the build in release mode (enable optimizations)
|
|
- this will make your code run faster, but it will take longer to compile
|
|
- package: this is the executable package that will be built
|
|
- change this if you implement your strategy in a new package
|
|
- toolchain: which rust toolchain to compile with
|
|
- can be stable, beta, or nightly
|
|
- compile-target: the directory which will be compiled to, then bundled
|
|
|
|
When you upload your algo to our servers, they will compile your code, and move the binary into the `algo-target` directory with
|
|
the name `algo`. Any file in the `algo-target` directory at that point, will be accessible when your algo runs. `algo-target`
|
|
comes with a `run.sh` script, the entry point to all algos. You probably don't need to modify that.
|
|
|
|
If you ever change your build process such that you wish to build to some other directory than `algo-target`, you can simply
|
|
change the target path in the `compile-target` field of `algo.json`.
|
|
|
|
### Code Patterns
|
|
|
|
Idiomatic Rust code is often able to use the type system and borrow checker to minimize points of runtime failure, and then use
|
|
the type system to explicitly denote where these points of failure are. This presents a challenge with creating a terminal
|
|
algo, since essentially any operation performed in a move can fail for many reasons, including:
|
|
|
|
- An operation was performed on a tile which does not exist
|
|
- An operation was performed with a unit type which does not make sense
|
|
- For example, asking how much is costs to spawn the "remove" unit type
|
|
- Attempting to spawn a unit on a tile which is blocked
|
|
- Attempting to spawn a unit without sufficient resources
|
|
|
|
The `algo` packages attacks the first two of these problems using the type and borrowing system.
|
|
|
|
#### Unit type
|
|
|
|
The `units` module contains several different types, each of which has variants for a subset of unit types.
|
|
|
|
- `UnitType`: an enum over all unit types, including remove
|
|
- `StructureUnitType`: an enum over all structure unit types
|
|
- `MobileUnitType`: an enum over all mobile unit types
|
|
- `RemoveUnitType`: a `()`-like struct denoting the remove unit type
|
|
- `SpawnableUnitType`: a union of `StructureUnitType` and `MobileUnitType`
|
|
|
|
These types are convertible into each other, both fallibly and infallibly, through the `Into` trait. For example,
|
|
All unit types implement `Into<UnitType>`. Both `StructureUnitType`, `MobileUnitType`, and `SpawnableUnitType` implement
|
|
`Into<SpawnableUnitType>`, but `RemoveUnitType` does not.
|
|
|
|
This type system allows code which deals with units to be restrictive at compile-time over which unit types are allowed.
|
|
For example, the `Map`'s `cost_of` function accepts an `impl Into<SpawnableUnitType>`, allowing it to be called with
|
|
any `MobileUnitType` or `StructureUnitType`, but never a `RemoveUnitType`.
|