From a68ae0699a4abbb73473780d86c1dd2469ba7e93 Mon Sep 17 00:00:00 2001 From: Shane Madden Date: Sat, 19 Jun 2021 18:24:53 -0600 Subject: [PATCH] Initial commit --- .gitignore | 40 ++++++++++++ Cargo.toml | 16 +++++ LICENSE | 21 +++++++ README.md | 46 ++++++++++++++ example-screeps.toml | 18 ++++++ rust-toolchain | 1 + src/logging.rs | 56 +++++++++++++++++ src/main.rs | 141 +++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 339 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 example-screeps.toml create mode 100644 rust-toolchain create mode 100644 src/logging.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e42aad8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Compiled source # +################### +target + +# Build Files # +############### +Cargo.lock + +# Packages # +############ +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# OS generated files # +###################### +.DS_Store +ehthumbs.db +Icon? +Thumbs.db + +# Project files # +################# +.classpath +.externalToolBuilders +.idea +.project +.settings +nbproject +*.iml +*.ipr +*.iws +*.sublime-project +*.sublime-workspace +screeps.toml diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..83846c5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "screeps-starter-rust" +version = "0.0.0" +authors = [] +edition = "2018" + +[dependencies] +stdweb = "0.4" +log = "0.4" +fern = "0.6" +screeps-game-api = "0.9" + +[profile.release] +panic = "abort" +opt-level = "s" +lto = true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bae16f1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 David Ross + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6f71a97 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# screeps-starter-rust + +Starter Rust AI for [Screeps][screeps], the JavaScript-based MMO game. + +This uses the [`screeps-game-api`] bindings from the [rustyscreeps] organization. + +It's also recommended to use [`cargo-screeps`] for uploading the code, but the code should still +compile if using [`cargo-web`] directly instead. + +The documentation is currently a bit sparse. API docs which list functions one +can use are located at https://docs.rs/screeps-game-api/. + +Almost all crates on https://crates.io/ are usable (only things which interact with OS +apis are broken). + +[`stdweb`](https://crates.io/crates/stdweb) can be used to embed custom JavaScript +into code. + +Quickstart: + +```sh +# clone: + +git clone https://github.com/rustyscreeps/screeps-starter-rust.git +cd screeps-starter-rust + +# cli dependencies: + +cargo install cargo-screeps + +# configure for uploading: + +cp example-screeps.toml screeps.toml +nano screeps.toml + +# build tool: + +cargo screeps --help +``` + +[screeps]: https://screeps.com/ +[`stdweb`]: https://github.com/koute/stdweb +[`cargo-web`]: https://github.com/koute/cargo-web +[`cargo-screeps`]: https://github.com/rustyscreeps/cargo-screeps/ +[`screeps-game-api`]: https://github.com/rustyscreeps/screeps-game-api/ +[rustyscreeps]: https://github.com/rustyscreeps/ diff --git a/example-screeps.toml b/example-screeps.toml new file mode 100644 index 0000000..957b3bd --- /dev/null +++ b/example-screeps.toml @@ -0,0 +1,18 @@ +default_deploy_mode = "upload" + +[upload] +username = "your username or email" +password = "your password" +branch = "default" +# ptr = false + +# hostname = "screeps.com" +# ssl = true +# port = 443 + +# hostname = "localhost" +# ssl = false +# port = 21025 + +# for full syntax, see +# https://github.com/daboross/screeps-in-rust-via-wasm/blob/master/screeps-defaults.toml diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 0000000..245744f --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +1.47.0 \ No newline at end of file diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 0000000..7f5894e --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,56 @@ +use stdweb::js; + +pub use log::LevelFilter::*; + +struct JsLog; +struct JsNotify; + +impl log::Log for JsLog { + fn enabled(&self, _: &log::Metadata<'_>) -> bool { + true + } + fn log(&self, record: &log::Record<'_>) { + let message = format!("{}", record.args()); + js! { + console.log(@{message}); + } + } + fn flush(&self) {} +} +impl log::Log for JsNotify { + fn enabled(&self, _: &log::Metadata<'_>) -> bool { + true + } + fn log(&self, record: &log::Record<'_>) { + let message = format!("{}", record.args()); + js! { + Game.notify(@{message}); + } + } + fn flush(&self) {} +} + +pub fn setup_logging(verbosity: log::LevelFilter) { + fern::Dispatch::new() + .level(verbosity) + .format(|out, message, record| { + out.finish(format_args!( + "({}) {}: {}", + record.level(), + record.target(), + message + )) + }) + .chain(Box::new(JsLog) as Box) + .chain( + fern::Dispatch::new() + .level(log::LevelFilter::Warn) + .format(|out, message, _record| { + let time = screeps::game::time(); + out.finish(format_args!("[{}] {}", time, message)) + }) + .chain(Box::new(JsNotify) as Box), + ) + .apply() + .expect("expected setup_logging to only ever be called once per instance"); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..644da76 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,141 @@ +use std::collections::HashSet; + +use log::*; +use screeps::{find, prelude::*, Part, ResourceType, ReturnCode, RoomObjectProperties}; +use stdweb::js; + +mod logging; + +fn main() { + logging::setup_logging(logging::Info); + + js! { + var game_loop = @{game_loop}; + + module.exports.loop = function() { + // Provide actual error traces. + try { + game_loop(); + } catch (error) { + // console_error function provided by 'screeps-game-api' + console_error("caught exception:", error); + if (error.stack) { + console_error("stack trace:", error.stack); + } + console_error("resetting VM next tick."); + // reset the VM since we don't know if everything was cleaned up and don't + // want an inconsistent state. + module.exports.loop = wasm_initialize; + } + } + } +} + +fn game_loop() { + debug!("loop starting! CPU: {}", screeps::game::cpu::get_used()); + + debug!("running spawns"); + for spawn in screeps::game::spawns::values() { + debug!("running spawn {}", spawn.name()); + let body = [Part::Move, Part::Move, Part::Carry, Part::Work]; + + if spawn.energy() >= body.iter().map(|p| p.cost()).sum() { + // create a unique name, spawn. + let name_base = screeps::game::time(); + let mut additional = 0; + let res = loop { + let name = format!("{}-{}", name_base, additional); + let res = spawn.spawn_creep(&body, &name); + + if res == ReturnCode::NameExists { + additional += 1; + } else { + break res; + } + }; + + if res != ReturnCode::Ok { + warn!("couldn't spawn: {:?}", res); + } + } + } + + debug!("running creeps"); + for creep in screeps::game::creeps::values() { + let name = creep.name(); + debug!("running creep {}", name); + if creep.spawning() { + continue; + } + + if creep.memory().bool("harvesting") { + if creep.store_free_capacity(Some(ResourceType::Energy)) == 0 { + creep.memory().set("harvesting", false); + } + } else { + if creep.store_used_capacity(None) == 0 { + creep.memory().set("harvesting", true); + } + } + + if creep.memory().bool("harvesting") { + let source = &creep + .room() + .expect("room is not visible to you") + .find(find::SOURCES)[0]; + if creep.pos().is_near_to(source) { + let r = creep.harvest(source); + if r != ReturnCode::Ok { + warn!("couldn't harvest: {:?}", r); + } + } else { + creep.move_to(source); + } + } else { + if let Some(c) = creep + .room() + .expect("room is not visible to you") + .controller() + { + let r = creep.upgrade_controller(&c); + if r == ReturnCode::NotInRange { + creep.move_to(&c); + } else if r != ReturnCode::Ok { + warn!("couldn't upgrade: {:?}", r); + } + } else { + warn!("creep room has no controller!"); + } + } + } + + let time = screeps::game::time(); + + if time % 32 == 3 { + info!("running memory cleanup"); + cleanup_memory().expect("expected Memory.creeps format to be a regular memory object"); + } + + info!("done! cpu: {}", screeps::game::cpu::get_used()) +} + +fn cleanup_memory() -> Result<(), Box> { + let alive_creeps: HashSet = screeps::game::creeps::keys().into_iter().collect(); + + let screeps_memory = match screeps::memory::root().dict("creeps")? { + Some(v) => v, + None => { + warn!("not cleaning game creep memory: no Memory.creeps dict"); + return Ok(()); + } + }; + + for mem_name in screeps_memory.keys() { + if !alive_creeps.contains(&mem_name) { + debug!("cleaning up creep memory of dead creep {}", mem_name); + screeps_memory.del(&mem_name); + } + } + + Ok(()) +}