├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── README.md ├── autosave │ ├── README.md │ ├── main.rs │ └── sink.rs ├── initiative │ ├── README.md │ ├── main.rs │ └── rules.rs ├── king_of_the_hill │ ├── README.md │ ├── main.rs │ ├── rules.rs │ └── tcp.rs ├── passive │ ├── README.md │ ├── main.rs │ └── rules.rs ├── pirates │ ├── README.md │ ├── game.rs │ ├── main.rs │ └── rules.rs ├── space │ ├── README.md │ ├── main.rs │ └── rules.rs ├── status │ ├── README.md │ ├── main.rs │ └── rules.rs ├── undo │ ├── README.md │ ├── main.rs │ └── rules.rs └── user_event │ ├── README.md │ ├── main.rs │ └── rules.rs ├── resources ├── client.drawio ├── client.png ├── event.drawio ├── event.png ├── server.drawio └── server.png ├── src ├── ability.rs ├── actor.rs ├── battle.rs ├── character.rs ├── client.rs ├── creature.rs ├── entity.rs ├── entropy.rs ├── error.rs ├── event.rs ├── fight.rs ├── history.rs ├── lib.rs ├── metric.rs ├── object.rs ├── player.rs ├── power.rs ├── round.rs ├── rules │ ├── ability.rs │ ├── empty.rs │ ├── entropy.rs │ ├── generic.rs │ ├── mod.rs │ ├── statistic.rs │ └── status.rs ├── serde.rs ├── server.rs ├── space.rs ├── status.rs ├── team.rs ├── user.rs └── util.rs ├── tests ├── ability_test.rs ├── actor_test.rs ├── battle_test.rs ├── character_test.rs ├── client_server_test.rs ├── creature_test.rs ├── entity_test.rs ├── entropy_test.rs ├── event_test.rs ├── fight_test.rs ├── helper.rs ├── history_test.rs ├── object_test.rs ├── power_test.rs ├── round_test.rs ├── space_test.rs ├── status_test.rs └── team_test.rs └── utilities ├── Cargo.toml └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | TODO.md 4 | **/Cargo.lock 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | sudo: required 3 | rust: 4 | - stable 5 | - beta 6 | - nightly 7 | matrix: 8 | allow_failures: 9 | - rust: nightly 10 | before_script: 11 | - rustup component add rustfmt clippy 12 | script: 13 | - cargo fmt --all -- --check 14 | - cargo clippy --tests --all-features -- -D warnings 15 | - cargo test 16 | - cargo clean 17 | - CARGO_INCREMENTAL=0 RUSTFLAGS="-Ccodegen-units=1 -Cinline-threshold=0 -Coverflow-checks=off" cargo test --all-features 18 | 19 | addons: 20 | apt: 21 | packages: 22 | - libcurl4-openssl-dev 23 | - libelf-dev 24 | - libdw-dev 25 | - cmake 26 | - gcc 27 | - binutils-dev 28 | - libiberty-dev 29 | 30 | after_success: | 31 | test $TRAVIS_RUST_VERSION = "stable" && 32 | wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz && 33 | tar xzf master.tar.gz && 34 | cd kcov-master && 35 | mkdir build && 36 | cd build && 37 | cmake .. && 38 | make && 39 | make install DESTDIR=../../kcov-build && 40 | cd ../.. && 41 | rm -rf kcov-master && 42 | for file in target/debug/*-*; do if [ ${file: -2} == ".d" ]; then continue; fi; mkdir -p "target/cov/$(basename $file)"; ./kcov-build/usr/local/bin/kcov --exclude-path=./tests --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"; done && 43 | bash <(curl -s https://codecov.io/bash) && 44 | echo "Uploaded code coverage" 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.11.0] - 2020-11-03 8 | ### Added 9 | - Added the possibility to invoke team powers, similarly to actors' abilities. 10 | - New associated type `Invocation` in `TeamRules`. 11 | - New methods `invocable` and `invoke` in `TeamRules`. 12 | - New event `InvokePower`. 13 | - Added team powers. 14 | - New associated types `Power`, `PowersSeed` and `PowersAlteration` in `TeamRules`. 15 | - New methods `generate_powers` and `alter_powers` in `TeamRules`. 16 | - New events `AlterPowers` and `RegeneratePowers`. 17 | 18 | ## [0.10.0] - 2020-08-22 19 | ### Fixed 20 | - Improved the ergonomics of handling errors from `EventProcessor`. `ProcessOutput` has a new method `result()` to get a `WeaselResult`. 21 | 22 | ## [0.9.0] - 2020-08-15 23 | ### Changed 24 | - Rounds and turns now reflect the most used definition (a round is made of multiple turns). 25 | - Renamed `StartRound` into `StartTurn`, swapped `EndRound` and `EndTurn` and renamed `EnvironmentRound` into `EnvironmentTurn`. 26 | - Renamed `check_objectives_on_round` into `check_objectives_on_turn`. 27 | - Renamed `on_round_start` into `on_turn_start` and `on_round_end` into `on_turn_end`. 28 | - Renamed `RoundState` into `TurnState`. 29 | 30 | ## [0.8.1] - 2020-08-12 31 | ### Added 32 | - Event trigger `RemoveEntityTrigger` that can fire either a `RemoveCreature` or a `RemoveObject`. 33 | - Re-exported the most used names. 34 | 35 | ### Fixed 36 | - Fixed the incorrect name `ConcludeMissionTrigger`. It is now `ConcludeObjectivesTrigger`. 37 | 38 | ## [0.8.0] - 2020-07-06 39 | ### Added 40 | - New methods `on_character_added` and `on_character_transmuted` in `CharacterRules`. 41 | - `Client` and `Server` are now `Send`. For this to happen some types requires `Send` as well. 42 | - Client and Server implements a new trait, `BattleController`. 43 | - Multiplayer example 'King of the hill'. 44 | - Added accessors to flat event structures. 45 | - Removed metric `ROUNDS_STARTED`. Added counters for rounds and turns in `Rounds`. Added also an `EndTurn` event. 46 | - Introduced `BattleController` trait. 47 | 48 | ### Fixed 49 | - Ambiguous metric ids for `CREATURES_CREATED` and `OBJECTS_CREATED`. 50 | 51 | ## [0.7.0] - 2020-03-30 52 | ### Added 53 | - Implemented `Hash` and `Eq` for `EntityId`. 54 | - Methods to obtain a mutable access to all rules and models. 55 | 56 | ### Changed 57 | - Rounds can now be initiated by multiple actors. 58 | 59 | ## [0.6.0] - 2020-03-11 60 | ### Added 61 | - Support for status effects. 62 | - New methods `generate_status` and `alter_statuses` in `CharacterRules`. 63 | - New methods `apply_status`, `update_status` and `delete_status` in `FightRules`. 64 | - `InflictStatus` and `ClearStatus` events. 65 | - Added `StatusNotPresent` to `WeaselError`. 66 | - Mutable iterators over statistics and abilities. 67 | - New event `EnvironmentRound`. 68 | - New associated type `Potency` in `FightRules`. 69 | - New associated types `Status` and `StatusesAlteration` in `CharacterRules`. 70 | - Example to showcase status effects. 71 | 72 | ### Changed 73 | - Renamed `ActorRules`'s `alter` into `alter_abilities` and `CharacterRules`'s `alter` into `alter_statistics`. 74 | 75 | ### Fixed 76 | - Event's origin is not overridden anymore by the server if it is already set. 77 | 78 | ## [0.5.0] - 2020-02-26 79 | ### Added 80 | - Example for undo/redo of events. 81 | - Added a `GenericError` variant to `WeaselError`. 82 | - Example to showcase passive abilities. 83 | 84 | ### Changed 85 | - The methods `activable`, `on_round_start` and `on_round_end` now take `BattleState` as argument. 86 | - The methods `allow_new_entity`, `activable`, `check_move` now return a `WeaselResult` instead of a bool. 87 | 88 | ## [0.4.1] - 2020-02-22 89 | ### Changed 90 | - Replaced most usages of `HashMap` with `IndexMap`. 91 | 92 | ## [0.4.0] - 2020-02-21 93 | ### Added 94 | - Doc tests for all events and few other structs. 95 | - `Originated` decorator. 96 | - Introduced inanimate objects. 97 | - New events `CreateObject` and `RemoveObject`. 98 | - Improved public API for `Battle` and its submodules. 99 | - New associated type `ObjectId` in `CharacterRules`. 100 | 101 | ### Changed 102 | - It's now possible to manually set an event's origin. 103 | 104 | ## [0.3.1] - 2020-02-17 105 | ### Added 106 | - Order of rounds and initiative example. 107 | - Methods to retrieve an iterator over actors or characters. 108 | - `on_actor_removed` method in `RoundsRules`. 109 | 110 | ## [0.3.0] - 2020-02-16 111 | ### Added 112 | - `AlterSpace` event. 113 | - Example showing different ways to manipulate the space model. 114 | 115 | ### Changed 116 | - `SpaceRules`'s `check_move` and `move_entity` now take as argument a `PositionClaim` instead of an `Option<&dyn Entity>`. 117 | - `SpaceRules`'s `move_entity` is used also to move entities out of the space model. 118 | - `RemoveCreature` frees the entity's position. 119 | - `RoundsRules`'s and `on_start` and `on_end` take as arguments the entities and the space manager objects. 120 | 121 | ## [0.2.0] - 2020-02-15 122 | ### Added 123 | - `RemoveTeam` event. 124 | - An example showing how to use event sinks. 125 | - Example to demonstrate how to create user defined events and metrics. 126 | - `RegenerateStatistics` event. 127 | - `RegenerateAbilities` event. 128 | - `EntityId` now implements `Copy`. 129 | 130 | ## [0.1.0] - 2020-02-08 131 | ### Added 132 | - First available version. 133 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at trisfald@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Thank you for considering contributing to weasel! We all wish to build a great framework together. 4 | 5 | Following these guidelines will help the other developers to make the project grow, in a friendly environment. 6 | 7 | Weasel is looking for all kind of contributions. Helping with the implementation or the design, writing documentation, tests and examples, are great ways to help the project. Using the library and providing your feedback is another great contribution. 8 | 9 | # Ground Rules 10 | 11 | Responsibilities 12 | * Every change should be covered by tests. 13 | * Create issues for any major changes and enhancements that you wish to make. Discuss things transparently and get community feedback. 14 | * Be welcoming to newcomers and encourage diverse new contributors from all backgrounds. 15 | 16 | # Where to start? 17 | 18 | You are welcome to open an issue with our design ideas or proposals. Otherwise, you can pick up a task from the project board. 19 | 20 | # Getting started 21 | 22 | Please follow this format to submit your changes. 23 | 1. Create your own fork of the code 24 | 2. Do the changes in your fork 25 | 3. If you like the change and think the project could use it: 26 | * Be sure you have followed the code style for the project (rustfmt). 27 | * Open a pull request. 28 | 29 | # How to report a bug 30 | 31 | When filing an issue, try to answer these five questions: 32 | 1. What version of Rust are you using? 33 | 2. What operating system and processor architecture are you using? 34 | 3. What did you do? 35 | 4. What did you expect to see? 36 | 5. What did you see instead? 37 | 38 | # How to suggest a feature or enhancement 39 | 40 | As first step, you should check the wiki to ensure that your idea is not already in the roadmap!\ 41 | Then, open an issue on our issues list on GitHub which describes the feature you would like to see, why you need it, and how it should work. 42 | 43 | # Code review process 44 | 45 | The maintainer looks at Pull Requests on a regular basis. He shall review the code in the next few days.\ 46 | After feedback has been given we expect responses within two weeks. After two weeks we may close the pull request if it isn't showing any activity. 47 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "weasel" 3 | version = "0.11.0" 4 | authors = ["Trisfald "] 5 | edition = "2018" 6 | description = "A customizable battle system for turn-based games." 7 | readme = "README.md" 8 | repository = "https://github.com/Trisfald/weasel" 9 | documentation = "https://docs.rs/weasel" 10 | keywords = ["game", "weasel", "turn-based"] 11 | categories = ["game-development"] 12 | license = "MIT" 13 | include = [ 14 | "Cargo.toml", 15 | "LICENSE", 16 | "src/**/*", 17 | "examples/**/*" 18 | ] 19 | 20 | [badges] 21 | maintenance = { status = "actively-developed" } 22 | 23 | [features] 24 | default = [] 25 | random = ["rand", "rand_pcg"] 26 | serialization = ["serde"] 27 | 28 | [dependencies] 29 | num-traits = "0.2" 30 | log = "0.4" 31 | indexmap = "1.3" 32 | rand = { version = "0.7", optional = true } 33 | rand_pcg = { version = "0.2", optional = true } 34 | serde = { version = "1.0", optional = true, features = ["derive"] } 35 | 36 | [dev-dependencies] 37 | util = { path = "utilities" } 38 | serde_json = "1.0" 39 | 40 | [package.metadata.docs.rs] 41 | all-features = true 42 | 43 | [[test]] 44 | name = "entropy-test" 45 | path = "tests/entropy_test.rs" 46 | required-features = ["random"] 47 | 48 | [[example]] 49 | name = "pirates" 50 | required-features = ["random", "serialization"] 51 | 52 | [[example]] 53 | name = "autosave" 54 | required-features = ["serialization"] 55 | 56 | [[example]] 57 | name = "user-event" 58 | path = "examples/user_event/main.rs" 59 | required-features = ["serialization"] 60 | 61 | [[example]] 62 | name = "space" 63 | 64 | [[example]] 65 | name = "initiative" 66 | required-features = ["random"] 67 | 68 | [[example]] 69 | name = "undo" 70 | 71 | [[example]] 72 | name = "passive" 73 | 74 | [[example]] 75 | name = "status" 76 | 77 | [[example]] 78 | name = "king" 79 | path = "examples/king_of_the_hill/main.rs" 80 | required-features = ["random", "serialization"] 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Andrea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Weasel Turn Battle System 2 | [![Build Status](https://travis-ci.org/Trisfald/weasel.svg?branch=master)](https://travis-ci.org/Trisfald/weasel) 3 | [![Code Coverage](https://codecov.io/gh/Trisfald/weasel/branch/master/graph/badge.svg)](https://codecov.io/gh/Trisfald/weasel) 4 | [![crates.io](https://meritbadge.herokuapp.com/weasel)](https://crates.io/crates/weasel) 5 | [![Released API docs](https://docs.rs/weasel/badge.svg)](https://docs.rs/weasel) 6 | [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 7 | 8 | weasel is a customizable battle system for turn-based games. 9 | 10 | * Simple way to define the combat's rules, taking advantage of Rust's strong type system. 11 | * Battle events are collected into a timeline to support save and restore, replays, and more. 12 | * Client/server architecture; all battle events are verified by the server. 13 | * Minimal performance overhead. 14 | 15 | ## Examples 16 | 17 | ```rust 18 | use weasel::{ 19 | battle_rules, rules::empty::*, Battle, BattleController, 20 | BattleRules, CreateTeam, EventTrigger, Server, 21 | }; 22 | 23 | battle_rules! {} 24 | 25 | let battle = Battle::builder(CustomRules::new()).build(); 26 | let mut server = Server::builder(battle).build(); 27 | 28 | CreateTeam::trigger(&mut server, 1).fire().unwrap(); 29 | assert_eq!(server.battle().entities().teams().count(), 1); 30 | ``` 31 | 32 | You can find real examples of battle systems made with weasel in [examples](examples/). 33 | 34 | ## How does it work? 35 | 36 | To use this library, you would create instances of its main objects: `server` and `client`. 37 | You will notice that both of them are parameterized with a `BattleRules` generic type.\ 38 | A `server` is mandatory to manage a game. A server can be also a client. 39 | For example, a typical single player game needs only one server.\ 40 | A `client` is a participant to a game. It sends commands to a server on behalf of a player. 41 | A multiplayer game would have one server and multiple clients. 42 | 43 | Once you have instantiated a `server` and possibly one or more `clients`, 44 | you are ready to begin a new game.\ 45 | Games are carried forward by creating `events`. 46 | There are many kind of events, see the documentation to know more. 47 | 48 | Through a `server` or a `client` you'll be able to access the full state of the battle, 49 | including the entire timeline of events. 50 | 51 | ## Features 52 | 53 | weasel provides many functionalities to ease the development of a turn based game: 54 | 55 | - Creatures and inanimate objects. 56 | - Statistics and abilities for characters. 57 | - Long lasting status effects. 58 | - Player managed teams. 59 | - Team objectives and diplomacy. 60 | - Division of the battle into turns and rounds. 61 | - Rules to govern the game subdivided into orthogonal traits. 62 | - Fully serializable battle history. 63 | - Cause-effect relationship between events. 64 | - Server side verification of clients' events. 65 | - Player permissions and authorization. 66 | - Versioning for battle rules. 67 | - User defined events. 68 | - System and user defined metrics. 69 | - Sinks to forward events to an arbitrary destination. 70 | - Small collection of predefined rules. 71 | 72 | ## Contributing 73 | 74 | Thanks for your interest in contributing! There are many ways to contribute to this project. See [CONTRIBUTING.md](CONTRIBUTING.md). 75 | 76 | ## License 77 | 78 | weasel is provided under the MIT license. See [LICENSE](LICENSE). 79 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | In this directory you can find examples for weasel. 4 | 5 | ## [Pirates](pirates/) 6 | 7 | A simple game in which the player engages in a naval battle, ideal to get yourself acquainted with the fundamental concepts of weasel. 8 | 9 | ## [Autosave](autosave/) 10 | 11 | A simple interactive program to demonstrate how to use event sinks to forward events. 12 | 13 | ## [User event](user_event/) 14 | 15 | Small example showing how to implement user defined events and metrics. 16 | 17 | ## [Space](space/) 18 | 19 | Example program showing the different ways to manage the space dimension in weasel. 20 | 21 | ## [Initiative](initiative/) 22 | 23 | A simple implementation of `RoundsRules` to decide the order of turns based on a creature's statistic. 24 | 25 | ## [Undo](undo/) 26 | 27 | Interactive example in which the player moves around a creature and can undo/redo his moves. 28 | 29 | ## [Passive](passive/) 30 | 31 | An example to show how to define simple passive abilities. 32 | 33 | ## [Status](status/) 34 | 35 | Example to demonstrate the usage of status effects and how to write rules for them. 36 | 37 | ## [King of the hill](king_of_the_hill/) 38 | 39 | Example of a multiplayer card game. 40 | -------------------------------------------------------------------------------- /examples/autosave/README.md: -------------------------------------------------------------------------------- 1 | # Autosave 2 | 3 | An example showing how to use an event sink to populate an autosave.\ 4 | The same pattern can be used to send events to another destination. 5 | 6 | Remember that there are other ways to create savestates, which in certain situations may be better than the one described in this example. For instance, you can manually create a new savestate after each player action or at any other arbitrary moment.\ 7 | If you really care about ensuring that player's progress is not lost, it's better to keep several files and rotate them. 8 | 9 | Run the example with: 10 | ``` 11 | cargo run --example autosave --all-features 12 | ``` 13 | 14 | The program is implemented in two source code files: 15 | - [sink.rs](sink.rs): a sink to dump events to a file. 16 | - [main.rs](main.rs): user input, output messages and managing of the battle. 17 | 18 | The autosave is persisted to disk in `/tmp/autosave`.\ 19 | Since the file is saved in json format, you can open it and have a look at the timeline of events. 20 | -------------------------------------------------------------------------------- /examples/autosave/main.rs: -------------------------------------------------------------------------------- 1 | use crate::sink::AutosaveSink; 2 | use std::convert::TryInto; 3 | use std::fs::File; 4 | use std::{env, io::BufRead, io::BufReader, io::Read}; 5 | use weasel::event::EventSinkId; 6 | use weasel::team::TeamId; 7 | use weasel::{ 8 | battle_rules, rules::empty::*, Battle, BattleController, BattleRules, CreateCreature, 9 | CreateTeam, EventReceiver, EventTrigger, FlatVersionedEvent, Server, 10 | }; 11 | 12 | mod sink; 13 | 14 | // It's not a real game so we can use generic no-op battle rules. 15 | battle_rules! {} 16 | 17 | static TEAM_ID: TeamId = 0; 18 | const AUTOSAVE_NAME: &str = "autosave"; 19 | const SINK_ID: EventSinkId = 0; 20 | 21 | fn main() { 22 | print_intro(); 23 | // The loop where the game progresses. 24 | game_loop(); 25 | // When this point is reached, the game has ended. 26 | println!(); 27 | println!("Goodbye!"); 28 | } 29 | 30 | fn print_intro() { 31 | println!("Autosave"); 32 | println!(); 33 | println!("Example to demonstrate how to use an event sink to create autosaves with weasel."); 34 | println!("Create soldiers and exit whenever you want."); 35 | println!("Next time you launch the game it will resume from the latest progress!"); 36 | println!(); 37 | println!(" Controls:"); 38 | println!(" c - Create a new soldier"); 39 | println!(" q - Quit"); 40 | } 41 | 42 | fn game_loop() { 43 | // Create a server. 44 | let mut server = create_server(); 45 | println!(); 46 | print_soldiers_count(&server); 47 | // Main loop. 48 | loop { 49 | // Read a char from stdin. 50 | let input: Option = std::io::stdin() 51 | .bytes() 52 | .next() 53 | .and_then(|result| result.ok()) 54 | .map(|byte| byte as char); 55 | // Take an action depending on the user input. 56 | if let Some(key) = input { 57 | match key { 58 | 'c' => { 59 | create_soldier(&mut server); 60 | print_soldiers_count(&server); 61 | } 62 | 'q' => break, 63 | _ => {} 64 | } 65 | } 66 | } 67 | } 68 | 69 | /// Retrieves how many creatures are in the battle. 70 | fn get_soldiers_count(server: &Server) -> usize { 71 | server.battle().entities().creatures().count() 72 | } 73 | 74 | fn print_soldiers_count(server: &Server) { 75 | println!("Current number of soldiers: {}", get_soldiers_count(server)); 76 | } 77 | 78 | /// Creates a new 'soldier' creature. 79 | fn create_soldier(server: &mut Server) { 80 | let next_id = get_soldiers_count(server).try_into().unwrap(); 81 | CreateCreature::trigger(server, next_id, TEAM_ID, ()) 82 | .fire() 83 | .unwrap(); 84 | } 85 | 86 | /// Creates a new server. The battle state will be loaded from the autosave, if found. 87 | fn create_server() -> Server { 88 | // Create a new server to manage the battle. 89 | let battle = Battle::builder(CustomRules::new()).build(); 90 | let mut server = Server::builder(battle).build(); 91 | // Read the json stored in a temporary file. 92 | let mut path = env::temp_dir(); 93 | path.push(AUTOSAVE_NAME); 94 | let file = File::open(path); 95 | match file { 96 | Ok(file) => { 97 | let mut reader = BufReader::new(file); 98 | // Deserialize all events, one at a time, because we append them in sequence. 99 | loop { 100 | let mut buffer = Vec::new(); 101 | // We use a delimiter to separate the different json objects. 102 | let result = reader.read_until(b'#', &mut buffer).unwrap(); 103 | if result > 0 { 104 | // Remove the delimiter. 105 | buffer.truncate(buffer.len() - 1); 106 | // Replay the event in the server. 107 | let event: FlatVersionedEvent<_> = serde_json::from_slice(&buffer).unwrap(); 108 | server.receive(event.into()).unwrap() 109 | } else { 110 | // End of file. 111 | break; 112 | } 113 | } 114 | attach_sink(&mut server); 115 | // Return the server with the restored autosave. 116 | server 117 | } 118 | Err(_) => { 119 | // No autosave, so setup a fresh battle. 120 | attach_sink(&mut server); 121 | // Create a team where we will put all soldiers. 122 | CreateTeam::trigger(&mut server, TEAM_ID).fire().unwrap(); 123 | server 124 | } 125 | } 126 | } 127 | 128 | /// Attaches a sink to the server to dump events into a file. 129 | fn attach_sink(server: &mut Server) { 130 | let sink = AutosaveSink::new(SINK_ID, AUTOSAVE_NAME); 131 | server.client_sinks_mut().add_sink(Box::new(sink)).unwrap(); 132 | } 133 | -------------------------------------------------------------------------------- /examples/autosave/sink.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs::{File, OpenOptions}; 3 | use std::io::Write; 4 | use weasel::event::{ClientSink, EventSink, EventSinkId}; 5 | use weasel::{BattleRules, FlatVersionedEvent, VersionedEventWrapper, WeaselResult}; 6 | 7 | /// A sink that dumps events into a file. 8 | pub struct AutosaveSink { 9 | id: EventSinkId, 10 | file: File, 11 | _phantom: std::marker::PhantomData, 12 | } 13 | 14 | impl AutosaveSink { 15 | pub fn new(id: EventSinkId, filename: &str) -> Self { 16 | // Open the autosave file. 17 | let mut path = env::temp_dir(); 18 | path.push(filename); 19 | let file = OpenOptions::new() 20 | .append(true) 21 | .create(true) 22 | .open(path) 23 | .unwrap(); 24 | Self { 25 | id, 26 | file, 27 | _phantom: std::marker::PhantomData, 28 | } 29 | } 30 | } 31 | 32 | impl EventSink for AutosaveSink { 33 | fn id(&self) -> EventSinkId { 34 | self.id 35 | } 36 | 37 | fn on_disconnect(&mut self) { 38 | println!("oh no! the sink got disconnected!") 39 | } 40 | } 41 | 42 | impl ClientSink for AutosaveSink { 43 | fn send(&mut self, event: &VersionedEventWrapper) -> WeaselResult<(), R> { 44 | // Serialize the event to json. 45 | let flat_event: FlatVersionedEvent = event.clone().into(); 46 | let json = serde_json::to_string(&flat_event).unwrap(); 47 | // Append to the file. 48 | self.file.write_all(json.as_bytes()).unwrap(); 49 | // Append a delimiter between json objects. 50 | self.file.write_all(b"#").unwrap(); 51 | Ok(()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/initiative/README.md: -------------------------------------------------------------------------------- 1 | # Initiative 2 | 3 | This example shows how to implement `RoundsRules` to decide the order of acting during a battle. 4 | 5 | The first step is to create five creatures, each one with a different value of *speed*. Then, we will repeatedly start and end turns while also displaying the global order of initiative. 6 | 7 | Run the example with: 8 | ``` 9 | cargo run --example initiative --all-features 10 | ``` 11 | 12 | The program is implemented in two source code files: 13 | - [rules.rs](rules.rs): rules definition (round rules, in particular). 14 | - [main.rs](main.rs): manages the battle and creates a few events. 15 | -------------------------------------------------------------------------------- /examples/initiative/main.rs: -------------------------------------------------------------------------------- 1 | use crate::rules::*; 2 | use std::time::SystemTime; 3 | use weasel::team::TeamId; 4 | use weasel::{ 5 | Battle, BattleController, CreateCreature, CreateTeam, EndTurn, EventTrigger, RemoveCreature, 6 | ResetEntropy, Server, StartTurn, 7 | }; 8 | 9 | mod rules; 10 | 11 | static TEAM_ID: TeamId = 1; 12 | 13 | fn main() { 14 | // Create a server to manage the battle. 15 | let battle = Battle::builder(CustomRules::new()).build(); 16 | let mut server = Server::builder(battle).build(); 17 | // Reset entropy with a 'random enough' seed. 18 | let time = SystemTime::now() 19 | .duration_since(SystemTime::UNIX_EPOCH) 20 | .unwrap(); 21 | ResetEntropy::trigger(&mut server) 22 | .seed(time.as_secs()) 23 | .fire() 24 | .unwrap(); 25 | // Spawn five creatures. 26 | CreateTeam::trigger(&mut server, TEAM_ID).fire().unwrap(); 27 | for i in 1..=5 { 28 | CreateCreature::trigger(&mut server, i, TEAM_ID, ()) 29 | // The speed of the creature is equal to id * 2 + 10. 30 | .statistics_seed((i * 2 + 10).into()) 31 | .fire() 32 | .unwrap(); 33 | } 34 | // Carry out five turn. 35 | for _ in 0..5 { 36 | turn(&mut server); 37 | } 38 | // Remove one creature. 39 | println!("Creature (1) removed!"); 40 | println!(); 41 | RemoveCreature::trigger(&mut server, 1).fire().unwrap(); 42 | // Do a final turn. 43 | turn(&mut server); 44 | } 45 | 46 | fn turn(server: &mut Server) { 47 | // Display in which turn we are. 48 | println!( 49 | "Turn {} - Initiative table:", 50 | server.battle().rounds().completed_turns() + 1 51 | ); 52 | println!(); 53 | // Display the order of initiative. 54 | let initiative = server.battle().rounds().model(); 55 | println!("{}", initiative); 56 | // Find out who should act. 57 | // In this game it's always the creature at the top of the initiative table. 58 | let actor_id = initiative.top(); 59 | println!("It's the turn of: {}", actor_id); 60 | println!(); 61 | // Start the turn. 62 | StartTurn::trigger(server, actor_id).fire().unwrap(); 63 | // Since this's an example, creatures do nothing and immediately end the turn. 64 | EndTurn::trigger(server).fire().unwrap(); 65 | } 66 | -------------------------------------------------------------------------------- /examples/initiative/rules.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result}; 2 | use weasel::character::StatisticId; 3 | use weasel::rules::entropy::UniformDistribution; 4 | use weasel::rules::statistic::SimpleStatistic; 5 | use weasel::{ 6 | battle_rules, rules::empty::*, Actor, BattleRules, CharacterRules, Entities, EntityId, Entropy, 7 | RoundsRules, Space, WriteMetrics, 8 | }; 9 | 10 | static SPEED: StatisticId = 0; 11 | 12 | // Declare the battle rules with the help of a macro. 13 | battle_rules! { 14 | EmptyTeamRules, 15 | // We must provide our own character rules to define creatures' statistics. 16 | CustomCharacterRules, 17 | EmptyActorRules, 18 | EmptyFightRules, 19 | EmptyUserRules, 20 | EmptySpaceRules, 21 | // Our own rules to decide the acting order. 22 | CustomRoundsRules, 23 | // We want entropy rules that can randomize the initiative score. 24 | UniformDistribution 25 | } 26 | 27 | // Define our custom character rules. 28 | #[derive(Default)] 29 | pub struct CustomCharacterRules {} 30 | 31 | impl CharacterRules for CustomCharacterRules { 32 | // Just use an integer as creature id. 33 | type CreatureId = u8; 34 | // Same for objects. 35 | type ObjectId = u8; 36 | // Use statistics with integers as both id and value. 37 | type Statistic = SimpleStatistic; 38 | // The seed will contain the value of speed. 39 | type StatisticsSeed = u16; 40 | // We never alter statistics in this example. 41 | type StatisticsAlteration = (); 42 | // No status effects in this game. 43 | type Status = EmptyStatus; 44 | type StatusesAlteration = (); 45 | 46 | fn generate_statistics( 47 | &self, 48 | seed: &Option, 49 | _entropy: &mut Entropy, 50 | _metrics: &mut WriteMetrics, 51 | ) -> Box> { 52 | // Generate a single statistic: speed. 53 | let v = vec![SimpleStatistic::new(SPEED, seed.unwrap())]; 54 | Box::new(v.into_iter()) 55 | } 56 | } 57 | 58 | // Define our custom rounds rules. 59 | #[derive(Default)] 60 | pub struct CustomRoundsRules {} 61 | 62 | impl RoundsRules for CustomRoundsRules { 63 | // No need for a seed. We always use a fresh model at the start of the battle. 64 | type RoundsSeed = (); 65 | // The model is a struct to hold the initiative of all actors. 66 | type RoundsModel = InitiativeModel; 67 | 68 | fn generate_model(&self, _: &Option) -> Self::RoundsModel { 69 | // Return a default model. 70 | InitiativeModel::default() 71 | } 72 | 73 | fn eligible(&self, model: &Self::RoundsModel, actor: &dyn Actor) -> bool { 74 | // An actor can act only if he's at the top of the initiative ranking. 75 | if model.actors.is_empty() { 76 | false 77 | } else { 78 | model.actors[0].0 == *actor.entity_id() 79 | } 80 | } 81 | 82 | fn on_end( 83 | &self, 84 | entities: &Entities, 85 | _: &Space, 86 | model: &mut Self::RoundsModel, 87 | actor: &dyn Actor, 88 | entropy: &mut Entropy, 89 | _: &mut WriteMetrics, 90 | ) { 91 | // We add speed +- 25% to all actors's initiative. 92 | for actor in entities.actors() { 93 | let speed: f64 = actor.statistic(&SPEED).unwrap().value().into(); 94 | model.update( 95 | actor, 96 | entropy.generate( 97 | (speed - (speed * 0.25)) as u32, 98 | (speed + (speed * 0.25)) as u32, 99 | ), 100 | ); 101 | } 102 | // We set the initiative of the outgoing actor to 0. 103 | model.reset(actor); 104 | // Now sort the initiative of all actors. 105 | model.sort(); 106 | } 107 | 108 | fn on_actor_added( 109 | &self, 110 | model: &mut Self::RoundsModel, 111 | actor: &dyn Actor, 112 | _: &mut Entropy, 113 | _: &mut WriteMetrics, 114 | ) { 115 | // When a new actor is added we simply insert him in the initiative table. 116 | model.insert(actor); 117 | } 118 | 119 | fn on_actor_removed( 120 | &self, 121 | model: &mut Self::RoundsModel, 122 | actor: &dyn Actor, 123 | _: &mut Entropy, 124 | _: &mut WriteMetrics, 125 | ) { 126 | // Remove the actor from the model. 127 | model.remove(actor); 128 | } 129 | } 130 | 131 | #[derive(Default)] 132 | pub(crate) struct InitiativeModel { 133 | // A vector where we store all actors with their current initiative score. 134 | actors: Vec<(EntityId, u32)>, 135 | } 136 | 137 | impl InitiativeModel { 138 | /// Sorts the actors by their initiative score (descending). 139 | fn sort(&mut self) { 140 | self.actors.sort_by(|lhs, rhs| rhs.1.cmp(&lhs.1)); 141 | } 142 | 143 | fn insert(&mut self, actor: &dyn Actor) { 144 | // Insert the actor with an initial score equal to his speed. 145 | self.actors.push(( 146 | *actor.entity_id(), 147 | actor.statistic(&SPEED).unwrap().value().into(), 148 | )); 149 | // Sort the actors. 150 | self.sort(); 151 | } 152 | 153 | /// Returns the actor with the highest score of initiative. 154 | pub(crate) fn top(&self) -> EntityId { 155 | self.actors[0].0 156 | } 157 | 158 | /// Sets the initiative score of the given actor to 0. 159 | fn reset(&mut self, actor: &dyn Actor) { 160 | if let Some(index) = self.actor_index(actor) { 161 | self.actors[index].1 = 0; 162 | } 163 | } 164 | 165 | /// Adds `value` to the initiative of `actor`. 166 | fn update(&mut self, actor: &dyn Actor, value: u32) { 167 | if let Some(index) = self.actor_index(actor) { 168 | self.actors[index].1 += value; 169 | } 170 | } 171 | 172 | /// Removes the given actor. 173 | fn remove(&mut self, actor: &dyn Actor) { 174 | if let Some(index) = self.actor_index(actor) { 175 | self.actors.remove(index); 176 | } 177 | } 178 | 179 | fn actor_index(&self, actor: &dyn Actor) -> Option { 180 | self.actors 181 | .iter() 182 | .position(|(actor_id, _)| actor_id == actor.entity_id()) 183 | } 184 | } 185 | 186 | impl Display for InitiativeModel { 187 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 188 | // Print a table with all actors and their initiative score. 189 | writeln!(f, "Actor Score")?; 190 | for (actor_id, score) in &self.actors { 191 | writeln!(f, "{} {}", actor_id, score)?; 192 | } 193 | Ok(()) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /examples/king_of_the_hill/README.md: -------------------------------------------------------------------------------- 1 | # King of the hill 2 | 3 | A multiplayer card game designed for three players. 4 | 5 | One player must act as server: 6 | ``` 7 | cargo run --example king --all-features 8 | ``` 9 | 10 | The other players must connect to the server: 11 | ``` 12 | cargo run --example king --all-features :3000 13 | ``` 14 | 15 | ## The rules 16 | 17 | The rules are extremely simple to keep the focus on how to use the library, not on the game itself. Any similarity with existing games is purely coincidental. 18 | - The game starts with three players and a deck of nine cards numbered from 1 to 9. 19 | - Each player is given three cards, randomly. 20 | - During each turn, for a total of three turns, every player chooses a card to play. 21 | - The highest card wins. 22 | - The player with the most number of wins is the game's winner. 23 | 24 | Clients can leave the game and reconnect to resume playing! 25 | 26 | ## Let's get to business 27 | 28 | The *King of hill* game is implemented in three source code files: 29 | - [rules.rs](rules.rs): contains all rules for our card game. 30 | - [main.rs](main.rs): all the necessary code to handle player input, textual output and game progress. 31 | - [tpc.rs](tpc.rs): manages networking between players. 32 | -------------------------------------------------------------------------------- /examples/king_of_the_hill/rules.rs: -------------------------------------------------------------------------------- 1 | use weasel::ability::AbilityId; 2 | use weasel::character::StatisticId; 3 | use weasel::rules::{ability::SimpleAbility, statistic::SimpleStatistic}; 4 | use weasel::{ 5 | battle_rules, rules::empty::*, Action, Actor, ActorRules, BattleRules, BattleState, 6 | CharacterRules, Entities, EntityId, Entropy, EventQueue, EventTrigger, MoveEntity, 7 | PositionClaim, RoundsRules, Space, SpaceRules, TeamRules, WeaselError, WeaselResult, 8 | WriteMetrics, 9 | }; 10 | 11 | pub(crate) static CARD_VALUE_STAT: StatisticId = 0; 12 | pub(crate) static PLAY_CARD_ABILITY: AbilityId = 0; 13 | 14 | // Define our custom character rules. 15 | // Since this's a card game, players will just handle creatures (the 'cards'). 16 | #[derive(Default)] 17 | pub struct MyCharacterRules {} 18 | 19 | impl CharacterRules for MyCharacterRules { 20 | // Id for cards. 21 | type CreatureId = u8; 22 | // No objects in this game 23 | type ObjectId = (); 24 | // Our cards have just a statistic that tells what the card's value is. 25 | // We use the `SimpleStatistic` type from weasel to avoid implementing our own. 26 | type Statistic = SimpleStatistic; 27 | // The seed is equal to the card value. 28 | type StatisticsSeed = u8; 29 | // A card value is immutable. 30 | type StatisticsAlteration = (); 31 | // This game doesn't have long lasting status effects. 32 | type Status = EmptyStatus; 33 | type StatusesAlteration = (); 34 | 35 | // In this method we generate statistics of cards. 36 | fn generate_statistics( 37 | &self, 38 | seed: &Option, 39 | _: &mut Entropy, 40 | _: &mut WriteMetrics, 41 | ) -> Box> { 42 | let value = seed.unwrap(); 43 | // Generate one statistic with the card value. 44 | let v = vec![SimpleStatistic::new(CARD_VALUE_STAT, value)]; 45 | Box::new(v.into_iter()) 46 | } 47 | } 48 | 49 | // Define our custom team rules. A team is equivalent to a player. 50 | #[derive(Default)] 51 | pub struct MyTeamRules {} 52 | 53 | impl TeamRules for MyTeamRules { 54 | type Id = u8; 55 | // Teams don't have powers in this example. 56 | type Power = EmptyPower; 57 | type PowersSeed = (); 58 | type Invocation = (); 59 | type PowersAlteration = (); 60 | // How many turns a team has won. 61 | type ObjectivesSeed = u8; 62 | // Our objective is to win 'turns', so a simple counter will suffice. 63 | type Objectives = u8; 64 | 65 | fn generate_objectives(&self, seed: &Option) -> Self::Objectives { 66 | seed.unwrap_or_default() 67 | } 68 | } 69 | 70 | // We define the round rules to impose an ordering to player's moves. 71 | #[derive(Default)] 72 | pub struct MyRoundsRules {} 73 | 74 | impl RoundsRules for MyRoundsRules { 75 | // No seed. Rounds' ordering is static. 76 | type RoundsSeed = (); 77 | // The model is just a counter. 78 | type RoundsModel = u8; 79 | 80 | fn generate_model(&self, _: &Option) -> Self::RoundsModel { 81 | // The first player to move will be the one at index 0. 82 | 0 83 | } 84 | 85 | fn eligible(&self, model: &Self::RoundsModel, actor: &dyn Actor) -> bool { 86 | // A card can be played only if it belongs to the right team. 87 | actor.team_id() == model 88 | } 89 | 90 | fn on_end( 91 | &self, 92 | _: &Entities, 93 | _: &Space, 94 | model: &mut Self::RoundsModel, 95 | _: &dyn Actor, 96 | _: &mut Entropy, 97 | _: &mut WriteMetrics, 98 | ) { 99 | // When a player turn ends bump the counter, wrapping at 3 100 | // so that it cycles between 0, 1 and 2. 101 | *model = (*model + 1) % 3; 102 | } 103 | } 104 | 105 | // Space rules in this case define the position of the cards during the game. 106 | // The game is played by moving cards around, after all! 107 | // In summary, a card can be either in a player's hand or on the table. 108 | #[derive(Default)] 109 | pub struct MySpaceRules {} 110 | 111 | impl SpaceRules for MySpaceRules { 112 | // false: in hand, true: on the table. 113 | type Position = bool; 114 | type SpaceSeed = (); 115 | // Array with the id of cards on the table. 116 | type SpaceModel = [Option>; 3]; 117 | type SpaceAlteration = (); 118 | 119 | fn generate_model(&self, _seed: &Option) -> Self::SpaceModel { 120 | // At the start the table is empty. 121 | [None, None, None] 122 | } 123 | 124 | fn check_move( 125 | &self, 126 | model: &Self::SpaceModel, 127 | _claim: PositionClaim, 128 | position: &Self::Position, 129 | ) -> WeaselResult<(), CustomRules> { 130 | // We can play a card only if the table isn't full. 131 | if *position { 132 | if model.iter().any(|e| e.is_none()) { 133 | return Ok(()); 134 | } else { 135 | return Err(WeaselError::UserError("move not allowed".to_string())); 136 | } 137 | } 138 | Ok(()) 139 | } 140 | 141 | fn move_entity( 142 | &self, 143 | model: &mut Self::SpaceModel, 144 | claim: PositionClaim, 145 | position: Option<&Self::Position>, 146 | _: &mut WriteMetrics, 147 | ) { 148 | match position { 149 | Some(play) => { 150 | // If we try to put a card to the table. 151 | if *play { 152 | // Find an empty slot. 153 | let index = model.iter().position(|e| e.is_none()).unwrap(); 154 | // Insert the card. 155 | model[index] = Some(*claim.entity_id()); 156 | } 157 | } 158 | None => { 159 | // Remove the card from the table. 160 | for entry in model { 161 | if let Some(id) = entry { 162 | if id == claim.entity_id() { 163 | *entry = None; 164 | } 165 | } 166 | } 167 | } 168 | } 169 | } 170 | } 171 | 172 | // Define our custom actor rules. 173 | #[derive(Default)] 174 | pub struct MyActorRules {} 175 | 176 | impl ActorRules for MyActorRules { 177 | // The only ability in this game is 'play a card'. 178 | type Ability = SimpleAbility; 179 | type AbilitiesSeed = (); 180 | // We don't need anything else but the card, to play it. 181 | type Activation = (); 182 | // Abilities are immutable. 183 | type AbilitiesAlteration = (); 184 | 185 | fn generate_abilities( 186 | &self, 187 | _: &Option, 188 | _: &mut Entropy, 189 | _: &mut WriteMetrics, 190 | ) -> Box> { 191 | let v = vec![SimpleAbility::new(PLAY_CARD_ABILITY, ())]; 192 | Box::new(v.into_iter()) 193 | } 194 | 195 | fn activate( 196 | &self, 197 | _: &BattleState, 198 | action: Action, 199 | event_queue: &mut Option>, 200 | _: &mut Entropy, 201 | _: &mut WriteMetrics, 202 | ) { 203 | // The result of playing a card is to change its position from the hand to the table. 204 | let card = action.actor.entity_id(); 205 | MoveEntity::trigger(event_queue, *card, true).fire(); 206 | } 207 | } 208 | 209 | // We use the `battle_rules` macro to define a type `CustomRules` that implements 210 | // the `BattleRules` trait, which as the name suggests defines the game's rules. 211 | // We mix our custom defined rules with default (empty) ones. 212 | battle_rules! { 213 | MyTeamRules, 214 | MyCharacterRules, 215 | MyActorRules, 216 | EmptyFightRules, 217 | EmptyUserRules, 218 | MySpaceRules, 219 | MyRoundsRules, 220 | EmptyEntropyRules 221 | } 222 | -------------------------------------------------------------------------------- /examples/passive/README.md: -------------------------------------------------------------------------------- 1 | # Passive 2 | 3 | In this example we implement a simple passive ability. This skill increases the power of another ability at every turn. 4 | 5 | At the start we'll spawn two soldiers. Both of them known the ability *punch*, but only one has the passive ability *power up*. 6 | 7 | Every time the soldier ends his turn, *power up* increases the power of *punch* by one times the number of creatures on the battlefield. 8 | 9 | Run the example with: 10 | ``` 11 | cargo run --example passive 12 | ``` 13 | 14 | The program is implemented in two source code files: 15 | - [rules.rs](rules.rs): actor rules definition. 16 | - [main.rs](main.rs): manages the battle and creates a few events. 17 | -------------------------------------------------------------------------------- /examples/passive/main.rs: -------------------------------------------------------------------------------- 1 | use crate::rules::*; 2 | use weasel::creature::CreatureId; 3 | use weasel::team::TeamId; 4 | use weasel::{ 5 | Actor, Battle, BattleController, CreateCreature, CreateTeam, EndTurn, Entity, EntityId, 6 | EventTrigger, Server, StartTurn, 7 | }; 8 | 9 | mod rules; 10 | 11 | static TEAM_ID: TeamId = 1; 12 | static CREATURE_1_ID: CreatureId = 1; 13 | static CREATURE_2_ID: CreatureId = 2; 14 | static ENTITY_1_ID: EntityId = EntityId::Creature(CREATURE_1_ID); 15 | static ENTITY_2_ID: EntityId = EntityId::Creature(CREATURE_2_ID); 16 | 17 | fn main() { 18 | // Create a server to manage the battle. 19 | let battle = Battle::builder(CustomRules::new()).build(); 20 | let mut server = Server::builder(battle).build(); 21 | // Create a team. 22 | CreateTeam::trigger(&mut server, TEAM_ID).fire().unwrap(); 23 | println!("Spawning two creatures..."); 24 | println!(); 25 | // Spawn a creature with a single ability: PUNCH. 26 | CreateCreature::trigger(&mut server, CREATURE_1_ID, TEAM_ID, ()) 27 | .abilities_seed(vec![PUNCH]) 28 | .fire() 29 | .unwrap(); 30 | // Spawn a creature with two abilities: PUNCH and POWER_UP. 31 | CreateCreature::trigger(&mut server, CREATURE_2_ID, TEAM_ID, ()) 32 | .abilities_seed(vec![PUNCH, POWER_UP]) 33 | .fire() 34 | .unwrap(); 35 | println!( 36 | "Now doing three rounds of combat. Notice how Creature (2) punches get more powerful!" 37 | ); 38 | println!(); 39 | // Carry out three round. 40 | for i in 0..3 { 41 | round(&mut server, i); 42 | } 43 | } 44 | 45 | /// Does a round, containing a turn for each creatures. 46 | fn round(server: &mut Server, turn: u32) { 47 | // Display in which round we are. 48 | println!("Round {}", turn + 1); 49 | println!(); 50 | print_power(server, CREATURE_1_ID); 51 | print_power(server, CREATURE_2_ID); 52 | // Start and end a turn for the first creature. 53 | println!("Turn of Creature (1)..."); 54 | StartTurn::trigger(server, ENTITY_1_ID).fire().unwrap(); 55 | EndTurn::trigger(server).fire().unwrap(); 56 | // Start and end a turn for the second creature. 57 | println!("Turn of Creature (2)..."); 58 | StartTurn::trigger(server, ENTITY_2_ID).fire().unwrap(); 59 | EndTurn::trigger(server).fire().unwrap(); 60 | print_power(server, CREATURE_1_ID); 61 | print_power(server, CREATURE_2_ID); 62 | println!(); 63 | } 64 | 65 | /// Displays the punch power of a creature. 66 | fn print_power(server: &Server, id: CreatureId) { 67 | let creature = server.battle().entities().creature(&id).unwrap(); 68 | println!( 69 | "{} punch power: {:?}", 70 | creature.entity_id(), 71 | creature.ability(&PUNCH).unwrap().power() 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /examples/passive/rules.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "serialization")] 2 | use serde::{Deserialize, Serialize}; 3 | use weasel::ability::AbilityId; 4 | use weasel::rules::ability::SimpleAbility; 5 | use weasel::{ 6 | battle_rules, battle_rules_with_actor, rules::empty::*, Actor, ActorRules, AlterAbilities, 7 | BattleRules, BattleState, Entropy, EventQueue, EventTrigger, WriteMetrics, 8 | }; 9 | 10 | /// Id for the active ability 'punch'. 11 | pub(crate) static PUNCH: AbilityId = 1; 12 | /// Id for the passive ability 'power up'. 13 | pub(crate) static POWER_UP: AbilityId = 2; 14 | /// Starting power for punches. 15 | pub(crate) const PUNCH_START_POWER: u32 = 10; 16 | 17 | // In this example we only need to redefine the actor rules 18 | battle_rules_with_actor! { 19 | CustomActorRules 20 | } 21 | 22 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 23 | #[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))] 24 | pub enum AbilityPower { 25 | Passive, 26 | Attack(u32), 27 | } 28 | 29 | // Define our custom actor rules. 30 | #[derive(Default)] 31 | pub struct CustomActorRules {} 32 | 33 | impl ActorRules for CustomActorRules { 34 | // Abilities can either be passives or direct attacks. 35 | type Ability = SimpleAbility; 36 | // Vector with Id of abilities to generate. 37 | type AbilitiesSeed = Vec>; 38 | // We don't care for activations in this example. 39 | type Activation = (); 40 | // We need to be able to modify the PUNCH ability. 41 | // Let's use a tuple with ability id and a new AbilityPower. 42 | type AbilitiesAlteration = (u32, AbilityPower); 43 | 44 | fn generate_abilities( 45 | &self, 46 | seed: &Option, 47 | _entropy: &mut Entropy, 48 | _metrics: &mut WriteMetrics, 49 | ) -> Box> { 50 | let mut v = Vec::new(); 51 | // Generate abilities from the seed. 52 | if let Some(seed) = seed { 53 | for id in seed { 54 | if *id == PUNCH { 55 | // For PUNCH we generate an attack ability. 56 | v.push(SimpleAbility::new( 57 | *id, 58 | AbilityPower::Attack(PUNCH_START_POWER), 59 | )); 60 | } else if *id == POWER_UP { 61 | // For POWER_UP we generate a passive ability. 62 | v.push(SimpleAbility::new(*id, AbilityPower::Passive)); 63 | } 64 | } 65 | } 66 | Box::new(v.into_iter()) 67 | } 68 | 69 | fn alter_abilities( 70 | &self, 71 | actor: &mut dyn Actor, 72 | alteration: &Self::AbilitiesAlteration, 73 | _entropy: &mut Entropy, 74 | _metrics: &mut WriteMetrics, 75 | ) { 76 | // Alter abilities. 77 | // We know the id to alter and what is the new value. 78 | let id_to_alter = alteration.0; 79 | let new_power = alteration.1; 80 | if let Some(ability) = actor.ability_mut(&id_to_alter) { 81 | ability.set_power(new_power); 82 | } 83 | } 84 | 85 | fn on_turn_end( 86 | &self, 87 | state: &BattleState, 88 | actor: &dyn Actor, 89 | event_queue: &mut Option>, 90 | _entropy: &mut Entropy, 91 | _metrics: &mut WriteMetrics, 92 | ) { 93 | // In this method we activate the effect of our passive. 94 | // First check if the actor knows the passive ability. 95 | if actor.ability(&POWER_UP).is_some() { 96 | // Now we take the number of creatures in the game. 97 | let count = state.entities().creatures().count(); 98 | // Get the current power of the actor's punch. 99 | if let Some(punch) = actor.ability(&PUNCH) { 100 | // Sum the number of creatures to the power of punch. 101 | let current_power = if let AbilityPower::Attack(p) = punch.power() { 102 | p 103 | } else { 104 | 0 105 | }; 106 | let new_power = current_power + count as u32; 107 | // Construct an ability alteration. 108 | let alteration = (PUNCH, AbilityPower::Attack(new_power)); 109 | // Alter the actor punch ability. 110 | AlterAbilities::trigger(event_queue, *actor.entity_id(), alteration).fire(); 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /examples/pirates/README.md: -------------------------------------------------------------------------------- 1 | # Pirates 2 | 3 | A simple singleplayer game to demonstrate the basic capabilities of `weasel`. The game has very simple rules, thus it's a good example for people to understand how `weasel` works. 4 | 5 | Run the example with: 6 | ``` 7 | cargo run --example pirates --all-features 8 | ``` 9 | 10 | ## The objective 11 | 12 | Create a simple game with the following characteristics: 13 | - Two teams, one controlled by the computer. 14 | - Each team has one ship. 15 | - Ships' position doesn't matter. 16 | - Ships have values for hull (100) and crew (100). 17 | - Ships have two abilities: one to damage the hull and another to damage the crew. 18 | - Damage of attacks is randomized between 10 + crew/20 and 10 + crew/5. 19 | - Ships sink when their hull reaches 0. 20 | - The last team standing will be the winner. 21 | - At the start of each player turn, it's possible to save the game. 22 | - Savestates can be loaded at any time. 23 | 24 | ## Let's get to business 25 | 26 | The *Pirates* game is implemented in three source code files: 27 | - [rules.rs](rules.rs): contains all rules for the battle system. 28 | - [game.rs](game.rs): manages a running game. 29 | - [main.rs](main.rs): all the necessary code to handle player input, textual output and initialization. 30 | 31 | During the game you will have to possibility to create or load a savestate. The latter is persisted to disk in `/tmp/savegame`.\ 32 | Since the file is saved in json format, you can open it and have a look at the timeline of events. 33 | -------------------------------------------------------------------------------- /examples/pirates/main.rs: -------------------------------------------------------------------------------- 1 | use crate::game::Game; 2 | use std::io::Read; 3 | 4 | mod game; 5 | mod rules; 6 | 7 | fn main() { 8 | print_intro(); 9 | // The loop where the game progresses. 10 | game_loop(); 11 | // When this point is reached, the game has ended. 12 | println!(); 13 | println!("Goodbye!"); 14 | } 15 | 16 | fn print_intro() { 17 | println!("Welcome to Pirates!"); 18 | println!(); 19 | println!("Sink the enemy's ship by shooting your cannons!"); 20 | println!(); 21 | print_controls(); 22 | } 23 | 24 | fn print_controls() { 25 | println!(" Controls:"); 26 | println!(" 1 - Fire cannonballs"); 27 | println!(" 2 - Fire grapeshots"); 28 | println!(" h - Display the controls"); 29 | println!(" s - Save the game state"); 30 | println!(" l - Load the savegame"); 31 | println!(" q - Quit"); 32 | } 33 | 34 | fn print_separator() { 35 | println!("--------------------------------------------------------------------------------"); 36 | } 37 | 38 | fn game_loop() { 39 | // Create a game instance. 40 | let mut game = Game::new(); 41 | turn_header(&game); 42 | // In this loop we process user input and dispatch to the corresponding method. 43 | loop { 44 | // Read a char from stdin. 45 | let input: Option = std::io::stdin() 46 | .bytes() 47 | .next() 48 | .and_then(|result| result.ok()) 49 | .map(|byte| byte as char); 50 | // Take an action depending on the user input. 51 | if let Some(key) = input { 52 | match key { 53 | // When the player takes a move we first fire an ability, then let the enemy act. 54 | // Remember to always check if there's a winner. 55 | '1' => { 56 | print_separator(); 57 | game.fire_cannonball(); 58 | if game.check_winner() { 59 | break; 60 | } 61 | game.enemy_turn(); 62 | if game.check_winner() { 63 | break; 64 | } 65 | turn_header(&game); 66 | } 67 | '2' => { 68 | print_separator(); 69 | game.fire_grapeshot(); 70 | if game.check_winner() { 71 | break; 72 | } 73 | game.enemy_turn(); 74 | if game.check_winner() { 75 | break; 76 | } 77 | turn_header(&game); 78 | } 79 | 'h' => print_controls(), 80 | 's' => game.save(), 81 | 'l' => { 82 | game.load(); 83 | if game.check_winner() { 84 | break; 85 | } 86 | turn_header(&game); 87 | } 88 | 'q' => break, 89 | _ => {} 90 | } 91 | } 92 | } 93 | } 94 | 95 | /// Displays the player's and enemy's statistics. 96 | fn turn_header(game: &Game) { 97 | let (player_ship_hull, player_ship_crew) = game.player_stats(); 98 | let (enemy_ship_hull, enemy_ship_crew) = game.enemy_stats(); 99 | let stat_to_string = |stat| { 100 | let i = (stat as usize + 5 - 1) / 5; // ceiling 101 | std::iter::repeat("=") 102 | .take(i) 103 | .chain(std::iter::repeat(" ").take(20 - i)) 104 | .collect::() 105 | }; 106 | print_separator(); 107 | println!("--- PLAYER SHIP ENEMY SHIP ---"); 108 | println!( 109 | "--- HULL [{}] HULL [{}] ---", 110 | stat_to_string(player_ship_hull), 111 | stat_to_string(enemy_ship_hull) 112 | ); 113 | println!( 114 | "--- CREW [{}] CREW [{}] ---", 115 | stat_to_string(player_ship_crew), 116 | stat_to_string(enemy_ship_crew) 117 | ); 118 | print_separator(); 119 | } 120 | -------------------------------------------------------------------------------- /examples/space/README.md: -------------------------------------------------------------------------------- 1 | # Space 2 | 3 | In this example we'll discover how to manage the *space dimension* in weasel. 4 | 5 | Our space model will start as a two dimensional plane, divided in squares. We will then spawn a few creatures, each one on a different square. 6 | 7 | As the next step, deadly traps will be placed across the two diagonals. 8 | 9 | Finally, we are going to regenerate the space, transforming the 2D plane into a single line of squares; in other words we drop one dimension. 10 | 11 | Run the example with: 12 | ``` 13 | cargo run --example space 14 | ``` 15 | 16 | The program is implemented in two source code files: 17 | - [rules.rs](rules.rs): rules definition (space rules, in particular). 18 | - [main.rs](main.rs): manages the battle and creates a few events. 19 | -------------------------------------------------------------------------------- /examples/space/main.rs: -------------------------------------------------------------------------------- 1 | use crate::rules::*; 2 | use rules::BattlefieldSeed; 3 | use weasel::creature::CreatureId; 4 | use weasel::team::TeamId; 5 | use weasel::{ 6 | AlterSpace, Battle, BattleController, CreateCreature, CreateTeam, EventTrigger, ResetSpace, 7 | Server, 8 | }; 9 | 10 | mod rules; 11 | 12 | static TEAM_ID: TeamId = 1; 13 | static CREATURE_1: CreatureId = 1; 14 | static CREATURE_2: CreatureId = 2; 15 | static CREATURE_3: CreatureId = 3; 16 | 17 | fn main() { 18 | // Create a server to manage the battle. 19 | let battle = Battle::builder(CustomRules::new()).build(); 20 | let mut server = Server::builder(battle).build(); 21 | // Set the space model to be a 2D battlefield of squares. 22 | ResetSpace::trigger(&mut server) 23 | .seed(BattlefieldSeed::TwoDimensions) 24 | .fire() 25 | .unwrap(); 26 | // Display the space model. 27 | println!("Battlefield:\n{}", server.battle().space().model()); 28 | // Spawn three creatures. 29 | println!("Spawning three creatures..."); 30 | CreateTeam::trigger(&mut server, TEAM_ID).fire().unwrap(); 31 | // First creature goes in [1;0]. 32 | CreateCreature::trigger(&mut server, CREATURE_1, TEAM_ID, Square { x: 1, y: 0 }) 33 | .fire() 34 | .unwrap(); 35 | // Second creature goes in [3;3]. 36 | CreateCreature::trigger(&mut server, CREATURE_2, TEAM_ID, Square { x: 3, y: 3 }) 37 | .fire() 38 | .unwrap(); 39 | // Third creature goes in [4;3]. 40 | CreateCreature::trigger(&mut server, CREATURE_3, TEAM_ID, Square { x: 4, y: 3 }) 41 | .fire() 42 | .unwrap(); 43 | println!(); 44 | // Display the space model and the creatures. 45 | println!("Battlefield:\n{}", server.battle().space().model()); 46 | // Put traps on the squares across the diagonals. 47 | println!("Placing traps on the diagonals!"); 48 | AlterSpace::trigger( 49 | &mut server, 50 | vec![ 51 | Square { x: 0, y: 0 }, 52 | Square { x: 1, y: 1 }, 53 | Square { x: 2, y: 2 }, 54 | Square { x: 3, y: 3 }, 55 | Square { x: 4, y: 4 }, 56 | Square { x: 0, y: 4 }, 57 | Square { x: 1, y: 3 }, 58 | Square { x: 3, y: 1 }, 59 | Square { x: 4, y: 0 }, 60 | ], 61 | ) 62 | .fire() 63 | .unwrap(); 64 | assert_eq!(server.battle().entities().entities().count(), 2); 65 | println!(); 66 | // Display the space model and the creatures. Some of them died! 67 | println!("Battlefield:\n{}", server.battle().space().model()); 68 | // Now completely reset the space model, dropping one dimension. 69 | println!("Removing the y-axis!"); 70 | ResetSpace::trigger(&mut server) 71 | .seed(BattlefieldSeed::OneDimension) 72 | .fire() 73 | .unwrap(); 74 | println!(); 75 | // Display the space model and the creatures. Their positions have been adapted! 76 | println!("Battlefield:\n{}", server.battle().space().model()); 77 | } 78 | -------------------------------------------------------------------------------- /examples/status/README.md: -------------------------------------------------------------------------------- 1 | # Status 2 | 3 | In this example we will demonstrate how to implement different types of long lasting status effects. 4 | 5 | We will first create a creature and an object. Then, we inflict a status effect on the creature that will increase its health as long as it is active. The object, instead, will be dealt damage over time which will reduce its life at each turn. Finally, we will see how to end the effects manually or after a certain number of turns. 6 | 7 | Run the example with: 8 | ``` 9 | cargo run --example status 10 | ``` 11 | 12 | The program is implemented in two source code files: 13 | - [rules.rs](rules.rs): rules definition. 14 | - [main.rs](main.rs): manages the displayed messages and handles the battle server. 15 | -------------------------------------------------------------------------------- /examples/status/main.rs: -------------------------------------------------------------------------------- 1 | use crate::rules::*; 2 | use weasel::creature::CreatureId; 3 | use weasel::object::ObjectId; 4 | use weasel::team::TeamId; 5 | use weasel::{ 6 | Battle, BattleController, ClearStatus, CreateCreature, CreateObject, CreateTeam, EndTurn, 7 | EntityId, EnvironmentTurn, EventKind, EventTrigger, Id, InflictStatus, Server, StartTurn, 8 | }; 9 | 10 | mod rules; 11 | 12 | static TEAM_ID: TeamId = 1; 13 | static CREATURE_ID: CreatureId = 1; 14 | static OBJECT_ID: ObjectId = 2; 15 | static ENTITY_1_ID: EntityId = EntityId::Creature(CREATURE_ID); 16 | static ENTITY_2_ID: EntityId = EntityId::Object(OBJECT_ID); 17 | 18 | fn main() { 19 | // Create a server to manage the battle. 20 | let battle = Battle::builder(CustomRules::new()).build(); 21 | let mut server = Server::builder(battle).build(); 22 | // Create a team. 23 | CreateTeam::trigger(&mut server, TEAM_ID).fire().unwrap(); 24 | // Spawn a creature and an object, both with 50 HEALTH. 25 | println!("Spawning a creature..."); 26 | CreateCreature::trigger(&mut server, CREATURE_ID, TEAM_ID, ()) 27 | .statistics_seed(50) 28 | .fire() 29 | .unwrap(); 30 | println!("Spawning an object..."); 31 | CreateObject::trigger(&mut server, OBJECT_ID, ()) 32 | .statistics_seed(50) 33 | .fire() 34 | .unwrap(); 35 | // Display the entities' state. 36 | print_state(&server); 37 | // Inflict a power-up status effect on the creature, with no time limit. 38 | println!("Inflicting a power-up on the creature..."); 39 | InflictStatus::trigger(&mut server, ENTITY_1_ID, VIGOR) 40 | .potency((50, None)) 41 | .fire() 42 | .unwrap(); 43 | // Inflict a DoT status effect on the object, for two turns. 44 | println!("Inflicting a DoT on the object..."); 45 | InflictStatus::trigger(&mut server, ENTITY_2_ID, DOT) 46 | .potency((10, Some(2))) 47 | .fire() 48 | .unwrap(); 49 | // Display the entities' state. 50 | print_state(&server); 51 | // Do two full rounds. 52 | for i in 1..=2 { 53 | round(&mut server, i); 54 | } 55 | // The DoT should have been cleared automatically. 56 | // Remove the power-up manually. 57 | println!("Removing the creature power-up..."); 58 | ClearStatus::trigger(&mut server, ENTITY_1_ID, VIGOR) 59 | .fire() 60 | .unwrap(); 61 | print_state(&server); 62 | // Display the link between the DoT status and the effects it created. 63 | print_dot_effects(&server); 64 | } 65 | 66 | /// Performs a round. 67 | fn round(server: &mut Server, turn: u32) { 68 | // Display in which round we are. 69 | println!("Round {}", turn); 70 | println!(); 71 | // Start and end a turn for the creature. 72 | println!("Turn of Creature (1)..."); 73 | StartTurn::trigger(server, ENTITY_1_ID).fire().unwrap(); 74 | EndTurn::trigger(server).fire().unwrap(); 75 | // Do a turn for all non-actor entities, to update their statuses. 76 | println!("Turn of environment..."); 77 | EnvironmentTurn::trigger(server).fire().unwrap(); 78 | // Display the entities' state. 79 | print_state(server); 80 | } 81 | 82 | /// Displays briefly the state of all entities. 83 | fn print_state(server: &Server) { 84 | println!(); 85 | println!("------------------------- Entities -------------------------"); 86 | for character in server.battle().entities().characters() { 87 | let statuses: Vec<_> = character 88 | .statuses() 89 | .map(|status| match *status.id() { 90 | VIGOR => "vigor", 91 | DOT => "DoT", 92 | _ => unimplemented!(), 93 | }) 94 | .collect(); 95 | println!( 96 | "{:?} => health: {}, statuses: {:?}", 97 | character.entity_id(), 98 | character.statistic(&HEALTH).unwrap().value(), 99 | statuses 100 | ); 101 | } 102 | println!(); 103 | } 104 | 105 | fn print_dot_effects(server: &Server) { 106 | println!("Event derived from the DOT status:"); 107 | // We want to show the chain of events derived from the DOT status. 108 | // First find the event that put the DOT on the object. 109 | // We know it's first InflictStatus iterating in reverse order. 110 | let events = server.battle().history().events(); 111 | let inflict_event = events 112 | .iter() 113 | .rev() 114 | .find(|e| e.kind() == EventKind::InflictStatus) 115 | .unwrap(); 116 | println!("{:?}", inflict_event.event()); 117 | // Get all events with inflict_event as origin. 118 | for event in events { 119 | if event.origin() == Some(inflict_event.id()) { 120 | println!("+-- {:?}", event.event()); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /examples/status/rules.rs: -------------------------------------------------------------------------------- 1 | use weasel::rules::{statistic::SimpleStatistic, status::SimpleStatus}; 2 | use weasel::status::{Application, AppliedStatus, Potency, Status, StatusDuration, StatusId}; 3 | use weasel::{ 4 | battle_rules, rules::empty::*, AlterStatistics, BattleRules, BattleState, Character, 5 | CharacterRules, Entropy, EventQueue, EventTrigger, FightRules, Id, LinkedQueue, Transmutation, 6 | WriteMetrics, 7 | }; 8 | 9 | pub(crate) const HEALTH: u8 = 0; 10 | /// Id of the status that increases HEALTH. 11 | pub(crate) const VIGOR: u8 = 0; 12 | /// Id of the status that deals damage over time. 13 | pub(crate) const DOT: u8 = 1; 14 | 15 | // Declare the battle rules with the help of a macro. 16 | battle_rules! { 17 | EmptyTeamRules, 18 | // Use our own character rules to define how statuses are created. 19 | CustomCharacterRules, 20 | EmptyActorRules, 21 | // Use our own fight rules to specify the status' side effects. 22 | CustomFightRules, 23 | EmptyUserRules, 24 | EmptySpaceRules, 25 | EmptyRoundsRules, 26 | EmptyEntropyRules 27 | } 28 | 29 | // Define our custom character rules. 30 | #[derive(Default)] 31 | pub struct CustomCharacterRules {} 32 | 33 | impl CharacterRules for CustomCharacterRules { 34 | // Just use an integer as creature id. 35 | type CreatureId = u8; 36 | // Same for objects. 37 | type ObjectId = u8; 38 | // Use statistics with integers as both id and value. 39 | type Statistic = SimpleStatistic; 40 | // The seed will contain the value of HEALTH. 41 | type StatisticsSeed = i8; 42 | // We alter the HEALTH statistic in this example. 43 | // This represents the quantity to add to the current value. 44 | type StatisticsAlteration = i8; 45 | // Simple statuses with integers as both id and value. 46 | type Status = SimpleStatus; 47 | // We don't alter statuses in this example. 48 | type StatusesAlteration = (); 49 | 50 | fn generate_statistics( 51 | &self, 52 | seed: &Option, 53 | _entropy: &mut Entropy, 54 | _metrics: &mut WriteMetrics, 55 | ) -> Box> { 56 | // Generate a single statistic: HEALTH. 57 | let min = 0; 58 | let max = 100; 59 | let value = seed.unwrap(); 60 | let v = vec![SimpleStatistic::with_value(HEALTH, min, max, value)]; 61 | Box::new(v.into_iter()) 62 | } 63 | 64 | fn alter_statistics( 65 | &self, 66 | character: &mut dyn Character, 67 | alteration: &Self::StatisticsAlteration, 68 | _entropy: &mut Entropy, 69 | _metrics: &mut WriteMetrics, 70 | ) -> Option { 71 | // Apply the change to the character's health. 72 | let current = character.statistic(&HEALTH).unwrap().value(); 73 | character 74 | .statistic_mut(&HEALTH) 75 | .unwrap() 76 | .set_value(current + alteration); 77 | None 78 | } 79 | 80 | fn generate_status( 81 | &self, 82 | _character: &dyn Character, 83 | status_id: &StatusId, 84 | potency: &Option>, 85 | _entropy: &mut Entropy, 86 | _metrics: &mut WriteMetrics, 87 | ) -> Option> { 88 | // We expect to always have a valid potency. 89 | let potency = potency.unwrap(); 90 | let effect = potency.0; 91 | let duration = potency.1; 92 | // Return a new status in any case. If it already exists on the character, 93 | // the old one is replaced (anyway it doesn't happen in this example). 94 | Some(SimpleStatus::new(*status_id, effect, duration)) 95 | } 96 | } 97 | 98 | // Define our custom fight rules. 99 | #[derive(Default)] 100 | pub struct CustomFightRules {} 101 | 102 | impl FightRules for CustomFightRules { 103 | // We don't use impacts. 104 | type Impact = (); 105 | // Potency will tell how strong a status is and how long will it lasts. 106 | type Potency = (i8, Option); 107 | 108 | fn apply_status( 109 | &self, 110 | _state: &BattleState, 111 | character: &dyn Character, 112 | application: Application, 113 | event_queue: &mut Option>, 114 | _entropy: &mut Entropy, 115 | _metrics: &mut WriteMetrics, 116 | ) { 117 | // Treat all applications in the same way. We won't have replacements in this example. 118 | // So we only need to get the new status definition. 119 | let status = match application { 120 | Application::New(new) => new, 121 | Application::Replacement(_, new) => new, 122 | }; 123 | // If the status is VIGOR buff the character's HEALTH. 124 | if *status.id() == VIGOR { 125 | AlterStatistics::trigger(event_queue, *character.entity_id(), status.effect()).fire(); 126 | } 127 | } 128 | 129 | fn update_status( 130 | &self, 131 | _state: &BattleState, 132 | character: &dyn Character, 133 | status: &AppliedStatus, 134 | linked_queue: &mut Option>, 135 | _entropy: &mut Entropy, 136 | _metrics: &mut WriteMetrics, 137 | ) -> bool { 138 | // If the status is DOT deal some damage to the character. 139 | if *status.id() == DOT { 140 | AlterStatistics::trigger(linked_queue, *character.entity_id(), -status.effect()).fire(); 141 | } 142 | // Terminate the status if its duration expired. 143 | if let Some(max_duration) = status.max_duration() { 144 | status.duration() == max_duration 145 | } else { 146 | false 147 | } 148 | } 149 | 150 | fn delete_status( 151 | &self, 152 | _state: &BattleState, 153 | character: &dyn Character, 154 | status: &AppliedStatus, 155 | event_queue: &mut Option>, 156 | _entropy: &mut Entropy, 157 | _metrics: &mut WriteMetrics, 158 | ) { 159 | // If the status is VIGOR remove the buff. 160 | if *status.id() == VIGOR { 161 | AlterStatistics::trigger(event_queue, *character.entity_id(), -status.effect()).fire(); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /examples/undo/README.md: -------------------------------------------------------------------------------- 1 | # Undo 2 | 3 | In this example the player can move a creature on a two dimensional space. He will be able to undo or redo his moves. 4 | 5 | As you will see in the code, we use a cheap trick to implement the undo mechanics. That is, we replay the history up to the last completed turn.\ 6 | Doing so should be fine if your battles are quite short. In the case of complex and long fights replaying the history might take a not negligible amount of time. 7 | 8 | Run the example with: 9 | ``` 10 | cargo run --example undo 11 | ``` 12 | 13 | The program is implemented in two source code files: 14 | - [rules.rs](rules.rs): rules definition. 15 | - [main.rs](main.rs): manages the battle, the player input and implements the undo/redo mechanism. 16 | -------------------------------------------------------------------------------- /examples/undo/main.rs: -------------------------------------------------------------------------------- 1 | use crate::rules::*; 2 | use std::io::Read; 3 | use weasel::creature::CreatureId; 4 | use weasel::team::TeamId; 5 | use weasel::{ 6 | ActivateAbility, Battle, BattleController, CreateCreature, CreateTeam, EndTurn, EntityId, 7 | EventKind, EventReceiver, EventTrigger, Server, StartTurn, VersionedEventWrapper, 8 | }; 9 | 10 | mod rules; 11 | 12 | static TEAM_ID: TeamId = 0; 13 | static CREATURE_ID: CreatureId = 0; 14 | static ENTITY_ID: EntityId = EntityId::Creature(CREATURE_ID); 15 | 16 | fn main() { 17 | print_intro(); 18 | // The loop where the game progresses. 19 | game_loop(); 20 | // When this point is reached, the game has ended. 21 | println!(); 22 | println!("Goodbye!"); 23 | } 24 | 25 | fn print_intro() { 26 | println!("Undo"); 27 | println!(); 28 | println!("Example to demonstrate how to undo/redo player actions with weasel."); 29 | println!("Move around the soldier on the battlefield."); 30 | println!(); 31 | print_controls(); 32 | } 33 | 34 | fn print_controls() { 35 | println!(" Controls:"); 36 | println!(" w - Move up"); 37 | println!(" s - Move down"); 38 | println!(" d - Move right"); 39 | println!(" a - Move left"); 40 | println!(" u - Undo"); 41 | println!(" r - Redo"); 42 | println!(" h - Display the controls"); 43 | println!(" q - Quit"); 44 | } 45 | 46 | fn game_loop() { 47 | // Create a server. 48 | let mut server = create_game(); 49 | // Create a buffer to keep events for redo. 50 | let mut event_buffer = Vec::new(); 51 | println!(); 52 | display_world(&server); 53 | // Main loop. 54 | loop { 55 | // Read a char from stdin. 56 | let input: Option = std::io::stdin() 57 | .bytes() 58 | .next() 59 | .and_then(|result| result.ok()) 60 | .map(|byte| byte as char); 61 | // Take an action depending on the user input. 62 | if let Some(key) = input { 63 | match key { 64 | 'w' => { 65 | walk(&mut server, &mut event_buffer, Direction::Up); 66 | display_world(&server); 67 | } 68 | 's' => { 69 | walk(&mut server, &mut event_buffer, Direction::Down); 70 | display_world(&server); 71 | } 72 | 'd' => { 73 | walk(&mut server, &mut event_buffer, Direction::Right); 74 | display_world(&server); 75 | } 76 | 'a' => { 77 | walk(&mut server, &mut event_buffer, Direction::Left); 78 | display_world(&server); 79 | } 80 | 'u' => { 81 | server = undo(server, &mut event_buffer); 82 | display_world(&server); 83 | } 84 | 'r' => { 85 | redo(&mut server, &mut event_buffer); 86 | display_world(&server); 87 | } 88 | 'h' => print_controls(), 89 | 'q' => break, 90 | _ => {} 91 | } 92 | } 93 | } 94 | } 95 | 96 | fn display_world(server: &Server) { 97 | // Display the number of steps taken and the space model. 98 | let steps = server 99 | .battle() 100 | .history() 101 | .events() 102 | .iter() 103 | .filter(|e| e.kind() == EventKind::ActivateAbility) 104 | .count(); 105 | let battlefield = server.battle().space().model(); 106 | println!("Steps: {}\nBattlefield:\n{}", steps, battlefield); 107 | } 108 | 109 | /// Creates a new server 110 | fn create_server() -> Server { 111 | let battle = Battle::builder(CustomRules::new()).build(); 112 | Server::builder(battle).build() 113 | } 114 | 115 | /// Creates a new game: a server with a team and a creature. 116 | fn create_game() -> Server { 117 | let mut server = create_server(); 118 | // Create a team and a creature. 119 | CreateTeam::trigger(&mut server, TEAM_ID).fire().unwrap(); 120 | CreateCreature::trigger(&mut server, CREATURE_ID, TEAM_ID, Square { x: 0, y: 0 }) 121 | .fire() 122 | .unwrap(); 123 | server 124 | } 125 | 126 | /// Moves the creature on step towards the given direction. 127 | fn walk( 128 | server: &mut Server, 129 | event_buffer: &mut Vec>, 130 | direction: Direction, 131 | ) { 132 | // Clean the buffered events to invalidate the redo action. 133 | event_buffer.clear(); 134 | // Start a turn. 135 | StartTurn::trigger(server, ENTITY_ID).fire().unwrap(); 136 | // Activate the 'walk' ability of the creature. 137 | let result = ActivateAbility::trigger(server, ENTITY_ID, WALK) 138 | .activation(direction) 139 | .fire(); 140 | // We print the error in case the movement is not allowed. 141 | if let Err(e) = result { 142 | println!("{:?}", e.unfold()); 143 | } 144 | // End the turn. 145 | EndTurn::trigger(server).fire().unwrap(); 146 | } 147 | 148 | /// Undo the last action. 149 | fn undo( 150 | server: Server, 151 | event_buffer: &mut Vec>, 152 | ) -> Server { 153 | // Retrieve the last event of type ActivateAbility. 154 | let last_activation_index = server 155 | .battle() 156 | .history() 157 | .events() 158 | .iter() 159 | .rposition(|e| e.kind() == EventKind::ActivateAbility); 160 | match last_activation_index { 161 | Some(last_activation_index) => { 162 | // We are gonna undo this turn. 163 | // First save the current history in a buffer, if it's empty. If it's not, it means 164 | // we are already undoing a series of events. 165 | if event_buffer.is_empty() { 166 | let mut events = server 167 | .battle() 168 | .versioned_events(std::ops::Range { 169 | start: 0, 170 | end: server.battle().history().len() as usize, 171 | }) 172 | .collect(); 173 | event_buffer.append(&mut events); 174 | } 175 | // Create a completely new server. 176 | let mut server = create_server(); 177 | // Replay history up to the last ActivateAbility before 'last', to skip turns in 178 | // which the player did a wrong move. 179 | // To nicely wrap the turn we should undo also the StartTurn event. 180 | let previous_start_turn_index = &event_buffer[..last_activation_index] 181 | .iter() 182 | .rposition(|e| e.kind() == EventKind::StartTurn); 183 | // We replay all events in the buffer up to the start turn (excluded). 184 | // There will always be a StartTurn before an ActivateAbility. 185 | for event in event_buffer 186 | .iter() 187 | .take(previous_start_turn_index.unwrap() as usize) 188 | { 189 | server.receive(event.clone()).unwrap(); 190 | } 191 | server 192 | } 193 | None => { 194 | // No single action was taken yet. We can't undo anything, so return the same server. 195 | server 196 | } 197 | } 198 | } 199 | 200 | /// Redo the last undoed action. 201 | fn redo( 202 | server: &mut Server, 203 | event_buffer: &mut Vec>, 204 | ) { 205 | let history_len = server.battle().history().events().len(); 206 | if event_buffer.len() > history_len { 207 | // There are some events to redo. 208 | // It's enough to replay the missing events on top of the existing server. 209 | // Let's first find the next ActivateAbility. 210 | let future_events = &event_buffer[history_len..]; 211 | let next_activation = future_events 212 | .iter() 213 | .position(|e| e.kind() == EventKind::ActivateAbility); 214 | // However, since we want to redo an entire turn, replay up to the EndTurn (included). 215 | if let Some(next_activation) = next_activation { 216 | // Find the EndTurn immediately after 'next_activation'. 217 | let end_turn = &future_events[next_activation..] 218 | .iter() 219 | .position(|e| e.kind() == EventKind::EndTurn) 220 | .unwrap(); 221 | // Add 'next_activation' index to get the index of 'end_turn' in 'future_events'. 222 | let end_turn = end_turn + next_activation; 223 | // Replay events up 'end_turn' (included). 224 | for event in &future_events[..=end_turn] { 225 | server.receive(event.clone()).unwrap(); 226 | } 227 | } 228 | } 229 | // Nothing to redo. 230 | } 231 | -------------------------------------------------------------------------------- /examples/undo/rules.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "serialization")] 2 | use serde::{Deserialize, Serialize}; 3 | use std::convert::TryInto; 4 | use std::fmt::{Display, Formatter, Result}; 5 | use weasel::rules::ability::SimpleAbility; 6 | use weasel::{ 7 | battle_rules, rules::empty::*, Action, ActorRules, BattleRules, BattleState, Entropy, 8 | EventQueue, EventTrigger, MoveEntity, PositionClaim, SpaceRules, WeaselError, WeaselResult, 9 | WriteMetrics, 10 | }; 11 | 12 | /// Length of each dimension of the battlefield. 13 | const BATTLEFIELD_LENGTH: usize = 5; 14 | 15 | /// Id of the only ability in this game. 16 | pub const WALK: u32 = 1; 17 | 18 | // Use the `battle_rules` macro to quickly create an object that implements 19 | // the `BattleRules` trait. 20 | battle_rules! { 21 | // No special behavior for teams. 22 | EmptyTeamRules, 23 | // No need for creatures to have statistics. 24 | EmptyCharacterRules, 25 | CustomActorRules, 26 | // Creatures don't fight in this example. 27 | EmptyFightRules, 28 | // We don't use user defined metrics or events. 29 | EmptyUserRules, 30 | CustomSpaceRules, 31 | // We handle rounds manually. 32 | EmptyRoundsRules, 33 | // No randomness at all. 34 | EmptyEntropyRules 35 | } 36 | 37 | // We define our own space rules. 38 | #[derive(Default)] 39 | pub struct CustomSpaceRules {} 40 | 41 | impl SpaceRules for CustomSpaceRules { 42 | // A square with two coordinates. 43 | type Position = Square; 44 | // We always initialize the space in the same way, so no seed. 45 | type SpaceSeed = (); 46 | // Our space model. 47 | type SpaceModel = Battlefield; 48 | // In this example we don't alter the space. 49 | type SpaceAlteration = (); 50 | 51 | fn generate_model(&self, _seed: &Option) -> Self::SpaceModel { 52 | Battlefield::new() 53 | } 54 | 55 | fn check_move( 56 | &self, 57 | _model: &Self::SpaceModel, 58 | _claim: PositionClaim, 59 | position: &Self::Position, 60 | ) -> WeaselResult<(), CustomRules> { 61 | // An entity can move into a square if it exists. 62 | // We don't check if the square is occupied because we know there will be only one entity. 63 | if position.valid() { 64 | Ok(()) 65 | } else { 66 | Err(WeaselError::UserError(format!( 67 | "invalid position: {}", 68 | position 69 | ))) 70 | } 71 | } 72 | 73 | fn move_entity( 74 | &self, 75 | model: &mut Self::SpaceModel, 76 | claim: PositionClaim, 77 | position: Option<&Self::Position>, 78 | _metrics: &mut WriteMetrics, 79 | ) { 80 | if let Some(position) = position { 81 | match claim { 82 | PositionClaim::Spawn(_) => model.insert(*position), 83 | PositionClaim::Movement(entity) => model.change(*entity.position(), *position), 84 | } 85 | } 86 | // In this example the entity never leaves the battlefield, thus we don't care about the 87 | // else condition. 88 | } 89 | } 90 | 91 | /// Position for entities. It contains the coordinates of a square. 92 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 93 | #[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))] 94 | pub struct Square { 95 | pub x: i8, 96 | pub y: i8, 97 | } 98 | 99 | impl Square { 100 | /// Returns another square which represents the same position after taking one step in 101 | /// the given direction. 102 | fn one_step_towards(self, dir: Direction) -> Self { 103 | match dir { 104 | Direction::Up => Self { 105 | x: self.x, 106 | y: self.y + 1, 107 | }, 108 | Direction::Down => Self { 109 | x: self.x, 110 | y: self.y - 1, 111 | }, 112 | Direction::Right => Self { 113 | x: self.x + 1, 114 | y: self.y, 115 | }, 116 | Direction::Left => Self { 117 | x: self.x - 1, 118 | y: self.y, 119 | }, 120 | } 121 | } 122 | 123 | fn valid(self) -> bool { 124 | let max = BATTLEFIELD_LENGTH.try_into().unwrap(); 125 | let min = 0; 126 | self.x < max && self.x >= min && self.y < max && self.y >= min 127 | } 128 | } 129 | 130 | impl Display for Square { 131 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 132 | write!(f, "x: {}, y: {}", self.x, self.y) 133 | } 134 | } 135 | 136 | /// The space model for this game. 137 | pub struct Battlefield { 138 | // A simple 2D battlefield. We only store if the square is occupied or not. 139 | squares: [[bool; BATTLEFIELD_LENGTH]; BATTLEFIELD_LENGTH], 140 | } 141 | 142 | impl Battlefield { 143 | /// Creates a battlefield. 144 | fn new() -> Self { 145 | Self { 146 | squares: [[false; BATTLEFIELD_LENGTH]; BATTLEFIELD_LENGTH], 147 | } 148 | } 149 | 150 | /// Marks one square as occupied. 151 | fn insert(&mut self, square: Square) { 152 | self.squares[square.y as usize][square.x as usize] = true; 153 | } 154 | 155 | /// Moves an entity from one square to another. 156 | fn change(&mut self, old: Square, new: Square) { 157 | self.squares[old.y as usize][old.x as usize] = false; 158 | self.insert(new); 159 | } 160 | } 161 | 162 | impl Display for Battlefield { 163 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 164 | // Iterate over the arrays and print the entity position. 165 | for (_, row) in self.squares.iter().rev().enumerate() { 166 | for (_, col) in row.iter().enumerate() { 167 | write!(f, "|")?; 168 | if *col { 169 | write!(f, "X")?; 170 | } else { 171 | write!(f, " ")?; 172 | } 173 | } 174 | write!(f, "|")?; 175 | writeln!(f)?; 176 | } 177 | Ok(()) 178 | } 179 | } 180 | 181 | // Define our custom actor rules. 182 | #[derive(Default)] 183 | pub struct CustomActorRules {} 184 | 185 | impl ActorRules for CustomActorRules { 186 | // Abilities will have fixed value. 187 | type Ability = SimpleAbility; 188 | // No need for a seed. Same abilities for everyone. 189 | type AbilitiesSeed = (); 190 | // Our single ability needs to know the direction of movement. 191 | type Activation = Direction; 192 | // Abilities can't be altered in our game. 193 | type AbilitiesAlteration = (); 194 | 195 | fn generate_abilities( 196 | &self, 197 | _seed: &Option, 198 | _entropy: &mut Entropy, 199 | _metrics: &mut WriteMetrics, 200 | ) -> Box> { 201 | // We always generate a single ability, 'walk'. 202 | let v = vec![SimpleAbility::new(WALK, ())]; 203 | Box::new(v.into_iter()) 204 | } 205 | 206 | fn activable( 207 | &self, 208 | _state: &BattleState, 209 | action: Action, 210 | ) -> WeaselResult<(), CustomRules> { 211 | // The ability can be activated only if the destination exists. 212 | if let Some(dir) = action.activation { 213 | let destination = action.actor.position().one_step_towards(*dir); 214 | if destination.valid() { 215 | Ok(()) 216 | } else { 217 | Err(WeaselError::UserError(format!( 218 | "invalid destination: {}", 219 | destination 220 | ))) 221 | } 222 | } else { 223 | Err(WeaselError::UserError("missing activation!".to_string())) 224 | } 225 | } 226 | 227 | fn activate( 228 | &self, 229 | _state: &BattleState, 230 | action: Action, 231 | event_queue: &mut Option>, 232 | _entropy: &mut Entropy, 233 | _metrics: &mut WriteMetrics, 234 | ) { 235 | // To activate our only ability (walk) we just need to fire a MoveEntity event. 236 | let entity_id = *action.actor.entity_id(); 237 | // Since this ability is activable, 'activation' will be set. 238 | let direction = action.activation.unwrap(); 239 | // We also know that the new position is valid. 240 | let position = action.actor.position().one_step_towards(direction); 241 | MoveEntity::trigger(event_queue, entity_id, position).fire(); 242 | } 243 | } 244 | 245 | #[derive(Clone, Copy, Debug)] 246 | #[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))] 247 | pub enum Direction { 248 | Up, 249 | Down, 250 | Right, 251 | Left, 252 | } 253 | -------------------------------------------------------------------------------- /examples/user_event/README.md: -------------------------------------------------------------------------------- 1 | # User event 2 | 3 | This example is a small program that shows how use user defined events and metrics. 4 | 5 | First, we define our own `UserRules`, a custom event `MakePizza`. Then we create a `server` and fire two `MakePizza` events.\ 6 | Before exiting, the program prints to the terminal the json serialized content of the battle history. 7 | 8 | Run the example with: 9 | ``` 10 | cargo run --example user-event --all-features 11 | ``` 12 | 13 | The program is implemented in two source code files: 14 | - [rules.rs](rules.rs): rules definition (user rules, in particular). 15 | - [main.rs](main.rs): manages the battle and creates a few events. 16 | -------------------------------------------------------------------------------- /examples/user_event/main.rs: -------------------------------------------------------------------------------- 1 | use crate::rules::*; 2 | use weasel::{Battle, BattleController, EventTrigger, FlatVersionedEvent, Server}; 3 | 4 | mod rules; 5 | 6 | fn main() { 7 | // Create a server which will manage the battle. 8 | let battle = Battle::builder(CustomRules::new()).build(); 9 | let mut server = Server::builder(battle).build(); 10 | // Fire two MakePizza events. 11 | MakePizza::trigger(&mut server, "margherita".to_string()) 12 | .fire() 13 | .unwrap(); 14 | MakePizza::trigger(&mut server, "diavola".to_string()) 15 | .fire() 16 | .unwrap(); 17 | // Check that our custom metric is working correctly. 18 | assert_eq!( 19 | server 20 | .battle() 21 | .metrics() 22 | .user_u64(PIZZAS_CREATED_METRIC.to_string()), 23 | Some(2) 24 | ); 25 | // Print the serialized history. 26 | let events: Vec> = server 27 | .battle() 28 | .versioned_events(std::ops::Range { 29 | start: 0, 30 | end: server.battle().history().len() as usize, 31 | }) 32 | .map(|e| e.into()) 33 | .collect(); 34 | println!("History:\n {}", serde_json::to_string(&events).unwrap()); 35 | } 36 | -------------------------------------------------------------------------------- /examples/user_event/rules.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::any::Any; 3 | use weasel::{ 4 | battle_rules, battle_rules_with_user, rules::empty::*, Battle, BattleRules, Event, EventKind, 5 | EventProcessor, EventQueue, EventTrigger, UserEventPacker, UserRules, WeaselError, 6 | WeaselResult, 7 | }; 8 | 9 | pub(crate) const PIZZAS_CREATED_METRIC: &str = "pizzas_created"; 10 | 11 | // It's not a real game so we can use generic no-op battle rules. 12 | // We still want to override the UserRules to define how to serialize our custom event and to 13 | // add custom metrics. 14 | battle_rules_with_user! { CustomUserRules } 15 | 16 | // Define our own user rules in order to have custom metrics and custom events. 17 | #[derive(Default)] 18 | pub struct CustomUserRules {} 19 | 20 | impl UserRules for CustomUserRules { 21 | // For our metrics we'll use a String id. 22 | type UserMetricId = String; 23 | // The type we will use to serialize and deserialize all user events. 24 | type UserEventPackage = EventPackage; 25 | } 26 | 27 | /// An user defined event. 28 | #[derive(Clone, Debug, Serialize, Deserialize)] 29 | pub struct MakePizza { 30 | // A simple data field containing the pizza's name. 31 | name: String, 32 | } 33 | 34 | impl MakePizza { 35 | /// Returns a trigger for this event. 36 | /// Triggers are not strictly required, but they offer a convenient way to fire events. 37 | pub(crate) fn trigger>( 38 | processor: &mut P, 39 | name: String, 40 | ) -> MakePizzaTrigger

{ 41 | MakePizzaTrigger { processor, name } 42 | } 43 | } 44 | 45 | impl Event for MakePizza { 46 | fn verify(&self, _battle: &Battle) -> WeaselResult<(), CustomRules> { 47 | // You should put here all the logic needed to verify if the event can be applied or not. 48 | // For the sake of the example the event is always accepted. 49 | Ok(()) 50 | } 51 | 52 | fn apply( 53 | &self, 54 | battle: &mut Battle, 55 | _event_queue: &mut Option>, 56 | ) { 57 | // In this method you can modify the battle state or even fire other events. 58 | // In this example the event does nothing except increasing a metric. 59 | let mut writer = battle.metrics_mut(); 60 | writer 61 | .add_user_u64(PIZZAS_CREATED_METRIC.to_string(), 1) 62 | .unwrap(); 63 | } 64 | 65 | fn kind(&self) -> EventKind { 66 | // This user event has id 0. If you add a second user event, it should have another id. 67 | EventKind::UserEvent(0) 68 | } 69 | 70 | fn box_clone(&self) -> Box + Send> { 71 | Box::new(self.clone()) 72 | } 73 | 74 | fn as_any(&self) -> &dyn Any { 75 | self 76 | } 77 | } 78 | 79 | /// Trigger to build and fire a `MakePizza` event. 80 | pub(crate) struct MakePizzaTrigger<'a, P> 81 | where 82 | P: EventProcessor, 83 | { 84 | processor: &'a mut P, 85 | name: String, 86 | } 87 | 88 | impl<'a, P> EventTrigger<'a, CustomRules, P> for MakePizzaTrigger<'a, P> 89 | where 90 | P: EventProcessor, 91 | { 92 | fn processor(&'a mut self) -> &'a mut P { 93 | self.processor 94 | } 95 | 96 | /// Returns a `MakePizza` event. 97 | fn event(&self) -> Box + Send> { 98 | Box::new(MakePizza { 99 | name: self.name.clone(), 100 | }) 101 | } 102 | } 103 | 104 | /// Type to serialize and deserialize user event. 105 | #[derive(Serialize, Deserialize)] 106 | pub(crate) enum EventPackage { 107 | MakePizza(MakePizza), 108 | } 109 | 110 | impl UserEventPacker for EventPackage { 111 | /// In this method we extract an event trait object out of a packaged user event. 112 | fn boxed(self) -> WeaselResult + Send>, CustomRules> { 113 | let event = match self { 114 | Self::MakePizza(event) => Box::new(event) as Box + Send>, 115 | }; 116 | Ok(event) 117 | } 118 | 119 | /// This method packages a boxed user event into an instance of EventPackage. 120 | fn flattened(event: Box + Send>) -> WeaselResult { 121 | match event.as_any().downcast_ref::() { 122 | Some(event) => Ok(Self::MakePizza(event.clone())), 123 | None => Err(WeaselError::UserEventPackingError( 124 | event.clone(), 125 | "bad cast".into(), 126 | )), 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /resources/client.drawio: -------------------------------------------------------------------------------- 1 | 7VnbctowEP0aHunYGIx5LARI0iTTDmnT9KUjbGEryBaVxS1f3xWWbxgMFBIySR+S8a7XK1l7zlnJVIyOv+hzNPFumYNppaY5i4pxUanVmpYJ/6VjGTnMVj1yuJw4kUtPHQPyjJVTU94pcXCYCxSMUUEmeafNggDbIudDnLN5PmzEaH7UCXJxwTGwES16H4gjvMhr1Zqp/xIT14tH1s1WdMdHcbB6k9BDDptnXEa3YnQ4YyK68hcdTOXaxevycLV8oDdjs3/9LfyDvre/3N/9qEbJeoc8krwCx4E4bepalHqG6FStl3pXsYwXkLNp4GCZRKsYbU/4FC51uHzCQixVwdFUMHAxLjzmsgDRG8YmKm7EAqHCdGnjwPksCwv2kDJ7HLl6hFI1Blgq3gIrFJyNk9rJBEkhZDBFQ0zbyB67q4l2GGUcbgUswDKVA2BQ75JOrpt6YTDBlz9lsk+N2HxMJgLGxSJnLZW1Z01U7UI25bZaUIveXFfvf3et6q9n3w37t9fOoKrgKBB3sSiJa0Rx8r0y+FYV72PmY5gkBHBMkSCzPAmQ4pKbxCWPfmUE3qOmKdrXNAV6xXojJkGcIpqoeipFHVxkppG6Vlg8AJdGAZcdjpHAcgbwN8B8hvmABGMw2PBJCsd23ErMzD0i8GCCVkWYg9LlsZzFKKxJ26UoDFWldwDwMCDAtAVelJYuvlvLl0A3lT1PZSzxeRkJq2vbq52rU0lRNmKvqA276ZVXj436sEFHMpU5jp9rPDkhYbUiYbcj+Qh+HlUxrZxGbSQExW+FQgVq7FutvSlknZtB8aboP4UyvWxnz2udk0KNEgoFjlwOzmwMSIeVmsG6hO+eRcle4Gw0apbLWocSvNrMfBBZSxb6bAWxCgUpWfR3eYw4oVaWbYh3amXzSK3c7ziQ2HGKaP6F48A5zxWbG3BRzh9xUbI/cFNu7Qk0vb4ZaQlCojz/Crw4hI1GIX4RKLQKSLiSTVx4so24yF/ti0G8K0avKGYe84fTcHf3yGmMVLAe8gmVa3SJ6QwLYqMNPQZR4gZg2FBSOYFN4gVDksAFy0yt+xX2qvVX7D11bc/eo68z/XSbarNQyTt2XkrnCJ3yewul43JTPBLy5oKIJAtcZ5KAleaQxltThUYpeKraJ63ZsHIASs5EhykFtH60zARMpG6E2ztYaw225trH0vJwo66twTQa/9VFS68XsN5dnUKysvXOt7uNF/wQBmb6QT8qWvqriNH9Cw== -------------------------------------------------------------------------------- /resources/client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trisfald/weasel/756cfd85da5b6fc03e3880e3be2b966980ceb209/resources/client.png -------------------------------------------------------------------------------- /resources/event.drawio: -------------------------------------------------------------------------------- 1 | 7VzbkqO2Fv0a18l5mC5AgPFj305mUkmlKz2TZJ5SMgibGoyIkLvtfH0kkLARwsY2l+45nodpEEIIae2ltbc2noD71eZHAtPlLzhA8cQygs0EPEwsy3SBy/7wkq0omc7somRBokCU7Qqeo3+QKDRE6ToKUFapSDGOaZRWC32cJMinlTJICH6tVgtxXH1qCheoVvDsw7he+kcU0GVR6jnGrvwjihZL+WTTEFdWUFYWBdkSBvh1rwg8TsA9wZgWR6vNPYr56MlxKe77X8PVsmMEJbTNDan1EzTj1y+//wq8T/dPPz5+pOgDKFp5gfFavPDEcmPW3l0QvfBO060YCffvNe/pXYgT+iHL5+mWVTCddLO7yI4W4m8sK1/eyrzbZl8RzBhMWQ1w+4zICyKyXTZ4RdPVx7HiWheaq+YDJ0utSjctgtdJgPiEGLwjy4ii5xT6/OorMyBWtqQr1rUHk79MFMf3OMYkvxcEEHmhz8ozSvA3tHfF9T00D9kVGEeLhJXFKOSPj+EcxU84i2iEebHPkMJeFtyxV6YRw/jPSoVVFAS8q2WFW9EgxWn5YGkGVvmCvDbaNMLSLMHOaALhFaJky6qIGz54wj4kQ7ji/HVnbq40quWeqQFZEQoTX5Rt76yAHQhDOMEo7EajePN4vo8jlKPyYjx3iVwHeYGtQ65nzYHr9oncOaYUr/oC78w6Dl7b04C3LOwcvO77Be8dpJS1MTJ4FSgqWA7D0PK1LBy4c9dxhyVP07AVAHp1AAJbB0C7LwBOD0uKdyAQChz+to6Z8OxNGfQ8JAO3YulaeUBhlCCu2Jf8/wVctTBuXXHH81s2S9rP3RgCUDJRufqpXOT5SM9Fc8+xHUPLOio7lStnJ3TkVOkIGHU6Mi0NHfWm5UyzkY7OR5WtQ9UnilZZ/qrsTS2DsFm2jDn0vy3ySecu4Trj7c+5MUSrNEYrPrF5tW1pJesMkZsLEaqAjk0orSKrCpgEJ0hBlyhS1sJG7OiAXIX6JegSYJrqxBXQrW29gcnSgEkZ7GM2naVFmCKMNnxwujDyHrww7VgPOtTNkYnWZgp0ZvqZwCRLMeFN+EuYJHnE6ofP908Ti3XV+O3pvjh4ilJUHCHq/3fCWaud2qxz7r7lLWHK6602Cx4xuwlj/Mq6QehNEBGGjL8CSGGDRR2DCsr/6aACXDADwSH36GSPq1C1HUCvdO4F9MqA2R70tNjrb83oIABwxd5R7BUIOhl4JWR7wJ6t0SsDY885xX3qWBjLaKjxHCXfztPEe518RknABRF6YbPBDyguVU4mnjSyPG4UNL146559nOnKCNIwaLtGizry0UgxWx2Ei/qNWJrgeMDImurC7VZvIJwNBcIvWc5uUZKuyxi51sU6GTM132p/2sp1FyZrGH/Kn37MZ8pbKnZDrTxATnyxOWp2FRr4xrTFsgP3TN5gKfQ21dGbbient1i47MAlyNJGtviVfS565EvcE8EU061UbguUIAL3/HuYbOVqyNdBxhiL3dZjC09flXoCVWGMNrd8p5tVYiuuOHzwY5hlkV9lK7SJ6J98ym8ccfZVAIAfP2z2T7bipMZovj+bHRR6mIE6ovx+b4ctFNQ22hVksbfDa+KjFlRBIVkgemjmwWF317iZ2jI3YQ+ejgadsoygGNLopfoSOsiKpz7hKJ/oMjY2rZqHYyqwL95e3LZDfr0lU9l1slV9UAzPgZZkRRyGGarUye2sHLALTK/uv3/OlZ/Ef5QVV6NA2AhcwCjJA2SFRIwxWxQnMmzMkM/q1YyA9SRKs6YQ1EFWPhDuKlm15EizK46U+l5KQFPDkToU9seROme3A6o5nTbYGJLtn5Kp+MnXHVXx0x0/5WeSoEpS26e0kuD0pNYfRUngH6WoAgl1qAxER6bq+k7PpiMl6G+rSrGBjjqjmpNc5jfp11x3HK87jtcdx3MWVMXnKPeDRttwBHWfQ7idYvsvRLncoRU1dEpozLxU7YjlOJ/IuuQZLH5bEVcLriEESMtM2Y48U0fRy7q4my5JqzeMWLqIxziaS2gn6Ry20U785AmRiI0Fn+idIjtDu/XoKrbVYWA2pg6bTjuSYZ7qFA6swkA97+s03vNF6uqb571L90wH4z11v0G3uzUo7wHvFKXesQaTqdH9b24dRfL3uLlltwDbsJtboL7KXkNQljlTxNDoESi7nqJX2Op/+PzANZuLhAGZywQ5Z0x9hBGfsvc9Fx+cNzcXurSrHpWpov/OCeU1L9wdqknQduPBbpjxYdRk+U3UpWrSNsdVk3Y9Kv24z9owTeOcAAwmrP6f6BroNlWHpQhduHV4ijjJY5XeaXnSzjttppZc+t+Vyd0KKhYEBnwBe8jT6grtn2DC56u8ttfq9PHWfezY/XXaur+FpY22DaF6rWdviip+dO3r0L4JS5dJNaBVnInwc6yp2SreAO7lwnE87OONiXtr1hHugT0y7nsKZR4m2FPlZCXdxbwQ3ToE65CPYEY7JvVpS3A7xmREcIOuSB2MTOpyPK4q1HCreSvO6J6qU/dUK3OTErzCRViHoYMH5+CkmrT3u4giiNve9wypoYTx/QSnJ0U02sqw01g7WfV1/9rJXkSXukf+ONLRpaEhM3Ig3eN1tTSAkZeG+nbXxyijOB9LHycZO0GJ/92FLGtprGCmIRrdJ1P9EY1uV+mtEU23DlZbCSqdrw6JRu7hHSea6ZhEU/uyD5xJNDXA11rqmWnc5l8wuH4eM3k3n8eUH1WN9jWMa43Bk10Kr2py0dmySwFDH2Eot60cc8cNvyrRI+dsllTjWSqK+ybJDn4uQptR/CjzO3yCRJZA8bF8vK26/TJtkzv8+Qf+IXNA81kj6CCZHiRODsY8i2hyODukMdeDIPaGcJ63xzGe8hnI58S5mzgPh7hN/KSquHlSZk3u28MBoml2VY0bKSK2lQdeCuZqm1b1/v4+dXLre5XXPJM3mGfi6n6ZbrT177te/dpuwowcjFCyJWqpaK2DEWYD1i9e/djp7heii+q7H9oGj/8C -------------------------------------------------------------------------------- /resources/event.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trisfald/weasel/756cfd85da5b6fc03e3880e3be2b966980ceb209/resources/event.png -------------------------------------------------------------------------------- /resources/server.drawio: -------------------------------------------------------------------------------- 1 | 7Vpbd+I2EP41nNM+sMfGYMxjICS7aXZPe0ibbl96hC1sLbJFZXHLr1/Jlq9ywAQcb7J9CJHGsi4z3zczktwxJv7uloKV95k4EHd6mrPrGNedXm9omfxXCPaxwBz1Y4FLkROL9EwwQ09QCjUpXSMHhoWGjBDM0KootEkQQJsVZIBSsi02WxBcHHUFXKgIZjbAqvQROcyLpVZvmMk/QuR6yci6OYqf+CBpLFcSesAh25zImHaMCSWExSV/N4FY6C7Ry+On/SO+X5q3d3+E/4E/x789fPmrG3d2c8or6RIoDNhlu+7FXW8AXkt9ybWyfaJAStaBA0UnWscYe8zHvKjz4jfI2F4aHKwZ4SJCmUdcEgB8T8hKtluQgMlmuqjDwLkShuX1OSb2MhbdIIzlGLwm21u8FjJKlqntRAepIURjDOYQj4G9dKOJTggmlD8KSABFVw4Hg1xLNrlpJh3X1K20QUjW1JaKsfD9Xffh36nV/efJd8Pbz3fOrCthxQB14aH+JKvE/HI4lZa7hcSHjO55AwoxYGhTBDOQnHDTdpndeUGa/gQYGAoMJhQCBsVY/G8G6QZSXiDzb4Kjz0NEmGfrIQZnKxDpacudShE2eTjwFY9dDMJQGvOIrU+zFZ8yg7uD2k2eJhSXPk43ZX2beYxU5uW8RV9ryCB9xSBfyM/GTG5kuv9bdPZhkFS/phPhletdobZ/EUryjD7E1DyjK5k/qMloibEEcrUJLnv6nSC+rKwJWSxCyIqeP2mzS4YqwrtvlWAbr02+VUJuOvOXg3mggPkrzwjKaD4OiDeN9+ZRWdlueBoq9cZQeSkwmQqYPoVCEZ4IVi7wxT9/LfJLDPZx0IpjVwDZllBu4xvVkXrEn6/D47GrYG+BphvgIyx09xHiDWTIBhURDmDkBrxic9PzqVQCiQ+JApfXzKz2EAGXx4LXi3xGr2bkS188J/RV+tH+2Z6hkvsVPiJn2PNiTYldF6R5RTpZqTSjzexxqFDyijFge4J7geAjEfzzCRXlCUZQ0CNYqiHgreeRhlli02vmkZW4ULdzPweZKpWh1YyZr0WmQ5N8bis2Boxh+KNsxRRq1LVW7YBktc0gs1UG6QX+ZHRqiUFGzXA0OJNB0as8NQf7XIOVyDHDiqRTQqffL0Jn0NdK1o97fGk6ekgjObrOPBAFujgjnSd89VDISKSFLWJe+tyOwmHbofDyPC6HwtQ4rRE52dr8HwtzpxVHmTxqMxaqBwdZLAwcoQ5KbBiK/R/cvEsalcPhoO7+rDEaqcn+DLL1KnVoIYcUE1vZqBjZiizKG/RfmMct43qp4X5995brte4ALcVyB5T+Bk/Y6t71NLdbr5seDS+RHh095k3rSRfx/JVj3qMdGeWznsudF1dH6pZ3rT9Wzj16zZxbwYJVnVMfg8KpuXtpGNMs3WXXm1Wjmb6upvoVd3ICB/fCTRXBpRwBi4CDbICv5AMfOU6MYxiiJzCP+hPwkprjnQ/GncH1oYglP6GQL3fSK6Y8FA8Q7tlQ1tU+aKOia4lrZ2Krq1vFVy5ya1C9wmbuoN7RhqCum9H71Vhp/FrzUlAYKUhQL5CiK6M3fFHUfDprajXT2eYuinT1I4lptCvMm/Kd7yqGDd4w8Gr2cVvMvuwLQWP6HQ== -------------------------------------------------------------------------------- /resources/server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trisfald/weasel/756cfd85da5b6fc03e3880e3be2b966980ceb209/resources/server.png -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | //! A battle client. 2 | 3 | use crate::battle::{Battle, BattleController, BattleRules, EventCallback}; 4 | use crate::error::WeaselResult; 5 | use crate::event::{ 6 | EventProcessor, EventPrototype, EventReceiver, MultiClientSink, MultiClientSinkHandle, 7 | MultiClientSinkHandleMut, ServerSink, VersionedEventWrapper, 8 | }; 9 | use crate::player::PlayerId; 10 | 11 | /// A client event processor. 12 | /// 13 | /// Clients can accept any kind of event from a remote server. 14 | /// Local events are sent to the server to which the client is connected. 15 | /// 16 | /// One or more client sinks can be connected to a client. Events received from 17 | /// the server are propagated to these sinks. 18 | pub struct Client { 19 | battle: Battle, 20 | server_sink: Box + Send>, 21 | client_sinks: MultiClientSink, 22 | player: Option, 23 | } 24 | 25 | impl Client { 26 | /// Returns a client builder. 27 | pub fn builder( 28 | battle: Battle, 29 | server_sink: Box + Send>, 30 | ) -> ClientBuilder { 31 | ClientBuilder { 32 | battle, 33 | server_sink, 34 | player: None, 35 | } 36 | } 37 | 38 | /// Returns whether or not client events authentication is enabled. 39 | pub fn authentication(&self) -> bool { 40 | self.player.is_some() 41 | } 42 | 43 | /// Returns the player id associated to this client. 44 | pub fn player(&self) -> &Option { 45 | &self.player 46 | } 47 | 48 | /// Returns a reference to the server sink to which all event prototypes 49 | /// initiated by this client are sent. 50 | pub fn server_sink(&self) -> &(dyn ServerSink + Send) { 51 | &*self.server_sink 52 | } 53 | 54 | /// Disconnects the current server sink and sets a new one. 55 | pub fn set_server_sink(&mut self, sink: Box + Send>) { 56 | self.server_sink.on_disconnect(); 57 | self.server_sink = sink; 58 | } 59 | 60 | /// Returns a handle to access the client sinks of this client. 61 | pub fn client_sinks(&self) -> MultiClientSinkHandle<'_, R> { 62 | MultiClientSinkHandle::new(&self.client_sinks) 63 | } 64 | 65 | /// Returns a mutable handle to manage the client sinks of this client. 66 | pub fn client_sinks_mut(&mut self) -> MultiClientSinkHandleMut<'_, R> { 67 | MultiClientSinkHandleMut::new(&mut self.client_sinks, &self.battle) 68 | } 69 | } 70 | 71 | impl BattleController for Client { 72 | fn battle(&self) -> &Battle { 73 | &self.battle 74 | } 75 | 76 | fn event_callback(&self) -> &Option> { 77 | &self.battle.event_callback 78 | } 79 | 80 | fn set_event_callback(&mut self, callback: Option>) { 81 | self.battle.event_callback = callback; 82 | } 83 | } 84 | 85 | impl EventProcessor for Client { 86 | type ProcessOutput = WeaselResult<(), R>; 87 | 88 | fn process(&mut self, event: EventPrototype) -> Self::ProcessOutput { 89 | self.battle.verify_prototype(&event)?; 90 | // Decorate the prototype with additional information. 91 | let event = event.client_prototype(self.battle().rules().version().clone(), self.player); 92 | // Send the event to the server. 93 | self.server_sink.send(&event) 94 | } 95 | } 96 | 97 | impl EventReceiver for Client { 98 | fn receive(&mut self, event: VersionedEventWrapper) -> WeaselResult<(), R> { 99 | // Verify the event. 100 | self.battle.verify_wrapper(&event)?; 101 | // Apply the event on the battle. 102 | self.battle.apply(&event.wrapper(), &mut None); 103 | // Send the event to all client sinks. 104 | self.client_sinks.send_all(&event); 105 | Ok(()) 106 | } 107 | } 108 | 109 | /// A builder object to create a client. 110 | pub struct ClientBuilder { 111 | battle: Battle, 112 | server_sink: Box + Send>, 113 | player: Option, 114 | } 115 | 116 | impl ClientBuilder { 117 | /// Enable authentication on the new client. 118 | /// All produced events will be authenticated with `player`. 119 | pub fn enable_authentication(mut self, player: PlayerId) -> Self { 120 | self.player = Some(player); 121 | self 122 | } 123 | 124 | /// Creates a new client. 125 | pub fn build(self) -> Client { 126 | Client { 127 | battle: self.battle, 128 | server_sink: self.server_sink, 129 | client_sinks: MultiClientSink::new(), 130 | player: self.player, 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/fight.rs: -------------------------------------------------------------------------------- 1 | //! Module to handle combat. 2 | 3 | use crate::battle::{Battle, BattleRules, BattleState}; 4 | use crate::character::Character; 5 | use crate::entropy::Entropy; 6 | use crate::error::WeaselResult; 7 | use crate::event::{Event, EventKind, EventProcessor, EventQueue, EventTrigger, LinkedQueue}; 8 | use crate::metric::WriteMetrics; 9 | use crate::status::{Application, AppliedStatus}; 10 | #[cfg(feature = "serialization")] 11 | use serde::{Deserialize, Serialize}; 12 | use std::any::Any; 13 | use std::fmt::Debug; 14 | 15 | /// Rules to determine how combat works. They manage the damage dealt, 16 | /// accuracy of attacks and, more in general, how to apply consequences of abilities. 17 | pub trait FightRules { 18 | #[cfg(not(feature = "serialization"))] 19 | /// See [Impact](type.Impact.html). 20 | type Impact: Clone + Debug + Send; 21 | #[cfg(feature = "serialization")] 22 | /// See [Impact](type.Impact.html). 23 | type Impact: Clone + Debug + Send + Serialize + for<'a> Deserialize<'a>; 24 | 25 | #[cfg(not(feature = "serialization"))] 26 | /// See [Potency](../status/type.Potency.html). 27 | type Potency: Clone + Debug + Send; 28 | #[cfg(feature = "serialization")] 29 | /// See [Potency](../status/type.Potency.html). 30 | type Potency: Clone + Debug + Send + Serialize + for<'a> Deserialize<'a>; 31 | 32 | /// Takes an impact and generates one or more events to change the state of creatures or 33 | /// other objects. 34 | /// 35 | /// The provided implementation does nothing. 36 | fn apply_impact( 37 | &self, 38 | _state: &BattleState, 39 | _impact: &Self::Impact, 40 | _event_queue: &mut Option>, 41 | _entropy: &mut Entropy, 42 | _metrics: &mut WriteMetrics, 43 | ) { 44 | } 45 | 46 | /// Applies the side effects of a status when it's inflicted upon a character. 47 | /// `application` contains the context in which the status was created. 48 | /// 49 | /// The status is automatically added to the character before the call to this method. 50 | /// 51 | /// The provided implementation does nothing. 52 | fn apply_status( 53 | &self, 54 | _state: &BattleState, 55 | _character: &dyn Character, 56 | _application: Application, 57 | _event_queue: &mut Option>, 58 | _entropy: &mut Entropy, 59 | _metrics: &mut WriteMetrics, 60 | ) { 61 | } 62 | 63 | /// Applies the periodic side effects of a status. 64 | /// Returns `true` if the status should end after this update. 65 | /// 66 | /// For actors: status updates happen at the start of their turn.\ 67 | /// For non-actor characters: status updates happen when the event `EnvironmentTurn` is fired. 68 | /// 69 | /// The provided implementation does nothing and it never ends any status. 70 | fn update_status( 71 | &self, 72 | _state: &BattleState, 73 | _character: &dyn Character, 74 | _status: &AppliedStatus, 75 | _linked_queue: &mut Option>, 76 | _entropy: &mut Entropy, 77 | _metrics: &mut WriteMetrics, 78 | ) -> bool { 79 | false 80 | } 81 | 82 | /// Removes the side effects of a status when the latter is removed from a character. 83 | /// 84 | /// The character is guaranteed to be affected by `status`. 85 | /// The status will be automatically dropped immediately after this method. 86 | /// 87 | /// The provided implementation does nothing. 88 | fn delete_status( 89 | &self, 90 | _state: &BattleState, 91 | _character: &dyn Character, 92 | _status: &AppliedStatus, 93 | _event_queue: &mut Option>, 94 | _entropy: &mut Entropy, 95 | _metrics: &mut WriteMetrics, 96 | ) { 97 | } 98 | } 99 | 100 | /// Impacts encapsulate information about which creatures or areas are affected 101 | /// and what force is applied to them. 102 | /// 103 | /// More specifically, an impact should contain 104 | /// the necessary data to generate altering events on creatures or other objects.\ 105 | /// It's important to understand that an impact is an indirection between an ability's output 106 | /// and its effect on the world. For instance, throwing a bomb could be considered the 107 | /// ability while the bomb's explosion would be the impact; the explosion might then 108 | /// cause damage to one or more creatures. 109 | pub type Impact = <::FR as FightRules>::Impact; 110 | 111 | /// An event to apply an impact on the game world. 112 | /// 113 | /// # Examples 114 | /// ``` 115 | /// use weasel::{ 116 | /// battle_rules, rules::empty::*, ApplyImpact, Battle, BattleController, BattleRules, 117 | /// EventKind, EventTrigger, Server, 118 | /// }; 119 | /// 120 | /// battle_rules! {} 121 | /// 122 | /// let battle = Battle::builder(CustomRules::new()).build(); 123 | /// let mut server = Server::builder(battle).build(); 124 | /// 125 | /// let impact = (); 126 | /// ApplyImpact::trigger(&mut server, impact).fire().unwrap(); 127 | /// assert_eq!( 128 | /// server.battle().history().events()[0].kind(), 129 | /// EventKind::ApplyImpact 130 | /// ); 131 | /// ``` 132 | #[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))] 133 | pub struct ApplyImpact { 134 | #[cfg_attr( 135 | feature = "serialization", 136 | serde(bound( 137 | serialize = "Impact: Serialize", 138 | deserialize = "Impact: Deserialize<'de>" 139 | )) 140 | )] 141 | impact: Impact, 142 | } 143 | 144 | impl ApplyImpact { 145 | /// Returns a trigger for this event. 146 | pub fn trigger<'a, P: EventProcessor>( 147 | processor: &'a mut P, 148 | impact: Impact, 149 | ) -> ApplyImpactTrigger<'a, R, P> { 150 | ApplyImpactTrigger { processor, impact } 151 | } 152 | 153 | /// Returns the impact inside this event. 154 | pub fn impact(&self) -> &Impact { 155 | &self.impact 156 | } 157 | } 158 | 159 | impl std::fmt::Debug for ApplyImpact { 160 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 161 | write!(f, "ApplyImpact {{ impact: {:?} }}", self.impact) 162 | } 163 | } 164 | 165 | impl Clone for ApplyImpact { 166 | fn clone(&self) -> Self { 167 | Self { 168 | impact: self.impact.clone(), 169 | } 170 | } 171 | } 172 | 173 | impl Event for ApplyImpact { 174 | fn verify(&self, _: &Battle) -> WeaselResult<(), R> { 175 | // For simplicity, don't verify an impact. 176 | // Trust the server to generate *processable* impacts. 177 | // `apply` should take care of generating correct events in all cases. 178 | Ok(()) 179 | } 180 | 181 | fn apply(&self, battle: &mut Battle, event_queue: &mut Option>) { 182 | battle.rules.fight_rules().apply_impact( 183 | &battle.state, 184 | &self.impact, 185 | event_queue, 186 | &mut battle.entropy, 187 | &mut battle.metrics.write_handle(), 188 | ); 189 | } 190 | 191 | fn kind(&self) -> EventKind { 192 | EventKind::ApplyImpact 193 | } 194 | 195 | fn box_clone(&self) -> Box + Send> { 196 | Box::new(self.clone()) 197 | } 198 | 199 | fn as_any(&self) -> &dyn Any { 200 | self 201 | } 202 | } 203 | 204 | /// Trigger to build and fire an `ApplyImpact` event. 205 | pub struct ApplyImpactTrigger<'a, R, P> 206 | where 207 | R: BattleRules, 208 | P: EventProcessor, 209 | { 210 | processor: &'a mut P, 211 | impact: Impact, 212 | } 213 | 214 | impl<'a, R, P> EventTrigger<'a, R, P> for ApplyImpactTrigger<'a, R, P> 215 | where 216 | R: BattleRules + 'static, 217 | P: EventProcessor, 218 | { 219 | fn processor(&'a mut self) -> &'a mut P { 220 | self.processor 221 | } 222 | 223 | /// Returns an `ApplyImpact` event. 224 | fn event(&self) -> Box + Send> { 225 | Box::new(ApplyImpact { 226 | impact: self.impact.clone(), 227 | }) 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/history.rs: -------------------------------------------------------------------------------- 1 | //! History of events. 2 | 3 | use crate::battle::BattleRules; 4 | use crate::error::{WeaselError, WeaselResult}; 5 | use crate::event::EventId; 6 | use crate::event::EventWrapper; 7 | use std::convert::TryInto; 8 | 9 | /// History is the place where all events are kept, in a way such that they 10 | /// construct a single, consistent timeline. 11 | pub struct History { 12 | events: Vec>, 13 | } 14 | 15 | impl History { 16 | /// Creates a new History. 17 | pub(crate) fn new() -> Self { 18 | Self { events: Vec::new() } 19 | } 20 | 21 | /// Returns all events inside this timeline. 22 | pub fn events(&self) -> &[EventWrapper] { 23 | &self.events 24 | } 25 | 26 | /// Stores a new event in the history logs. 27 | pub(crate) fn archive(&mut self, event: &EventWrapper) { 28 | assert_eq!(event.id() as usize, self.events.len()); 29 | self.events.push(event.clone()); 30 | } 31 | 32 | /// Verifies if an event has an id compatible with the current timeline. 33 | /// Timeline only accepts monotonically increasing ids with no gaps. 34 | pub(crate) fn verify_event(&self, event: &EventWrapper) -> WeaselResult<(), R> { 35 | if event.id() as usize != self.events.len() { 36 | return Err(WeaselError::NonContiguousEventId( 37 | event.id(), 38 | self.events.len().try_into().unwrap(), 39 | )); 40 | } 41 | Ok(()) 42 | } 43 | 44 | /// Returns the id for the next event. 45 | pub(crate) fn next_id(&self) -> EventId { 46 | self.events.len().try_into().unwrap() 47 | } 48 | 49 | /// Returns the number of events in this history. 50 | pub fn len(&self) -> EventId { 51 | self.events.len().try_into().unwrap() 52 | } 53 | 54 | /// Returns whether this history is empty. 55 | pub fn is_empty(&self) -> bool { 56 | self.events.is_empty() 57 | } 58 | } 59 | 60 | #[cfg(test)] 61 | mod tests { 62 | use super::*; 63 | use crate::event::{DummyEvent, EventTrigger}; 64 | use crate::{battle_rules, rules::empty::*}; 65 | 66 | #[test] 67 | fn verify_id() { 68 | battle_rules! {} 69 | let mut history = History::::new(); 70 | let mut try_archive = |id| -> WeaselResult<(), _> { 71 | let event = EventWrapper::new(id, None, DummyEvent::trigger(&mut ()).event()); 72 | history.verify_event(&event)?; 73 | history.archive(&event); 74 | Ok(()) 75 | }; 76 | assert!(try_archive(3).is_err()); 77 | assert!(try_archive(0).is_ok()); 78 | assert!(try_archive(2).is_err()); 79 | assert!(try_archive(1).is_ok()); 80 | assert!(try_archive(1).is_err()); 81 | assert!(try_archive(0).is_err()); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | #![doc(test(attr(warn(warnings))))] 3 | 4 | //! 5 | //! weasel is a customizable battle system for turn-based games. 6 | //! 7 | //! * Simple way to define the combat's rules, taking advantage of Rust's strong type system. 8 | //! * Battle events are collected into a timeline to support save and restore, replays, and more. 9 | //! * Client/server architecture; all battle events are verified by the server. 10 | //! * Minimal performance overhead. 11 | //! 12 | //! ## Examples 13 | //! 14 | //! ``` 15 | //! use weasel::{ 16 | //! battle_rules, rules::empty::*, Battle, BattleController, 17 | //! BattleRules, CreateTeam, EventTrigger, Server, 18 | //! }; 19 | //! 20 | //! battle_rules! {} 21 | //! 22 | //! let battle = Battle::builder(CustomRules::new()).build(); 23 | //! let mut server = Server::builder(battle).build(); 24 | //! 25 | //! CreateTeam::trigger(&mut server, 1).fire().unwrap(); 26 | //! assert_eq!(server.battle().entities().teams().count(), 1); 27 | //! ``` 28 | //! 29 | //! You can find real examples of battle systems made with weasel in 30 | //! [examples](https://github.com/Trisfald/weasel/tree/master/examples/). 31 | //! 32 | //! ## How does it work? 33 | //! 34 | //! To use this library, you would create instances of its main objects: `server` and `client`. 35 | //! You will notice that both of them are parameterized with a `BattleRules` generic type.\ 36 | //! A `server` is mandatory to manage a game. A server can be also a client. 37 | //! For example, a typical single player game needs only one server.\ 38 | //! A `client` is a participant to a game. It sends commands to a server on behalf of a player. 39 | //! A multiplayer game would have one server and multiple clients. 40 | //! 41 | //! Once you have instantiated a `server` and possibly one or more `clients`, 42 | //! you are ready to begin a new game.\ 43 | //! Games are carried forward by creating `events`. 44 | //! There are many kind of events, see the documentation to know more. 45 | //! 46 | //! Through a `server` or a `client` you'll be able to access the full state of the battle, 47 | //! including the entire timeline of events. 48 | //! 49 | //! ## Features 50 | //! 51 | //! weasel provides many functionalities to ease the development of a turn based game: 52 | //! 53 | //! - Creatures and inanimate objects. 54 | //! - Statistics and abilities for characters. 55 | //! - Long lasting status effects. 56 | //! - Player managed teams. 57 | //! - Team objectives and diplomacy. 58 | //! - Division of the battle into turns and rounds. 59 | //! - Rules to govern the game subdivided into orthogonal traits. 60 | //! - Fully serializable battle history. 61 | //! - Cause-effect relationship between events. 62 | //! - Server side verification of clients' events. 63 | //! - Player permissions and authorization. 64 | //! - Versioning for battle rules. 65 | //! - User defined events. 66 | //! - System and user defined metrics. 67 | //! - Sinks to forward events to an arbitrary destination. 68 | //! - Small collection of predefined rules. 69 | //! 70 | //! ## Define the game's rules via traits 71 | //! 72 | //! `BattleRules` is a collection of modules and it lets you define all the *rules* for your game by 73 | //! implementing a trait for each module.\ 74 | //! Having multiple modules helps you in decomposing your rules into smaller parts, orthogonal to 75 | //! each other. 76 | //! 77 | //! ### Predefined rules traits 78 | //! 79 | //! weasel contains a minimal set of predefined rules traits, mainly comprised of rules that do 80 | //! nothing and of basic rules for entropy. 81 | //! 82 | //! You can find the predefined rules in the `::rules` scope. 83 | //! 84 | //! ## Event based 85 | //! 86 | //! weasel is fully based on events. It means that all changes on the battle state must be done 87 | //! through events.\ 88 | //! Thanks to this strong restriction, the library can collect all events into a historical 89 | //! timeline. This timeline can then be exported and re-imported at a later stage; 90 | //! this's fundamental to implement save and load or even replays. 91 | //! 92 | //! Users can register on a callback each time an event is processed, to extend the library's 93 | //! functionalities with their own logic. 94 | //! 95 | //! It's possible to create your own events, by implementing the `Event` trait and using the 96 | //! reserved `EventKind::UserEvent`. Remember to also write a `UserEventPacker` in the case 97 | //! you wish to enable serialization. 98 | //! 99 | //! ## Client - server architecture 100 | //! 101 | //! The library uses a client - server architecture to support multiplayer games. Both server 102 | //! and clients contain a replica of the battle's state, but only the events verified by the server 103 | //! will be able to change the state. Client late connection and reconnection are supported. 104 | //! 105 | //! It is necessary that all peers use the same version of the rules. 106 | //! 107 | //! ## Metrics 108 | //! 109 | //! There's a built-in storage for metrics that let you retrieve and modify individual metrics 110 | //! based on an unique id. Metrics are divided in two kind: system and user defined. 111 | //! 112 | //! System metrics are predefined and handled by the library. You can only read their current value. 113 | //! 114 | //! User defined metrics can be created on the fly. The user has full power over them: they can be 115 | //! removed, modified and read. 116 | //! 117 | //! # Optional Features 118 | //! 119 | //! The following optional features are available: 120 | //! 121 | //! - `random`: enables built-in entropy rules that use a pseudorandom number generator. 122 | //! - `serialization`: enables serialization and deserialization of events. 123 | 124 | pub mod ability; 125 | pub use crate::ability::ActivateAbility; 126 | 127 | pub mod actor; 128 | pub use crate::actor::{Action, Actor, ActorRules, AlterAbilities, RegenerateAbilities}; 129 | 130 | pub mod battle; 131 | pub use crate::battle::{ 132 | Battle, BattleController, BattleRules, BattleState, EndBattle, EventCallback, Version, 133 | }; 134 | 135 | pub mod character; 136 | pub use crate::character::{AlterStatistics, Character, CharacterRules, RegenerateStatistics}; 137 | 138 | pub mod client; 139 | pub use crate::client::Client; 140 | 141 | pub mod creature; 142 | pub use crate::creature::{ConvertCreature, CreateCreature, Creature, RemoveCreature}; 143 | 144 | pub mod entity; 145 | pub use crate::entity::{Entities, Entity, EntityId, RemoveEntity, Transmutation}; 146 | 147 | pub mod entropy; 148 | pub use crate::entropy::{Entropy, EntropyRules, ResetEntropy}; 149 | 150 | pub mod error; 151 | pub use crate::error::{WeaselError, WeaselResult}; 152 | 153 | pub mod event; 154 | pub use crate::event::{ 155 | ClientEventPrototype, Event, EventId, EventKind, EventProcessor, EventPrototype, EventQueue, 156 | EventReceiver, EventRights, EventServer, EventTrigger, EventWrapper, LinkedQueue, 157 | VersionedEventWrapper, 158 | }; 159 | 160 | pub mod fight; 161 | pub use crate::fight::{ApplyImpact, FightRules}; 162 | 163 | pub mod history; 164 | pub use crate::history::History; 165 | 166 | pub mod metric; 167 | pub use crate::metric::{Metric, MetricId, ReadMetrics, SystemMetricId, WriteMetrics}; 168 | 169 | pub mod object; 170 | pub use crate::object::{CreateObject, Object, RemoveObject}; 171 | 172 | pub mod player; 173 | pub use crate::player::PlayerId; 174 | 175 | pub mod power; 176 | pub use crate::power::InvokePower; 177 | 178 | pub mod round; 179 | pub use crate::round::{ 180 | EndRound, EndTurn, EnvironmentTurn, ResetRounds, Rounds, RoundsRules, StartTurn, 181 | }; 182 | 183 | pub mod rules; 184 | 185 | #[cfg(feature = "serialization")] 186 | pub mod serde; 187 | #[cfg(feature = "serialization")] 188 | pub use crate::serde::{FlatClientEvent, FlatEvent, FlatVersionedEvent}; 189 | 190 | pub mod server; 191 | pub use crate::server::Server; 192 | 193 | pub mod space; 194 | pub use crate::space::{AlterSpace, MoveEntity, PositionClaim, ResetSpace, Space, SpaceRules}; 195 | 196 | pub mod status; 197 | pub use crate::status::{AlterStatuses, Application, AppliedStatus, ClearStatus, InflictStatus}; 198 | 199 | pub mod team; 200 | pub use crate::team::{ 201 | AlterPowers, Call, ConcludeObjectives, Conclusion, CreateTeam, EntityAddition, 202 | RegeneratePowers, Relation, RemoveTeam, ResetObjectives, SetRelations, Team, TeamRules, 203 | }; 204 | 205 | pub mod user; 206 | #[cfg(feature = "serialization")] 207 | pub use crate::user::UserEventPacker; 208 | pub use crate::user::{UserEventId, UserRules}; 209 | 210 | pub mod util; 211 | pub use crate::util::Id; 212 | -------------------------------------------------------------------------------- /src/power.rs: -------------------------------------------------------------------------------- 1 | //! Module to manage powers. 2 | 3 | use crate::battle::{Battle, BattleRules}; 4 | use crate::error::{WeaselError, WeaselResult}; 5 | use crate::event::{Event, EventKind, EventProcessor, EventQueue, EventRights, EventTrigger}; 6 | use crate::round::TurnStateType; 7 | use crate::team::{Call, TeamId, TeamRules}; 8 | use crate::util::Id; 9 | #[cfg(feature = "serialization")] 10 | use serde::{Deserialize, Serialize}; 11 | use std::any::Any; 12 | 13 | /// Type to represent a special power of a team. 14 | /// 15 | /// Powers are both a statistic and an ability. Thus, they can be used to give a team 16 | /// a certain property and/or an activable skill. 17 | pub type Power = <::TR as TeamRules>::Power; 18 | 19 | /// Alias for `Power::Id`. 20 | pub type PowerId = as Id>::Id; 21 | 22 | /// Type to drive the generation of the powers for a given team. 23 | pub type PowersSeed = <::TR as TeamRules>::PowersSeed; 24 | 25 | /// Type to customize in which way a power is invoked. 26 | pub type Invocation = <::TR as TeamRules>::Invocation; 27 | 28 | /// Encapsulates the data used to describe an alteration of one or more powers. 29 | pub type PowersAlteration = <::TR as TeamRules>::PowersAlteration; 30 | 31 | /// Event to make a team invoke a power. 32 | /// 33 | /// A team can invoke a power in between actor turns or during turns of actors 34 | /// that belongs to itself. 35 | /// 36 | /// # Examples 37 | /// ``` 38 | /// use weasel::{ 39 | /// battle_rules, rules::empty::*, Battle, BattleRules, CreateTeam, EntityId, 40 | /// EventTrigger, InvokePower, Server, 41 | /// }; 42 | /// 43 | /// battle_rules! {} 44 | /// 45 | /// let battle = Battle::builder(CustomRules::new()).build(); 46 | /// let mut server = Server::builder(battle).build(); 47 | /// 48 | /// let team_id = 1; 49 | /// CreateTeam::trigger(&mut server, team_id).fire().unwrap(); 50 | /// 51 | /// let power_id = 99; 52 | /// let result = InvokePower::trigger(&mut server, team_id, power_id).fire(); 53 | /// // We get an error because the team doesn't possess this power. 54 | /// // The team's powers must defined in 'TeamRules'. 55 | /// assert!(result.is_err()); 56 | /// ``` 57 | #[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))] 58 | pub struct InvokePower { 59 | #[cfg_attr( 60 | feature = "serialization", 61 | serde(bound( 62 | serialize = "TeamId: Serialize", 63 | deserialize = "TeamId: Deserialize<'de>" 64 | )) 65 | )] 66 | team_id: TeamId, 67 | 68 | #[cfg_attr( 69 | feature = "serialization", 70 | serde(bound( 71 | serialize = "PowerId: Serialize", 72 | deserialize = "PowerId: Deserialize<'de>" 73 | )) 74 | )] 75 | power_id: PowerId, 76 | 77 | #[cfg_attr( 78 | feature = "serialization", 79 | serde(bound( 80 | serialize = "Option>: Serialize", 81 | deserialize = "Option>: Deserialize<'de>" 82 | )) 83 | )] 84 | invocation: Option>, 85 | } 86 | 87 | impl InvokePower { 88 | /// Returns a trigger for this event. 89 | pub fn trigger>( 90 | processor: &mut P, 91 | team_id: TeamId, 92 | power_id: PowerId, 93 | ) -> InvokePowerTrigger { 94 | InvokePowerTrigger { 95 | processor, 96 | team_id, 97 | power_id, 98 | invocation: None, 99 | } 100 | } 101 | 102 | /// Returns the id of the team that is invoking the power. 103 | pub fn team_id(&self) -> &TeamId { 104 | &self.team_id 105 | } 106 | 107 | /// Returns the id of the power to be invoked. 108 | pub fn power_id(&self) -> &PowerId { 109 | &self.power_id 110 | } 111 | 112 | /// Returns the invocation profile for the power. 113 | pub fn invocation(&self) -> &Option> { 114 | &self.invocation 115 | } 116 | } 117 | 118 | impl std::fmt::Debug for InvokePower { 119 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 120 | write!( 121 | f, 122 | "InvokePower {{ team_id: {:?}, power_id: {:?}, invocation: {:?} }}", 123 | self.team_id, self.power_id, self.invocation 124 | ) 125 | } 126 | } 127 | 128 | impl Clone for InvokePower { 129 | fn clone(&self) -> Self { 130 | InvokePower { 131 | team_id: self.team_id.clone(), 132 | power_id: self.power_id.clone(), 133 | invocation: self.invocation.clone(), 134 | } 135 | } 136 | } 137 | 138 | impl Event for InvokePower { 139 | fn verify(&self, battle: &Battle) -> WeaselResult<(), R> { 140 | // Verify that the team exists. 141 | if let Some(team) = battle.entities().team(&self.team_id) { 142 | // Verify that the team can invoke a power at this stage. 143 | let ready = match battle.state.rounds.state() { 144 | TurnStateType::Ready => true, 145 | TurnStateType::Started(entities) => entities.iter().any(|e| { 146 | battle 147 | .state 148 | .entities 149 | .actor(&e) 150 | .map(|a| *a.team_id() == self.team_id) 151 | .unwrap_or(false) 152 | }), 153 | }; 154 | if !ready { 155 | return Err(WeaselError::TeamNotReady(self.team_id.clone())); 156 | } 157 | // Verify that the team possesses this power. 158 | if let Some(power) = team.power(&self.power_id) { 159 | // Verify if this power can be activated. 160 | battle 161 | .rules 162 | .team_rules() 163 | .invocable(&battle.state, Call::new(team, power, &self.invocation)) 164 | .map_err(|err| { 165 | WeaselError::PowerNotInvocable( 166 | self.team_id.clone(), 167 | self.power_id.clone(), 168 | Box::new(err), 169 | ) 170 | }) 171 | } else { 172 | Err(WeaselError::PowerNotKnown( 173 | self.team_id.clone(), 174 | self.power_id.clone(), 175 | )) 176 | } 177 | } else { 178 | Err(WeaselError::TeamNotFound(self.team_id.clone())) 179 | } 180 | } 181 | 182 | fn apply(&self, battle: &mut Battle, event_queue: &mut Option>) { 183 | let team = battle 184 | .state 185 | .entities 186 | .team(&self.team_id) 187 | .unwrap_or_else(|| panic!("constraint violated: team {:?} not found", self.team_id)); 188 | let power = team.power(&self.power_id).unwrap_or_else(|| { 189 | panic!( 190 | "constraint violated: power {:?} not found in team {:?}", 191 | self.power_id, self.team_id 192 | ) 193 | }); 194 | battle.rules.team_rules().invoke( 195 | &battle.state, 196 | Call::new(team, power, &self.invocation), 197 | event_queue, 198 | &mut battle.entropy, 199 | &mut battle.metrics.write_handle(), 200 | ); 201 | } 202 | 203 | fn kind(&self) -> EventKind { 204 | EventKind::InvokePower 205 | } 206 | 207 | fn box_clone(&self) -> Box + Send> { 208 | Box::new(self.clone()) 209 | } 210 | 211 | fn as_any(&self) -> &dyn Any { 212 | self 213 | } 214 | 215 | fn rights<'a>(&'a self, _: &'a Battle) -> EventRights<'a, R> { 216 | EventRights::Team(&self.team_id) 217 | } 218 | } 219 | 220 | /// Trigger to build and fire an `InvokePower` event. 221 | pub struct InvokePowerTrigger<'a, R, P> 222 | where 223 | R: BattleRules, 224 | P: EventProcessor, 225 | { 226 | processor: &'a mut P, 227 | team_id: TeamId, 228 | power_id: PowerId, 229 | invocation: Option>, 230 | } 231 | 232 | impl<'a, R, P> InvokePowerTrigger<'a, R, P> 233 | where 234 | R: BattleRules + 'static, 235 | P: EventProcessor, 236 | { 237 | /// Adds an invocation profile to customize this power instance. 238 | pub fn invocation(&'a mut self, invocation: Invocation) -> &'a mut Self { 239 | self.invocation = Some(invocation); 240 | self 241 | } 242 | } 243 | 244 | impl<'a, R, P> EventTrigger<'a, R, P> for InvokePowerTrigger<'a, R, P> 245 | where 246 | R: BattleRules + 'static, 247 | P: EventProcessor, 248 | { 249 | fn processor(&'a mut self) -> &'a mut P { 250 | self.processor 251 | } 252 | 253 | /// Returns an `InvokePower` event. 254 | fn event(&self) -> Box + Send> { 255 | Box::new(InvokePower { 256 | team_id: self.team_id.clone(), 257 | power_id: self.power_id.clone(), 258 | invocation: self.invocation.clone(), 259 | }) 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/rules/ability.rs: -------------------------------------------------------------------------------- 1 | //! Generic implementations for all purpose abilities. 2 | 3 | use crate::util::Id; 4 | #[cfg(feature = "serialization")] 5 | use serde::{Deserialize, Serialize}; 6 | use std::fmt::Debug; 7 | use std::hash::Hash; 8 | 9 | /// A simple generic ability. 10 | #[derive(PartialEq, Clone, Debug)] 11 | #[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))] 12 | pub struct SimpleAbility { 13 | id: I, 14 | power: V, 15 | } 16 | 17 | impl SimpleAbility { 18 | /// Creates a new `SimpleAbility`. 19 | pub fn new(id: I, power: V) -> Self { 20 | Self { id, power } 21 | } 22 | 23 | /// Returns this ability's power. 24 | pub fn power(&self) -> V { 25 | self.power 26 | } 27 | 28 | /// Change the power of this ability. 29 | pub fn set_power(&mut self, power: V) { 30 | self.power = power; 31 | } 32 | } 33 | 34 | #[cfg(not(feature = "serialization"))] 35 | impl Id for SimpleAbility 36 | where 37 | I: Debug + Hash + Eq + Clone + Send, 38 | { 39 | type Id = I; 40 | fn id(&self) -> &Self::Id { 41 | &self.id 42 | } 43 | } 44 | 45 | #[cfg(feature = "serialization")] 46 | impl Id for SimpleAbility 47 | where 48 | I: Debug + Hash + Eq + Clone + Send + Serialize + for<'a> Deserialize<'a>, 49 | { 50 | type Id = I; 51 | fn id(&self) -> &Self::Id { 52 | &self.id 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/rules/empty.rs: -------------------------------------------------------------------------------- 1 | //! This module contains implementations for components that do nothing. 2 | //! Such rules are useful if you don't need to implement any logic for a particular module. 3 | 4 | use crate::actor::ActorRules; 5 | use crate::battle::BattleRules; 6 | use crate::character::CharacterRules; 7 | use crate::fight::FightRules; 8 | use crate::round::RoundsRules; 9 | use crate::rules::entropy::FixedAverage; 10 | use crate::space::SpaceRules; 11 | use crate::team::TeamRules; 12 | use crate::user::UserRules; 13 | use crate::util::Id; 14 | #[cfg(feature = "serialization")] 15 | use serde::{Deserialize, Serialize}; 16 | 17 | /// An empty statistic. 18 | #[derive(Hash, Eq, PartialEq, Debug)] 19 | #[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))] 20 | pub struct EmptyStat { 21 | /// The id of this statistic. 22 | pub id: u32, 23 | } 24 | 25 | impl Id for EmptyStat { 26 | type Id = u32; 27 | fn id(&self) -> &u32 { 28 | &self.id 29 | } 30 | } 31 | 32 | /// An empty ability that does not contain any data. 33 | pub type EmptyAbility = EmptyStat; 34 | 35 | /// An empty status that does nothing. 36 | pub type EmptyStatus = EmptyStat; 37 | 38 | /// An empty power having no data nor behavior. 39 | pub type EmptyPower = EmptyStat; 40 | 41 | /// Minimalistic implementation of team rules, doing no-op for everything. 42 | #[derive(Default)] 43 | pub struct EmptyTeamRules {} 44 | 45 | impl TeamRules for EmptyTeamRules { 46 | type Id = u32; 47 | type Power = EmptyPower; 48 | type PowersSeed = (); 49 | type Invocation = (); 50 | type PowersAlteration = (); 51 | type ObjectivesSeed = (); 52 | type Objectives = (); 53 | } 54 | 55 | /// Minimalistic implementation of character rules, doing no-op for everything. 56 | #[derive(Default)] 57 | pub struct EmptyCharacterRules {} 58 | 59 | impl CharacterRules for EmptyCharacterRules { 60 | type CreatureId = u32; 61 | type ObjectId = u32; 62 | type Statistic = EmptyStat; 63 | type StatisticsSeed = (); 64 | type StatisticsAlteration = (); 65 | type Status = EmptyStatus; 66 | type StatusesAlteration = (); 67 | } 68 | 69 | /// Minimalistic implementation of actor rules, doing no-op for everything. 70 | #[derive(Default)] 71 | pub struct EmptyActorRules {} 72 | 73 | impl ActorRules for EmptyActorRules { 74 | type Ability = EmptyAbility; 75 | type AbilitiesSeed = (); 76 | type Activation = (); 77 | type AbilitiesAlteration = (); 78 | } 79 | 80 | /// Minimalistic implementation of space rules, doing no-op for everything. 81 | #[derive(Default)] 82 | pub struct EmptySpaceRules {} 83 | 84 | impl SpaceRules for EmptySpaceRules { 85 | type Position = (); 86 | type SpaceSeed = (); 87 | type SpaceModel = (); 88 | type SpaceAlteration = (); 89 | 90 | fn generate_model(&self, _seed: &Option) -> Self::SpaceModel {} 91 | } 92 | 93 | /// Minimalistic implementation of rounds rules, doing no-op for everything. 94 | #[derive(Default)] 95 | pub struct EmptyRoundsRules {} 96 | 97 | impl RoundsRules for EmptyRoundsRules { 98 | type RoundsSeed = (); 99 | type RoundsModel = (); 100 | 101 | fn generate_model(&self, _: &Option) -> Self::RoundsModel {} 102 | } 103 | 104 | /// Minimalistic implementation of fight rules, doing no-op for everything. 105 | #[derive(Default)] 106 | pub struct EmptyFightRules {} 107 | 108 | impl FightRules for EmptyFightRules { 109 | type Impact = (); 110 | type Potency = (); 111 | } 112 | 113 | /// Minimalistic implementation of user rules, doing no-op for everything. 114 | #[derive(Default)] 115 | pub struct EmptyUserRules {} 116 | 117 | impl UserRules for EmptyUserRules { 118 | type UserMetricId = u16; 119 | #[cfg(feature = "serialization")] 120 | type UserEventPackage = (); 121 | } 122 | 123 | /// Entropy rules that do not have randomness. They just return the average value. 124 | pub type EmptyEntropyRules = FixedAverage; 125 | -------------------------------------------------------------------------------- /src/rules/entropy.rs: -------------------------------------------------------------------------------- 1 | //! Predefined rules for entropy. 2 | 3 | use crate::entropy::EntropyRules; 4 | use num_traits::{Num, One}; 5 | #[cfg(feature = "random")] 6 | use rand::distributions::uniform::SampleUniform; 7 | #[cfg(feature = "random")] 8 | use rand::{Rng, SeedableRng}; 9 | #[cfg(feature = "random")] 10 | use rand_pcg::Lcg64Xsh32; 11 | use std::fmt::Debug; 12 | use std::marker::PhantomData; 13 | 14 | /// A deterministic rule that always returns the lowest value. 15 | #[derive(Debug, Default, Clone, Copy)] 16 | pub struct FixedLow { 17 | _phantom: PhantomData, 18 | } 19 | 20 | impl EntropyRules for FixedLow { 21 | type EntropySeed = (); 22 | type EntropyModel = (); 23 | type EntropyOutput = T; 24 | 25 | fn generate_model(&self, _seed: &Option) -> Self::EntropyModel {} 26 | 27 | /// Always returns `low`. 28 | fn generate( 29 | &self, 30 | _: &mut Self::EntropyModel, 31 | low: Self::EntropyOutput, 32 | _high: Self::EntropyOutput, 33 | ) -> Self::EntropyOutput { 34 | low 35 | } 36 | } 37 | 38 | /// A deterministic rule that always generates an average result. 39 | #[derive(Debug, Default, Clone, Copy)] 40 | pub struct FixedAverage { 41 | _phantom: PhantomData, 42 | } 43 | 44 | impl EntropyRules for FixedAverage { 45 | type EntropySeed = (); 46 | type EntropyModel = (); 47 | type EntropyOutput = T; 48 | 49 | fn generate_model(&self, _seed: &Option) -> Self::EntropyModel {} 50 | 51 | fn generate( 52 | &self, 53 | _: &mut Self::EntropyModel, 54 | low: Self::EntropyOutput, 55 | high: Self::EntropyOutput, 56 | ) -> T { 57 | let one: Self::EntropyOutput = One::one(); 58 | (high + low) / (one + one) 59 | } 60 | } 61 | 62 | /// Generate random numbers with uniform distribution. 63 | /// It uses a seedable pseudo random number generator with deterministic output. 64 | /// 65 | /// A seed is required to ensure a good level of entropy. 66 | #[cfg(feature = "random")] 67 | #[derive(Debug, Default, Clone, Copy)] 68 | pub struct UniformDistribution { 69 | _phantom: PhantomData, 70 | } 71 | 72 | #[cfg(feature = "random")] 73 | impl EntropyRules for UniformDistribution 74 | where 75 | T: PartialOrd + Copy + Num + Debug + SampleUniform, 76 | { 77 | type EntropySeed = u64; 78 | type EntropyModel = Lcg64Xsh32; 79 | type EntropyOutput = T; 80 | 81 | fn generate_model(&self, seed: &Option) -> Self::EntropyModel { 82 | Lcg64Xsh32::seed_from_u64(seed.unwrap_or(0)) 83 | } 84 | 85 | fn generate( 86 | &self, 87 | model: &mut Self::EntropyModel, 88 | low: Self::EntropyOutput, 89 | high: Self::EntropyOutput, 90 | ) -> Self::EntropyOutput { 91 | model.gen_range(low, high) 92 | } 93 | } 94 | 95 | #[cfg(test)] 96 | mod tests { 97 | use super::*; 98 | 99 | #[test] 100 | fn fixed_average() { 101 | let rule = FixedAverage::default(); 102 | assert_eq!(rule.generate(&mut (), 2, 12), 7); 103 | } 104 | 105 | #[test] 106 | fn fixed_low() { 107 | let rule = FixedLow::default(); 108 | assert_eq!(rule.generate(&mut (), 2, 12), 2); 109 | assert_eq!(rule.generate(&mut (), 4, 3), 4); 110 | } 111 | 112 | #[cfg(feature = "random")] 113 | #[test] 114 | fn uniform_distribution() { 115 | let seed = 1_204_678_643_940_597_513; 116 | let rule = UniformDistribution::default(); 117 | for _ in 0..2 { 118 | let mut model = rule.generate_model(&Some(seed)); 119 | assert_eq!(rule.generate(&mut model, 0, 10), 8); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/rules/generic.rs: -------------------------------------------------------------------------------- 1 | //! This module contains generic structs that you can use to compose rules. 2 | 3 | /// Macro to quickly generate battle rules. 4 | #[macro_export] 5 | macro_rules! battle_rules { 6 | () => { 7 | battle_rules! { 8 | EmptyTeamRules, 9 | EmptyCharacterRules, 10 | EmptyActorRules, 11 | EmptyFightRules, 12 | EmptyUserRules, 13 | EmptySpaceRules, 14 | EmptyRoundsRules, 15 | EmptyEntropyRules 16 | } 17 | }; 18 | ($ty: ty, $cy: ty, $ay: ty, $fy: ty, $uy: ty, $sy: ty, $ry: ty, $ey: ty) => { 19 | pub(crate) struct CustomRules { 20 | pub(crate) team_rules: $ty, 21 | pub(crate) character_rules: $cy, 22 | pub(crate) actor_rules: $ay, 23 | pub(crate) fight_rules: $fy, 24 | pub(crate) user_rules: $uy, 25 | pub(crate) space_rules: Option<$sy>, 26 | pub(crate) rounds_rules: Option<$ry>, 27 | pub(crate) entropy_rules: Option<$ey>, 28 | pub(crate) version: u32, 29 | } 30 | 31 | impl CustomRules { 32 | #[allow(dead_code)] 33 | pub(crate) fn new() -> Self { 34 | Self { 35 | team_rules: <$ty>::default(), 36 | character_rules: <$cy>::default(), 37 | actor_rules: <$ay>::default(), 38 | fight_rules: <$fy>::default(), 39 | user_rules: <$uy>::default(), 40 | space_rules: Some(<$sy>::default()), 41 | rounds_rules: Some(<$ry>::default()), 42 | entropy_rules: Some(<$ey>::default()), 43 | version: 0, 44 | } 45 | } 46 | } 47 | 48 | impl BattleRules for CustomRules { 49 | type TR = $ty; 50 | type CR = $cy; 51 | type AR = $ay; 52 | type FR = $fy; 53 | type UR = $uy; 54 | type SR = $sy; 55 | type RR = $ry; 56 | type ER = $ey; 57 | type Version = u32; 58 | 59 | fn team_rules(&self) -> &Self::TR { 60 | &self.team_rules 61 | } 62 | fn character_rules(&self) -> &Self::CR { 63 | &self.character_rules 64 | } 65 | fn actor_rules(&self) -> &Self::AR { 66 | &self.actor_rules 67 | } 68 | fn fight_rules(&self) -> &Self::FR { 69 | &self.fight_rules 70 | } 71 | fn user_rules(&self) -> &Self::UR { 72 | &self.user_rules 73 | } 74 | fn space_rules(&mut self) -> Self::SR { 75 | self.space_rules.take().expect("space_rules is None!") 76 | } 77 | fn rounds_rules(&mut self) -> Self::RR { 78 | self.rounds_rules.take().expect("rounds_rules is None!") 79 | } 80 | fn entropy_rules(&mut self) -> Self::ER { 81 | self.entropy_rules.take().expect("entropy_rules is None!") 82 | } 83 | fn version(&self) -> &Self::Version { 84 | &self.version 85 | } 86 | } 87 | }; 88 | } 89 | 90 | /// Empty battle rules with user defined `EntropyRules`. 91 | #[macro_export] 92 | macro_rules! battle_rules_with_entropy { 93 | ($ty: ty) => { 94 | battle_rules! { 95 | EmptyTeamRules, 96 | EmptyCharacterRules, 97 | EmptyActorRules, 98 | EmptyFightRules, 99 | EmptyUserRules, 100 | EmptySpaceRules, 101 | EmptyRoundsRules, 102 | $ty 103 | } 104 | }; 105 | } 106 | 107 | /// Empty battle rules with user defined `SpaceRules`. 108 | #[macro_export] 109 | macro_rules! battle_rules_with_space { 110 | ($ty: ty) => { 111 | battle_rules! { 112 | EmptyTeamRules, 113 | EmptyCharacterRules, 114 | EmptyActorRules, 115 | EmptyFightRules, 116 | EmptyUserRules, 117 | $ty, 118 | EmptyRoundsRules, 119 | EmptyEntropyRules 120 | } 121 | }; 122 | } 123 | 124 | /// Empty battle rules with user defined `RoundsRules`. 125 | #[macro_export] 126 | macro_rules! battle_rules_with_rounds { 127 | ($ty: ty) => { 128 | battle_rules! { 129 | EmptyTeamRules, 130 | EmptyCharacterRules, 131 | EmptyActorRules, 132 | EmptyFightRules, 133 | EmptyUserRules, 134 | EmptySpaceRules, 135 | $ty, 136 | EmptyEntropyRules 137 | } 138 | }; 139 | } 140 | 141 | /// Empty battle rules with user defined `TeamRules`. 142 | #[macro_export] 143 | macro_rules! battle_rules_with_team { 144 | ($ty: ty) => { 145 | battle_rules! { 146 | $ty, 147 | EmptyCharacterRules, 148 | EmptyActorRules, 149 | EmptyFightRules, 150 | EmptyUserRules, 151 | EmptySpaceRules, 152 | EmptyRoundsRules, 153 | EmptyEntropyRules 154 | } 155 | }; 156 | } 157 | 158 | /// Empty battle rules with user defined `ActorRules`. 159 | #[macro_export] 160 | macro_rules! battle_rules_with_actor { 161 | ($ty: ty) => { 162 | battle_rules! { 163 | EmptyTeamRules, 164 | EmptyCharacterRules, 165 | $ty, 166 | EmptyFightRules, 167 | EmptyUserRules, 168 | EmptySpaceRules, 169 | EmptyRoundsRules, 170 | EmptyEntropyRules 171 | } 172 | }; 173 | } 174 | 175 | /// Empty battle rules with user defined `CharacterRules`. 176 | #[macro_export] 177 | macro_rules! battle_rules_with_character { 178 | ($ty: ty) => { 179 | battle_rules! { 180 | EmptyTeamRules, 181 | $ty, 182 | EmptyActorRules, 183 | EmptyFightRules, 184 | EmptyUserRules, 185 | EmptySpaceRules, 186 | EmptyRoundsRules, 187 | EmptyEntropyRules 188 | } 189 | }; 190 | } 191 | 192 | /// Empty battle rules with user defined `FightRules`. 193 | #[macro_export] 194 | macro_rules! battle_rules_with_fight { 195 | ($ty: ty) => { 196 | battle_rules! { 197 | EmptyTeamRules, 198 | EmptyCharacterRules, 199 | EmptyActorRules, 200 | $ty, 201 | EmptyUserRules, 202 | EmptySpaceRules, 203 | EmptyRoundsRules, 204 | EmptyEntropyRules 205 | } 206 | }; 207 | } 208 | 209 | /// Empty battle rules with user defined `UserRules`. 210 | #[macro_export] 211 | macro_rules! battle_rules_with_user { 212 | ($ty: ty) => { 213 | battle_rules! { 214 | EmptyTeamRules, 215 | EmptyCharacterRules, 216 | EmptyActorRules, 217 | EmptyFightRules, 218 | $ty, 219 | EmptySpaceRules, 220 | EmptyRoundsRules, 221 | EmptyEntropyRules 222 | } 223 | }; 224 | } 225 | -------------------------------------------------------------------------------- /src/rules/mod.rs: -------------------------------------------------------------------------------- 1 | //! Collection of generic rules. 2 | 3 | pub mod ability; 4 | pub mod empty; 5 | pub mod entropy; 6 | mod generic; 7 | pub mod statistic; 8 | pub mod status; 9 | -------------------------------------------------------------------------------- /src/rules/statistic.rs: -------------------------------------------------------------------------------- 1 | //! Generic implementations for different types of statistic. 2 | 3 | use crate::util::Id; 4 | #[cfg(feature = "serialization")] 5 | use serde::{Deserialize, Serialize}; 6 | use std::cmp::PartialOrd; 7 | use std::fmt::Debug; 8 | use std::hash::Hash; 9 | use std::ops::Add; 10 | 11 | /// A simple generic statistic storing current value, minimum and maximum value. 12 | #[derive(PartialEq, Clone, Debug)] 13 | #[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))] 14 | pub struct SimpleStatistic { 15 | id: I, 16 | min: V, 17 | max: V, 18 | value: V, 19 | } 20 | 21 | impl SimpleStatistic { 22 | /// Creates a new `SimpleStatistic` with `value` equal to `max` 23 | /// and `min` equal to `V::default()`. 24 | pub fn new(id: I, max: V) -> Self { 25 | Self::with_value(id, V::default(), max, max) 26 | } 27 | 28 | /// Creates a new `SimpleStatistic` with the given value. 29 | pub fn with_value(id: I, min: V, max: V, value: V) -> Self { 30 | Self { 31 | id, 32 | min, 33 | max, 34 | value, 35 | } 36 | } 37 | } 38 | 39 | impl SimpleStatistic 40 | where 41 | V: Copy + PartialOrd + Add, 42 | { 43 | /// Returns the current value of this statistic. 44 | pub fn value(&self) -> V { 45 | self.value 46 | } 47 | 48 | /// Returns the minimum value of this statistic. 49 | pub fn min(&self) -> V { 50 | self.min 51 | } 52 | 53 | /// Returns the maximum value of this statistic. 54 | pub fn max(&self) -> V { 55 | self.max 56 | } 57 | 58 | /// Sets the current value to the new one, respecting the min/max bounds. 59 | pub fn set_value(&mut self, value: V) { 60 | self.value = value; 61 | if self.value < self.min { 62 | self.value = self.min; 63 | } else if self.value > self.max { 64 | self.value = self.max; 65 | } 66 | } 67 | 68 | /// Adds an increment `inc` to the value, respecting the min/max bounds. 69 | pub fn add(&mut self, inc: V) { 70 | self.set_value(self.value + inc); 71 | } 72 | } 73 | 74 | #[cfg(not(feature = "serialization"))] 75 | impl Id for SimpleStatistic 76 | where 77 | I: Debug + Hash + Eq + Clone + Send, 78 | { 79 | type Id = I; 80 | fn id(&self) -> &Self::Id { 81 | &self.id 82 | } 83 | } 84 | 85 | #[cfg(feature = "serialization")] 86 | impl Id for SimpleStatistic 87 | where 88 | I: Debug + Hash + Eq + Clone + Send + Serialize + for<'a> Deserialize<'a>, 89 | { 90 | type Id = I; 91 | fn id(&self) -> &Self::Id { 92 | &self.id 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use super::*; 99 | 100 | #[test] 101 | fn simple_statistic_bounds() { 102 | let mut stat = SimpleStatistic::with_value(1, 10, 20, 15); 103 | stat.add(100); 104 | assert_eq!(stat.value(), stat.max()); 105 | stat.add(-100); 106 | assert_eq!(stat.value(), stat.min()); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/rules/status.rs: -------------------------------------------------------------------------------- 1 | //! Generic implementations for all purpose statuses. 2 | 3 | use crate::status::StatusDuration; 4 | use crate::util::Id; 5 | #[cfg(feature = "serialization")] 6 | use serde::{Deserialize, Serialize}; 7 | use std::fmt::Debug; 8 | use std::hash::Hash; 9 | 10 | /// A simple generic status. 11 | #[derive(PartialEq, Clone, Debug)] 12 | #[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))] 13 | pub struct SimpleStatus { 14 | id: I, 15 | effect: V, 16 | max_duration: Option, 17 | } 18 | 19 | impl SimpleStatus { 20 | /// Creates a new `SimpleStatus`. 21 | pub fn new(id: I, effect: V, max_duration: Option) -> Self { 22 | Self { 23 | id, 24 | effect, 25 | max_duration, 26 | } 27 | } 28 | 29 | /// Returns the effect provoked by this status. 30 | pub fn effect(&self) -> V { 31 | self.effect 32 | } 33 | 34 | /// Change the effect of this status. 35 | pub fn set_effect(&mut self, effect: V) { 36 | self.effect = effect; 37 | } 38 | 39 | /// Returns the maximum duration of this status. 40 | /// `None` means infinite duration. 41 | pub fn max_duration(&self) -> Option { 42 | self.max_duration 43 | } 44 | } 45 | 46 | #[cfg(not(feature = "serialization"))] 47 | impl Id for SimpleStatus 48 | where 49 | I: Debug + Hash + Eq + Clone + Send, 50 | { 51 | type Id = I; 52 | fn id(&self) -> &Self::Id { 53 | &self.id 54 | } 55 | } 56 | 57 | #[cfg(feature = "serialization")] 58 | impl Id for SimpleStatus 59 | where 60 | I: Debug + Hash + Eq + Clone + Send + Serialize + for<'a> Deserialize<'a>, 61 | { 62 | type Id = I; 63 | fn id(&self) -> &Self::Id { 64 | &self.id 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | //! A battle server. 2 | 3 | use crate::battle::{Battle, BattleController, BattleRules, EventCallback}; 4 | use crate::error::{WeaselError, WeaselResult}; 5 | use crate::event::{ 6 | ClientEventPrototype, EventProcessor, EventPrototype, EventQueue, EventReceiver, EventRights, 7 | EventServer, EventWrapper, MultiClientSink, MultiClientSinkHandle, MultiClientSinkHandleMut, 8 | VersionedEventWrapper, 9 | }; 10 | use crate::player::{PlayerId, RightsHandle, RightsHandleMut}; 11 | use crate::team::TeamId; 12 | 13 | /// The server is the main object used to orchestrate a battle. 14 | /// 15 | /// A server owns all data of the battle and it can also process events. Events are the only way in 16 | /// which a battle can be evolved; all battle data can be retrieved through immutable references.\ 17 | /// Exactly one server is required in order to start a game. 18 | /// 19 | /// One or more client sinks can be connected to a server, to receive verified events. 20 | pub struct Server { 21 | pub(crate) battle: Battle, 22 | client_sinks: MultiClientSink, 23 | authentication: bool, 24 | } 25 | 26 | impl Server { 27 | /// Returns a server builder. 28 | pub fn builder(battle: Battle) -> ServerBuilder { 29 | ServerBuilder { 30 | battle, 31 | authentication: false, 32 | } 33 | } 34 | 35 | /// Returns true if the client events authentication is enforced. 36 | pub fn authentication(&self) -> bool { 37 | self.authentication 38 | } 39 | 40 | /// Returns a handle to access the players' rights to control one or more teams. 41 | pub fn rights(&self) -> RightsHandle { 42 | self.battle.rights() 43 | } 44 | 45 | /// Returns a mutable handle to manage the players' rights to control one or more teams. 46 | pub fn rights_mut<'a>(&'a mut self) -> RightsHandleMut>> { 47 | self.battle.rights_mut() 48 | } 49 | 50 | /// Returns a handle to access the client sinks of this server. 51 | pub fn client_sinks(&self) -> MultiClientSinkHandle<'_, R> { 52 | MultiClientSinkHandle::new(&self.client_sinks) 53 | } 54 | 55 | /// Returns a mutable handle to manage the client sinks of this server. 56 | pub fn client_sinks_mut(&mut self) -> MultiClientSinkHandleMut<'_, R> { 57 | MultiClientSinkHandleMut::new(&mut self.client_sinks, &self.battle) 58 | } 59 | 60 | /// Applies an event. The event must be valid. 61 | fn apply_event(&mut self, event: EventWrapper) -> WeaselResult<(), R> { 62 | let mut event_queue = Some(EventQueue::::new()); 63 | // Apply the event on the battle. 64 | self.battle.apply(&event, &mut event_queue); 65 | // Send the event to all client sinks. 66 | self.client_sinks 67 | .send_all(&event.clone().version(self.battle.rules().version().clone())); 68 | // Recursively process derived events. 69 | let mut errors = Vec::new(); 70 | if let Some(event_queue) = event_queue { 71 | for mut prototype in event_queue { 72 | // Set origin id in derived event, only if it wasn't set explicitly. 73 | if prototype.origin().is_none() { 74 | prototype.set_origin(Some(event.id())); 75 | } 76 | let result = self.process(prototype); 77 | if let Err(error) = result { 78 | errors.push(error); 79 | } 80 | } 81 | } 82 | // If there is an error, return it. 83 | // In the case of multiple errors, wrap them into a multi error. 84 | match errors.len() { 85 | 1 => Err(errors.swap_remove(0)), 86 | x if x > 1 => Err(WeaselError::MultiError(errors)), 87 | _ => Ok(()), 88 | } 89 | } 90 | 91 | /// Checks if the given player has rights to the given team. 92 | fn check_rights(&self, player: PlayerId, team_id: &TeamId) -> WeaselResult<(), R> { 93 | if !self.rights().check(player, team_id) { 94 | Err(WeaselError::AuthenticationError( 95 | Some(player), 96 | team_id.clone(), 97 | )) 98 | } else { 99 | Ok(()) 100 | } 101 | } 102 | } 103 | 104 | impl BattleController for Server { 105 | fn battle(&self) -> &Battle { 106 | &self.battle 107 | } 108 | 109 | fn event_callback(&self) -> &Option> { 110 | &self.battle.event_callback 111 | } 112 | 113 | fn set_event_callback(&mut self, callback: Option>) { 114 | self.battle.event_callback = callback; 115 | } 116 | } 117 | 118 | impl EventProcessor for Server { 119 | type ProcessOutput = WeaselResult<(), R>; 120 | 121 | fn process(&mut self, event: EventPrototype) -> Self::ProcessOutput { 122 | // Verify this event. 123 | self.battle 124 | .verify_prototype(&event) 125 | .map_err(|e| WeaselError::InvalidEvent(event.event().clone(), e.into()))?; 126 | // Promote verified event. 127 | let event = self.battle.promote(event); 128 | // Apply it. 129 | self.apply_event(event) 130 | } 131 | } 132 | 133 | impl EventServer for Server { 134 | fn process_client(&mut self, event: ClientEventPrototype) -> WeaselResult<(), R> { 135 | // Verify this event. 136 | self.battle.verify_client(&event)?; 137 | // Verify event's rights. 138 | match event.rights(&self.battle) { 139 | EventRights::Server => { 140 | return Err(WeaselError::ServerOnlyEvent); 141 | } 142 | EventRights::Team(team_id) => { 143 | if self.authentication { 144 | if let Some(player) = event.player() { 145 | // Player id is present. Check if it matches the event's rights. 146 | self.check_rights(player, team_id)?; 147 | } else { 148 | // No player id present. 149 | return Err(WeaselError::MissingAuthentication); 150 | } 151 | } 152 | } 153 | EventRights::Teams(teams_ids) => { 154 | if self.authentication { 155 | if let Some(player) = event.player() { 156 | // Player id is present. Check if it matches the event's rights. 157 | for team_id in teams_ids { 158 | self.check_rights(player, team_id)?; 159 | } 160 | } else { 161 | // No player id present. 162 | return Err(WeaselError::MissingAuthentication); 163 | } 164 | } 165 | } 166 | EventRights::None => {} 167 | } 168 | // Promote verified event. 169 | let event = self.battle.promote(event.prototype()); 170 | // Apply it. 171 | self.apply_event(event) 172 | } 173 | } 174 | 175 | impl EventReceiver for Server { 176 | fn receive(&mut self, event: VersionedEventWrapper) -> WeaselResult<(), R> { 177 | // Verify the event. 178 | self.battle.verify_wrapper(&event)?; 179 | // Apply the event on the battle. 180 | self.battle.apply(&event.wrapper(), &mut None); 181 | // Send the event to all client sinks. 182 | self.client_sinks.send_all(&event); 183 | Ok(()) 184 | } 185 | } 186 | 187 | /// A builder object to create a server. 188 | pub struct ServerBuilder { 189 | battle: Battle, 190 | authentication: bool, 191 | } 192 | 193 | impl ServerBuilder { 194 | /// Enforce authentication on all events sent by clients. 195 | /// Clients must present a valid `PlayerId` each time they want to send an event. 196 | pub fn enforce_authentication(mut self) -> Self { 197 | self.authentication = true; 198 | self 199 | } 200 | 201 | /// Creates a new server. 202 | pub fn build(self) -> Server { 203 | Server { 204 | battle: self.battle, 205 | client_sinks: MultiClientSink::new(), 206 | authentication: self.authentication, 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/user.rs: -------------------------------------------------------------------------------- 1 | //! User defined extension for battle rules functionalities. 2 | 3 | use crate::battle::BattleRules; 4 | #[cfg(feature = "serialization")] 5 | use crate::error::{WeaselError, WeaselResult}; 6 | #[cfg(feature = "serialization")] 7 | use crate::event::Event; 8 | #[cfg(feature = "serialization")] 9 | use serde::{Deserialize, Serialize}; 10 | use std::fmt::Debug; 11 | use std::hash::Hash; 12 | 13 | /// Numerical identifier to distinguish user events. 14 | pub type UserEventId = u16; 15 | 16 | /// Rules to extend some aspects of the battle with user defined behavior. 17 | pub trait UserRules { 18 | /// See [UserMetricId](type.UserMetricId.html). 19 | type UserMetricId: Eq + Hash + Clone + Debug + Send; 20 | #[cfg(feature = "serialization")] 21 | /// See [UserEventPackage](type.UserEventPackage.html). 22 | type UserEventPackage: UserEventPacker; 23 | } 24 | 25 | /// Id of user defined metrics. 26 | pub type UserMetricId = <::UR as UserRules>::UserMetricId; 27 | 28 | #[cfg(feature = "serialization")] 29 | /// Type containing the data to serialize and deserialize all defined user events.\ 30 | /// Use `()` if you didn't define any user event. 31 | pub type UserEventPackage = <::UR as UserRules>::UserEventPackage; 32 | 33 | #[cfg(feature = "serialization")] 34 | /// Stores one user event payload and manages its serialization/deserialization. 35 | pub trait UserEventPacker: Serialize + for<'a> Deserialize<'a> 36 | where 37 | R: BattleRules, 38 | { 39 | /// Returns a boxed trait object version of this packed user event. 40 | /// 41 | /// Returns an error if the conversion failed. 42 | fn boxed(self) -> WeaselResult + Send>, R>; 43 | 44 | /// Returns a UserEventPacker corresponding to the user event contained inside `event`. 45 | /// 46 | /// Fails if `event` is not an user event or if the conversion failed. 47 | fn flattened(event: Box + Send>) -> WeaselResult; 48 | } 49 | 50 | #[cfg(feature = "serialization")] 51 | impl UserEventPacker for () 52 | where 53 | R: BattleRules, 54 | { 55 | fn boxed(self) -> WeaselResult + Send>, R> { 56 | Err(WeaselError::UserEventUnpackingError( 57 | "empty UserEventPacker".into(), 58 | )) 59 | } 60 | 61 | fn flattened(event: Box + Send>) -> WeaselResult { 62 | Err(WeaselError::UserEventPackingError( 63 | event.clone(), 64 | "empty UserEventPacker".into(), 65 | )) 66 | } 67 | } 68 | 69 | #[cfg(feature = "serialization")] 70 | #[cfg(test)] 71 | mod tests { 72 | use super::*; 73 | use crate::event::{DummyEvent, EventTrigger}; 74 | use crate::{battle_rules, rules::empty::*}; 75 | 76 | #[test] 77 | fn empty_user_event_packer() { 78 | battle_rules! {} 79 | let result: WeaselResult<_, CustomRules> = ().boxed(); 80 | assert!(result.is_err()); 81 | let dummy = DummyEvent::::trigger(&mut ()).event(); 82 | let result: WeaselResult<_, CustomRules> = <()>::flattened(dummy); 83 | assert!(result.is_err()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | //! Collection of utilities. 2 | 3 | use indexmap::IndexMap; 4 | #[cfg(feature = "serialization")] 5 | use serde::{Deserialize, Serialize}; 6 | use std::fmt::Debug; 7 | use std::hash::Hash; 8 | 9 | /// Trait for an object that can provide an Id for itself. 10 | pub trait Id { 11 | #[cfg(not(feature = "serialization"))] 12 | /// Type of the id value. 13 | type Id: Hash + Eq + Clone + Debug + Send; 14 | #[cfg(feature = "serialization")] 15 | /// Type of the id value. 16 | type Id: Hash + Eq + Clone + Debug + Send + Serialize + for<'a> Deserialize<'a>; 17 | 18 | /// Returns a reference to the current id. 19 | fn id(&self) -> &Self::Id; 20 | } 21 | 22 | /// Collects an iterator into an indexmap. 23 | /// Subsequent values with same key are ignored. 24 | pub(crate) fn collect_from_iter( 25 | it: I, 26 | ) -> IndexMap<<::Item as Id>::Id, ::Item> 27 | where 28 | I: Iterator, 29 | ::Item: Id, 30 | { 31 | let mut map = IndexMap::new(); 32 | for e in it { 33 | if !map.contains_key(e.id()) { 34 | map.insert(e.id().clone(), e); 35 | } 36 | } 37 | map 38 | } 39 | 40 | /// Creates a server from the given battlerules. 41 | #[cfg(test)] 42 | pub(crate) mod tests { 43 | use crate::battle::{Battle, BattleRules}; 44 | use crate::creature::{CreateCreature, CreatureId}; 45 | use crate::event::{DefaultOutput, DummyEvent, EventProcessor, EventTrigger}; 46 | use crate::object::{CreateObject, ObjectId}; 47 | use crate::server::Server; 48 | use crate::space::Position; 49 | use crate::team::{CreateTeam, TeamId}; 50 | 51 | pub(crate) fn server(rules: R) -> Server { 52 | let battle = Battle::builder(rules).build(); 53 | Server::builder(battle).build() 54 | } 55 | 56 | /// Creates a team with default arguments. 57 | pub(crate) fn team<'a, R: BattleRules + 'static>(server: &'a mut Server, id: TeamId) { 58 | assert_eq!(CreateTeam::trigger(server, id).fire().err(), None); 59 | } 60 | 61 | /// Creates a creature with default arguments. 62 | pub(crate) fn creature<'a, R: BattleRules + 'static>( 63 | server: &'a mut Server, 64 | creature_id: CreatureId, 65 | team_id: TeamId, 66 | position: Position, 67 | ) { 68 | assert_eq!( 69 | CreateCreature::trigger(server, creature_id, team_id, position) 70 | .fire() 71 | .err(), 72 | None 73 | ); 74 | } 75 | 76 | /// Creates an object with default arguments. 77 | pub(crate) fn object<'a, R: BattleRules + 'static>( 78 | server: &'a mut Server, 79 | object_id: ObjectId, 80 | position: Position, 81 | ) { 82 | assert_eq!( 83 | CreateObject::trigger(server, object_id, position) 84 | .fire() 85 | .err(), 86 | None 87 | ); 88 | } 89 | 90 | /// Dummy event. 91 | pub(crate) fn dummy(processor: &mut P) 92 | where 93 | R: BattleRules + 'static, 94 | P: EventProcessor, 95 | { 96 | assert_eq!(DummyEvent::trigger(processor).fire().err(), None); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/ability_test.rs: -------------------------------------------------------------------------------- 1 | use weasel::ability::ActivateAbility; 2 | use weasel::actor::{Action, ActorRules}; 3 | use weasel::battle::{Battle, BattleController, BattleRules, BattleState}; 4 | use weasel::entity::EntityId; 5 | use weasel::entropy::Entropy; 6 | use weasel::event::{DummyEvent, EventKind, EventQueue, EventRights, EventServer, EventTrigger}; 7 | use weasel::metric::WriteMetrics; 8 | use weasel::player::PlayerId; 9 | use weasel::rules::empty::EmptyAbility; 10 | use weasel::{ 11 | battle_rules, battle_rules_with_actor, rules::empty::*, Server, WeaselError, WeaselResult, 12 | }; 13 | 14 | const TEAM_1_ID: u32 = 1; 15 | const TEAM_2_ID: u32 = 2; 16 | const CREATURE_1_ID: u32 = 1; 17 | const ENTITY_1_ID: EntityId = EntityId::Creature(CREATURE_1_ID); 18 | const CREATURE_ERR_ID: u32 = 5; 19 | const ENTITY_ERR_ID: EntityId = EntityId::Creature(CREATURE_ERR_ID); 20 | const ABILITY_ID: u32 = 1; 21 | const ABILITY_ERR_ID: u32 = 5; 22 | const PLAYER_1_ID: PlayerId = 1; 23 | 24 | #[derive(Default)] 25 | pub struct CustomActorRules {} 26 | 27 | impl ActorRules for CustomActorRules { 28 | type Ability = EmptyAbility; 29 | type AbilitiesSeed = u32; 30 | type Activation = u32; 31 | type AbilitiesAlteration = (); 32 | 33 | fn generate_abilities( 34 | &self, 35 | _: &Option, 36 | _entropy: &mut Entropy, 37 | _metrics: &mut WriteMetrics, 38 | ) -> Box> { 39 | let v = vec![EmptyAbility { id: ABILITY_ID }]; 40 | Box::new(v.into_iter()) 41 | } 42 | 43 | fn activable( 44 | &self, 45 | _state: &BattleState, 46 | action: Action, 47 | ) -> WeaselResult<(), CustomRules> { 48 | if action.activation.is_some() { 49 | Ok(()) 50 | } else { 51 | Err(WeaselError::GenericError) 52 | } 53 | } 54 | 55 | fn activate( 56 | &self, 57 | _state: &BattleState, 58 | action: Action, 59 | mut event_queue: &mut Option>, 60 | _entropy: &mut Entropy, 61 | _metrics: &mut WriteMetrics, 62 | ) { 63 | let count = action.activation.unwrap(); 64 | for _ in 0..count { 65 | DummyEvent::trigger(&mut event_queue).fire(); 66 | } 67 | } 68 | } 69 | 70 | battle_rules_with_actor! { CustomActorRules } 71 | 72 | #[test] 73 | fn abilities_generated() { 74 | // Create a server with a creature. 75 | let mut server = util::server(CustomRules::new()); 76 | util::team(&mut server, TEAM_1_ID); 77 | util::creature(&mut server, CREATURE_1_ID, TEAM_1_ID, ()); 78 | // Verify that abilities were generated. 79 | assert_eq!( 80 | server 81 | .battle() 82 | .entities() 83 | .actor(&ENTITY_1_ID) 84 | .unwrap() 85 | .abilities() 86 | .count(), 87 | 1 88 | ); 89 | } 90 | 91 | #[test] 92 | fn ability_activation() { 93 | // Create a server with a creature. 94 | let mut server = util::server(CustomRules::new()); 95 | util::team(&mut server, TEAM_1_ID); 96 | util::creature(&mut server, CREATURE_1_ID, TEAM_1_ID, ()); 97 | // Ability done by a missing creature should fail. 98 | assert_eq!( 99 | ActivateAbility::trigger(&mut server, ENTITY_ERR_ID, ABILITY_ID) 100 | .fire() 101 | .err() 102 | .map(|e| e.unfold()), 103 | Some(WeaselError::EntityNotFound(ENTITY_ERR_ID)) 104 | ); 105 | // Fail when creature has not started the turn. 106 | assert_eq!( 107 | ActivateAbility::trigger(&mut server, ENTITY_1_ID, ABILITY_ID) 108 | .fire() 109 | .err() 110 | .map(|e| e.unfold()), 111 | Some(WeaselError::ActorNotReady(ENTITY_1_ID)) 112 | ); 113 | // Start a turn. 114 | util::start_turn(&mut server, &ENTITY_1_ID); 115 | // Fail when creature does not know the ability. 116 | assert_eq!( 117 | ActivateAbility::trigger(&mut server, ENTITY_1_ID, ABILITY_ERR_ID) 118 | .fire() 119 | .err() 120 | .map(|e| e.unfold()), 121 | Some(WeaselError::AbilityNotKnown(ENTITY_1_ID, ABILITY_ERR_ID)) 122 | ); 123 | // Fail when `activable` returns false. 124 | assert_eq!( 125 | ActivateAbility::trigger(&mut server, ENTITY_1_ID, ABILITY_ID) 126 | .fire() 127 | .err() 128 | .map(|e| e.unfold()), 129 | Some(WeaselError::AbilityNotActivable( 130 | ENTITY_1_ID, 131 | ABILITY_ID, 132 | Box::new(WeaselError::GenericError) 133 | )) 134 | ); 135 | // Succeed in activating an ability. 136 | assert_eq!( 137 | ActivateAbility::trigger(&mut server, ENTITY_1_ID, ABILITY_ID) 138 | .activation(2) 139 | .fire() 140 | .err(), 141 | None 142 | ); 143 | let events = server.battle().history().events(); 144 | assert!(events.len() >= 2); 145 | assert_eq!(events[events.len() - 2].kind(), EventKind::DummyEvent); 146 | assert_eq!(events[events.len() - 1].kind(), EventKind::DummyEvent); 147 | assert_eq!(events[events.len() - 2].origin(), Some(3)); 148 | assert_eq!(events[events.len() - 1].origin(), Some(3)); 149 | } 150 | 151 | #[test] 152 | fn ability_rights() { 153 | // Create a server with a creature. Require authentication. 154 | let mut server = Server::builder(Battle::builder(CustomRules::new()).build()) 155 | .enforce_authentication() 156 | .build(); 157 | util::team(&mut server, TEAM_1_ID); 158 | util::creature(&mut server, CREATURE_1_ID, TEAM_1_ID, ()); 159 | // Create another team. 160 | util::team(&mut server, TEAM_2_ID); 161 | // Give to the player rights to the team without any creature. 162 | assert_eq!(server.rights_mut().add(PLAYER_1_ID, &TEAM_2_ID).err(), None); 163 | // Check event rights. 164 | util::start_turn(&mut server, &ENTITY_1_ID); 165 | let event = ActivateAbility::trigger(&mut server, ENTITY_1_ID, ABILITY_ID) 166 | .activation(2) 167 | .prototype() 168 | .client_prototype(0, Some(PLAYER_1_ID)); 169 | assert_eq!( 170 | event.event().rights(server.battle()), 171 | EventRights::Team(&TEAM_1_ID) 172 | ); 173 | // Ability activation should be rejected. 174 | assert_eq!( 175 | server 176 | .process_client(event.clone()) 177 | .err() 178 | .map(|e| e.unfold()), 179 | Some(WeaselError::AuthenticationError( 180 | Some(PLAYER_1_ID), 181 | TEAM_1_ID 182 | )) 183 | ); 184 | // Give rights to the player. 185 | assert_eq!(server.rights_mut().add(PLAYER_1_ID, &TEAM_1_ID).err(), None); 186 | // Check that now he can activate the ability. 187 | assert_eq!(server.process_client(event).err(), None); 188 | } 189 | -------------------------------------------------------------------------------- /tests/actor_test.rs: -------------------------------------------------------------------------------- 1 | use weasel::actor::{Actor, ActorRules, AlterAbilities}; 2 | use weasel::battle::{BattleController, BattleRules, BattleState}; 3 | use weasel::battle_rules_with_actor; 4 | use weasel::entity::EntityId; 5 | use weasel::entropy::Entropy; 6 | use weasel::event::{EventKind, EventQueue, EventTrigger}; 7 | use weasel::metric::WriteMetrics; 8 | use weasel::rules::empty::EmptyAbility; 9 | use weasel::space::MoveEntity; 10 | use weasel::{battle_rules, rules::empty::*}; 11 | 12 | const TEAM_1_ID: u32 = 1; 13 | const CREATURE_1_ID: u32 = 1; 14 | const ENTITY_1_ID: EntityId = EntityId::Creature(CREATURE_1_ID); 15 | 16 | #[derive(Default)] 17 | pub struct CustomActorRules {} 18 | 19 | impl ActorRules for CustomActorRules { 20 | type Ability = EmptyAbility; 21 | type AbilitiesSeed = u32; 22 | type Activation = u32; 23 | type AbilitiesAlteration = (); 24 | 25 | fn on_turn_start( 26 | &self, 27 | _state: &BattleState, 28 | actor: &dyn Actor, 29 | mut event_queue: &mut Option>, 30 | _entropy: &mut Entropy, 31 | _metrics: &mut WriteMetrics, 32 | ) { 33 | MoveEntity::trigger( 34 | &mut event_queue, 35 | actor.entity_id().clone(), 36 | actor.position().clone(), 37 | ) 38 | .fire(); 39 | } 40 | 41 | fn on_turn_end( 42 | &self, 43 | _state: &BattleState, 44 | actor: &dyn Actor, 45 | mut event_queue: &mut Option>, 46 | _entropy: &mut Entropy, 47 | _metrics: &mut WriteMetrics, 48 | ) { 49 | MoveEntity::trigger( 50 | &mut event_queue, 51 | actor.entity_id().clone(), 52 | actor.position().clone(), 53 | ) 54 | .fire(); 55 | } 56 | } 57 | 58 | battle_rules_with_actor! { CustomActorRules } 59 | 60 | #[test] 61 | fn turn_start_and_end() { 62 | // Create a new creature. 63 | let mut server = util::server(CustomRules::new()); 64 | util::team(&mut server, TEAM_1_ID); 65 | util::creature(&mut server, CREATURE_1_ID, TEAM_1_ID, ()); 66 | // Start a turn, by the rules a move entity event should have been spawned. 67 | util::start_turn(&mut server, &ENTITY_1_ID); 68 | { 69 | let events = server.battle().history().events(); 70 | assert_eq!(events[2].kind(), EventKind::StartTurn); 71 | assert_eq!(events[3].kind(), EventKind::MoveEntity); 72 | } 73 | // End the turn, by the rules another move entity event should have been spawned. 74 | util::end_turn(&mut server); 75 | { 76 | let events = server.battle().history().events(); 77 | assert_eq!(events[4].kind(), EventKind::EndTurn); 78 | assert_eq!(events[5].kind(), EventKind::MoveEntity); 79 | } 80 | } 81 | 82 | #[test] 83 | fn default_works() { 84 | battle_rules! {} 85 | // Create a server with a creature. 86 | let mut server = util::server(CustomRules::new()); 87 | util::team(&mut server, TEAM_1_ID); 88 | util::creature(&mut server, CREATURE_1_ID, TEAM_1_ID, ()); 89 | // Empty AlterAbilities with default rules does not return an error. 90 | assert_eq!( 91 | AlterAbilities::trigger(&mut server, EntityId::Creature(CREATURE_1_ID), ()) 92 | .fire() 93 | .err(), 94 | None 95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /tests/battle_test.rs: -------------------------------------------------------------------------------- 1 | use weasel::ability::ActivateAbility; 2 | use weasel::actor::{Action, ActorRules}; 3 | use weasel::battle::{BattleController, BattlePhase, BattleRules, BattleState, EndBattle}; 4 | use weasel::battle_rules_with_actor; 5 | use weasel::entity::EntityId; 6 | use weasel::entropy::Entropy; 7 | use weasel::event::{DummyEvent, EventQueue, EventTrigger}; 8 | use weasel::metric::WriteMetrics; 9 | use weasel::round::{EndTurn, StartTurn}; 10 | use weasel::rules::empty::EmptyAbility; 11 | use weasel::WeaselError; 12 | use weasel::{battle_rules, rules::empty::*}; 13 | 14 | const TEAM_1_ID: u32 = 1; 15 | const CREATURE_1_ID: u32 = 1; 16 | const ENTITY_1_ID: EntityId = EntityId::Creature(CREATURE_1_ID); 17 | const ABILITY_ID: u32 = 1; 18 | 19 | #[derive(Default)] 20 | pub struct CustomActorRules {} 21 | 22 | impl ActorRules for CustomActorRules { 23 | type Ability = EmptyAbility; 24 | type AbilitiesSeed = u32; 25 | type Activation = u32; 26 | type AbilitiesAlteration = (); 27 | 28 | fn generate_abilities( 29 | &self, 30 | _: &Option, 31 | _entropy: &mut Entropy, 32 | _metrics: &mut WriteMetrics, 33 | ) -> Box> { 34 | let v = vec![EmptyAbility { id: ABILITY_ID }]; 35 | Box::new(v.into_iter()) 36 | } 37 | 38 | fn activate( 39 | &self, 40 | _state: &BattleState, 41 | _action: Action, 42 | mut event_queue: &mut Option>, 43 | _entropy: &mut Entropy, 44 | _metrics: &mut WriteMetrics, 45 | ) { 46 | DummyEvent::trigger(&mut event_queue).fire(); 47 | EndBattle::trigger(&mut event_queue).fire(); 48 | EndTurn::trigger(&mut event_queue).fire(); 49 | } 50 | } 51 | 52 | battle_rules_with_actor! { CustomActorRules } 53 | 54 | #[test] 55 | fn end_battle() { 56 | // Create the scenario. 57 | let mut server = util::server(CustomRules::new()); 58 | util::team(&mut server, TEAM_1_ID); 59 | util::creature(&mut server, CREATURE_1_ID, TEAM_1_ID, ()); 60 | assert_eq!(server.battle().phase(), BattlePhase::Started); 61 | // End the battle and checks that new events aren't accepted. 62 | assert_eq!(EndBattle::trigger(&mut server).fire().err(), None); 63 | assert_eq!( 64 | StartTurn::trigger(&mut server, ENTITY_1_ID) 65 | .fire() 66 | .err() 67 | .map(|e| e.unfold()), 68 | Some(WeaselError::BattleEnded) 69 | ); 70 | assert_eq!(server.battle().phase(), BattlePhase::Ended); 71 | } 72 | 73 | #[test] 74 | fn end_battle_during_events() { 75 | // Create the scenario. 76 | let mut server = util::server(CustomRules::new()); 77 | util::team(&mut server, TEAM_1_ID); 78 | util::creature(&mut server, CREATURE_1_ID, TEAM_1_ID, ()); 79 | assert_eq!(server.battle().phase(), BattlePhase::Started); 80 | util::start_turn(&mut server, &ENTITY_1_ID); 81 | // Fire an ability that creates a dummy, an endbattle and an EndTurn. 82 | // Last event should have been rejected. 83 | assert_eq!( 84 | ActivateAbility::trigger(&mut server, ENTITY_1_ID, ABILITY_ID) 85 | .fire() 86 | .err() 87 | .map(|e| e.unfold()), 88 | Some(WeaselError::BattleEnded) 89 | ); 90 | assert_eq!(server.battle().phase(), BattlePhase::Ended); 91 | } 92 | -------------------------------------------------------------------------------- /tests/character_test.rs: -------------------------------------------------------------------------------- 1 | use weasel::battle::{BattleController, BattleRules}; 2 | use weasel::character::{AlterStatistics, Character}; 3 | use weasel::entity::EntityId; 4 | use weasel::event::EventTrigger; 5 | use weasel::status::InflictStatus; 6 | use weasel::{battle_rules, rules::empty::*}; 7 | 8 | const TEAM_1_ID: u32 = 1; 9 | const CREATURE_1_ID: u32 = 1; 10 | const STATUS_1_ID: u32 = 1; 11 | 12 | #[test] 13 | fn default_works() { 14 | battle_rules! {} 15 | // Create a server with a creature. 16 | let mut server = util::server(CustomRules::new()); 17 | util::team(&mut server, TEAM_1_ID); 18 | util::creature(&mut server, CREATURE_1_ID, TEAM_1_ID, ()); 19 | // Empty AlterStatistics with default rules does not return an error. 20 | assert_eq!( 21 | AlterStatistics::trigger(&mut server, EntityId::Creature(CREATURE_1_ID), ()) 22 | .fire() 23 | .err(), 24 | None 25 | ); 26 | // Empty rules don't add any status. 27 | assert_eq!( 28 | InflictStatus::trigger(&mut server, EntityId::Creature(CREATURE_1_ID), STATUS_1_ID) 29 | .fire() 30 | .err(), 31 | None 32 | ); 33 | assert_eq!( 34 | server 35 | .battle() 36 | .entities() 37 | .creature(&CREATURE_1_ID) 38 | .unwrap() 39 | .statuses() 40 | .count(), 41 | 0 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /tests/entity_test.rs: -------------------------------------------------------------------------------- 1 | use weasel::battle::{BattleController, BattleRules}; 2 | use weasel::entity::{Entity, EntityId}; 3 | use weasel::{battle_rules, rules::empty::*, WeaselError}; 4 | 5 | const TEAM_1_ID: u32 = 1; 6 | const CREATURE_1_ID: u32 = 1; 7 | const CREATURE_2_ID: u32 = 2; 8 | const OBJECT_1_ID: u32 = 1; 9 | const OBJECT_2_ID: u32 = 2; 10 | const ENTITY_C1_ID: EntityId = EntityId::Creature(CREATURE_1_ID); 11 | const ENTITY_C2_ID: EntityId = EntityId::Creature(CREATURE_2_ID); 12 | const ENTITY_O1_ID: EntityId = EntityId::Object(OBJECT_1_ID); 13 | const ENTITY_O2_ID: EntityId = EntityId::Object(OBJECT_2_ID); 14 | 15 | battle_rules! {} 16 | 17 | #[test] 18 | fn entity_id_methods() { 19 | // Create the battle. 20 | let mut server = util::server(CustomRules::new()); 21 | // Create a team. 22 | util::team(&mut server, TEAM_1_ID); 23 | // Create two creatures. 24 | util::creature(&mut server, CREATURE_1_ID, TEAM_1_ID, ()); 25 | util::creature(&mut server, CREATURE_2_ID, TEAM_1_ID, ()); 26 | // Create two objects. 27 | util::object(&mut server, OBJECT_1_ID, ()); 28 | util::object(&mut server, OBJECT_2_ID, ()); 29 | // Test is_* methods. 30 | let creature_id = server 31 | .battle() 32 | .entities() 33 | .creature(&CREATURE_1_ID) 34 | .unwrap() 35 | .entity_id(); 36 | let object_id = server 37 | .battle() 38 | .entities() 39 | .object(&OBJECT_1_ID) 40 | .unwrap() 41 | .entity_id(); 42 | assert!(creature_id.is_character()); 43 | assert!(object_id.is_character()); 44 | assert!(creature_id.is_actor()); 45 | assert!(!object_id.is_actor()); 46 | // Test extracting the concrete id. 47 | assert_eq!(creature_id.creature(), Ok(CREATURE_1_ID)); 48 | assert_eq!( 49 | creature_id.object().err(), 50 | Some(WeaselError::NotAnObject(ENTITY_C1_ID)) 51 | ); 52 | assert_eq!( 53 | object_id.creature().err(), 54 | Some(WeaselError::NotACreature(ENTITY_O1_ID)) 55 | ); 56 | assert_eq!(object_id.object(), Ok(OBJECT_1_ID)); 57 | } 58 | 59 | #[test] 60 | fn entity_id_equality() { 61 | assert_eq!(ENTITY_C1_ID, ENTITY_C1_ID); 62 | assert_ne!(ENTITY_C1_ID, ENTITY_C2_ID); 63 | assert_eq!(ENTITY_O1_ID, ENTITY_O1_ID); 64 | assert_ne!(ENTITY_O1_ID, ENTITY_O2_ID); 65 | assert_ne!(ENTITY_O1_ID, ENTITY_C1_ID); 66 | } 67 | -------------------------------------------------------------------------------- /tests/entropy_test.rs: -------------------------------------------------------------------------------- 1 | use weasel::actor::{Actor, ActorRules}; 2 | use weasel::battle::{Battle, BattleController, BattleRules}; 3 | use weasel::character::{Character, CharacterRules}; 4 | use weasel::entropy::{Entropy, ResetEntropy}; 5 | use weasel::event::EventTrigger; 6 | use weasel::metric::WriteMetrics; 7 | use weasel::rules::ability::SimpleAbility; 8 | use weasel::rules::entropy::UniformDistribution; 9 | use weasel::rules::statistic::SimpleStatistic; 10 | use weasel::server::Server; 11 | use weasel::{battle_rules, rules::empty::*}; 12 | 13 | #[cfg(feature = "serialization")] 14 | mod helper; 15 | 16 | const SEED: u64 = 1_204_678_643_940_597_513; 17 | const TEAM_1_ID: u32 = 1; 18 | const CREATURE_1_ID: u32 = 1; 19 | const STAT_ID: u32 = 1; 20 | const STAT_VALUE_MIN: i32 = 1; 21 | const STAT_VALUE_MAX: i32 = 1000; 22 | const STAT_VALUE: i32 = 820; 23 | const ABILITY_ID: u32 = 1; 24 | const ABILITY_POWER_MIN: i32 = 1; 25 | const ABILITY_POWER_MAX: i32 = 1000; 26 | const ABILITY_POWER: i32 = 33; 27 | 28 | #[derive(Default)] 29 | pub struct CustomCharacterRules {} 30 | 31 | impl CharacterRules for CustomCharacterRules { 32 | type CreatureId = u32; 33 | type ObjectId = (); 34 | type Statistic = SimpleStatistic; 35 | type StatisticsSeed = (); 36 | type StatisticsAlteration = (); 37 | type Status = EmptyStatus; 38 | type StatusesAlteration = (); 39 | 40 | fn generate_statistics( 41 | &self, 42 | _seed: &Option, 43 | entropy: &mut Entropy, 44 | _metrics: &mut WriteMetrics, 45 | ) -> Box> { 46 | let value = entropy.generate(STAT_VALUE_MIN, STAT_VALUE_MAX); 47 | let v = vec![SimpleStatistic::new(STAT_ID, value)]; 48 | Box::new(v.into_iter()) 49 | } 50 | } 51 | 52 | #[derive(Default)] 53 | pub struct CustomActorRules {} 54 | 55 | impl ActorRules for CustomActorRules { 56 | type Ability = SimpleAbility; 57 | type AbilitiesSeed = (); 58 | type Activation = i32; 59 | type AbilitiesAlteration = (); 60 | 61 | fn generate_abilities( 62 | &self, 63 | _: &Option, 64 | entropy: &mut Entropy, 65 | _metrics: &mut WriteMetrics, 66 | ) -> Box> { 67 | let power = entropy.generate(ABILITY_POWER_MIN, ABILITY_POWER_MAX); 68 | let v = vec![SimpleAbility::new(ABILITY_ID, power)]; 69 | Box::new(v.into_iter()) 70 | } 71 | } 72 | 73 | battle_rules! { 74 | EmptyTeamRules, 75 | CustomCharacterRules, 76 | CustomActorRules, 77 | EmptyFightRules, 78 | EmptyUserRules, 79 | EmptySpaceRules, 80 | EmptyRoundsRules, 81 | UniformDistribution 82 | } 83 | 84 | /// Creates a scenario with a custom entropy model, one team and a creature. 85 | macro_rules! scenario { 86 | () => {{ 87 | // Create the battle. 88 | let battle = Battle::builder(CustomRules::new()).build(); 89 | let mut server = Server::builder(battle).build(); 90 | assert_eq!( 91 | ResetEntropy::trigger(&mut server).seed(SEED).fire().err(), 92 | None 93 | ); 94 | // Create a team. 95 | util::team(&mut server, TEAM_1_ID); 96 | // Create a creature. 97 | util::creature(&mut server, CREATURE_1_ID, TEAM_1_ID, ()); 98 | server 99 | }}; 100 | } 101 | 102 | /// Checks that statistics and abilities have been randomized as predicted. 103 | macro_rules! stat_abi_randomness_check { 104 | ($server: expr) => {{ 105 | let creature = $server.battle().entities().creature(&CREATURE_1_ID); 106 | assert!(creature.is_some()); 107 | let creature = creature.unwrap(); 108 | assert_eq!(creature.statistic(&STAT_ID).unwrap().value(), STAT_VALUE); 109 | assert_eq!( 110 | creature.ability(&ABILITY_ID).unwrap().power(), 111 | ABILITY_POWER 112 | ); 113 | }}; 114 | } 115 | 116 | #[test] 117 | fn use_entropy() { 118 | let server = scenario!(); 119 | // Check that statistics and abilities have been randomized. 120 | stat_abi_randomness_check!(server); 121 | } 122 | 123 | #[cfg(feature = "serialization")] 124 | #[test] 125 | fn entropy_reload() { 126 | let server = scenario!(); 127 | // Check that statistics and abilities have been randomized. 128 | stat_abi_randomness_check!(server); 129 | // Save the battle. 130 | let history_json = helper::history_as_json(server.battle()); 131 | // Restore the battle. 132 | let mut server = util::server(CustomRules::new()); 133 | helper::load_json_history(&mut server, history_json); 134 | // Verify that randomization is the same. 135 | stat_abi_randomness_check!(server); 136 | } 137 | -------------------------------------------------------------------------------- /tests/fight_test.rs: -------------------------------------------------------------------------------- 1 | use weasel::ability::ActivateAbility; 2 | use weasel::actor::{Action, Actor, ActorRules, AlterAbilities}; 3 | use weasel::battle::{BattleController, BattleRules, BattleState}; 4 | use weasel::character::{AlterStatistics, Character, CharacterRules}; 5 | use weasel::entity::{EntityId, Transmutation}; 6 | use weasel::entropy::Entropy; 7 | use weasel::event::{EventKind, EventQueue, EventTrigger}; 8 | use weasel::fight::{ApplyImpact, FightRules}; 9 | use weasel::metric::WriteMetrics; 10 | use weasel::rules::ability::SimpleAbility; 11 | use weasel::rules::statistic::SimpleStatistic; 12 | use weasel::{battle_rules, rules::empty::*}; 13 | 14 | const TEAM_1_ID: u32 = 1; 15 | const CREATURE_1_ID: u32 = 1; 16 | const CREATURE_2_ID: u32 = 2; 17 | const ENTITY_1_ID: EntityId = EntityId::Creature(CREATURE_1_ID); 18 | const ENTITY_2_ID: EntityId = EntityId::Creature(CREATURE_2_ID); 19 | const ABILITY_ID: u32 = 1; 20 | const POWER: i32 = 1; 21 | const HEALTH: i32 = 10; 22 | const HEALTH_ID: &str = "health"; 23 | 24 | #[derive(Default)] 25 | pub struct CustomCharacterRules {} 26 | 27 | impl CharacterRules for CustomCharacterRules { 28 | type CreatureId = u32; 29 | type ObjectId = (); 30 | type Statistic = SimpleStatistic; 31 | type StatisticsSeed = (); 32 | type StatisticsAlteration = i32; 33 | type Status = EmptyStatus; 34 | type StatusesAlteration = (); 35 | 36 | fn generate_statistics( 37 | &self, 38 | _: &Option, 39 | _entropy: &mut Entropy, 40 | _metrics: &mut WriteMetrics, 41 | ) -> Box> { 42 | let v = vec![SimpleStatistic::new(HEALTH_ID.to_string(), HEALTH)]; 43 | Box::new(v.into_iter()) 44 | } 45 | 46 | fn alter_statistics( 47 | &self, 48 | character: &mut dyn Character, 49 | alteration: &Self::StatisticsAlteration, 50 | _entropy: &mut Entropy, 51 | _metrics: &mut WriteMetrics, 52 | ) -> Option { 53 | let health = character.statistic(&HEALTH_ID.to_string()).unwrap().value(); 54 | character 55 | .statistic_mut(&HEALTH_ID.to_string()) 56 | .unwrap() 57 | .set_value(health - *alteration); 58 | None 59 | } 60 | } 61 | 62 | #[derive(Default)] 63 | pub struct CustomActorRules {} 64 | 65 | impl ActorRules for CustomActorRules { 66 | type Ability = SimpleAbility; 67 | type AbilitiesSeed = (); 68 | type Activation = (); 69 | type AbilitiesAlteration = i32; 70 | 71 | fn generate_abilities( 72 | &self, 73 | _: &Option, 74 | _entropy: &mut Entropy, 75 | _metrics: &mut WriteMetrics, 76 | ) -> Box> { 77 | let v = vec![SimpleAbility::new(ABILITY_ID, POWER)]; 78 | Box::new(v.into_iter()) 79 | } 80 | 81 | fn activate( 82 | &self, 83 | _state: &BattleState, 84 | action: Action, 85 | mut event_queue: &mut Option>, 86 | _entropy: &mut Entropy, 87 | _metrics: &mut WriteMetrics, 88 | ) { 89 | AlterAbilities::trigger(&mut event_queue, ENTITY_1_ID, 0).fire(); 90 | ApplyImpact::trigger(&mut event_queue, action.ability.power() * 2).fire(); 91 | } 92 | 93 | fn alter_abilities( 94 | &self, 95 | actor: &mut dyn Actor, 96 | alteration: &Self::AbilitiesAlteration, 97 | _entropy: &mut Entropy, 98 | _metrics: &mut WriteMetrics, 99 | ) { 100 | actor 101 | .ability_mut(&ABILITY_ID) 102 | .unwrap() 103 | .set_power(*alteration); 104 | } 105 | } 106 | 107 | #[derive(Default)] 108 | pub struct CustomFightRules {} 109 | 110 | impl FightRules for CustomFightRules { 111 | type Impact = i32; 112 | type Potency = (); 113 | 114 | fn apply_impact( 115 | &self, 116 | _state: &BattleState, 117 | impact: &Self::Impact, 118 | mut event_queue: &mut Option>, 119 | _entropy: &mut Entropy, 120 | _metrics: &mut WriteMetrics, 121 | ) { 122 | AlterStatistics::trigger(&mut event_queue, ENTITY_2_ID, *impact * 2).fire(); 123 | } 124 | } 125 | 126 | battle_rules! { 127 | EmptyTeamRules, 128 | CustomCharacterRules, 129 | CustomActorRules, 130 | CustomFightRules, 131 | EmptyUserRules, 132 | EmptySpaceRules, 133 | EmptyRoundsRules, 134 | EmptyEntropyRules 135 | } 136 | 137 | #[test] 138 | fn simple_attack() { 139 | // Create scenario. 140 | let mut server = util::server(CustomRules::new()); 141 | util::team(&mut server, TEAM_1_ID); 142 | util::creature(&mut server, CREATURE_1_ID, TEAM_1_ID, ()); 143 | util::creature(&mut server, CREATURE_2_ID, TEAM_1_ID, ()); 144 | // Start a turn. 145 | util::start_turn(&mut server, &ENTITY_1_ID); 146 | // Fire ability. 147 | assert_eq!( 148 | ActivateAbility::trigger(&mut server, ENTITY_1_ID, ABILITY_ID) 149 | .fire() 150 | .err(), 151 | None 152 | ); 153 | // Check outcome of ability. 154 | // Attacker should have his ability's power set to zero. 155 | let creature = server.battle().entities().creature(&CREATURE_1_ID).unwrap(); 156 | assert_eq!(creature.ability(&ABILITY_ID).unwrap().power(), 0); 157 | // Defender should have received damage equal to twice the impact's power, which is 158 | // twice the ability's power (in total x4). 159 | let creature = server.battle().entities().creature(&CREATURE_2_ID).unwrap(); 160 | assert_eq!( 161 | creature.statistic(&HEALTH_ID.to_string()).unwrap().value(), 162 | HEALTH - POWER * 4 163 | ); 164 | // Check events origin. 165 | let events = server.battle().history().events(); 166 | assert_eq!(events[4].kind(), EventKind::ActivateAbility); 167 | assert_eq!(events[5].kind(), EventKind::AlterAbilities); 168 | assert_eq!(events[5].origin(), Some(4)); 169 | assert_eq!(events[6].kind(), EventKind::ApplyImpact); 170 | assert_eq!(events[6].origin(), Some(4)); 171 | assert_eq!(events[7].kind(), EventKind::AlterStatistics); 172 | assert_eq!(events[7].origin(), Some(6)); 173 | } 174 | 175 | #[test] 176 | fn default_works() { 177 | battle_rules! {} 178 | let mut server = util::server(CustomRules::new()); 179 | // ApplyImpact with default rules does not return an error. 180 | assert_eq!(ApplyImpact::trigger(&mut server, ()).fire().err(), None); 181 | } 182 | -------------------------------------------------------------------------------- /tests/helper.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "serialization")] 2 | use weasel::battle::{Battle, BattleRules}; 3 | #[cfg(feature = "serialization")] 4 | use weasel::event::EventReceiver; 5 | #[cfg(feature = "serialization")] 6 | use weasel::serde::FlatVersionedEvent; 7 | 8 | #[cfg(feature = "serialization")] 9 | /// Serializes the history of a battle into a json string. 10 | pub fn history_as_json(battle: &Battle) -> String 11 | where 12 | R: BattleRules + 'static, 13 | { 14 | let events: Vec> = battle 15 | .versioned_events(std::ops::Range { 16 | start: 0, 17 | end: battle.history().len() as usize, 18 | }) 19 | .map(|e| e.into()) 20 | .collect(); 21 | serde_json::to_string(&events).unwrap() 22 | } 23 | 24 | #[cfg(feature = "serialization")] 25 | /// Loads a history stored as json into an event receiver. 26 | pub fn load_json_history(receiver: &mut T, json: String) 27 | where 28 | R: BattleRules + 'static, 29 | T: EventReceiver, 30 | { 31 | let events: Vec> = serde_json::from_str(&json).unwrap(); 32 | for event in events { 33 | receiver.receive(event.into()).unwrap(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/history_test.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | use weasel::battle::{BattleController, BattleRules}; 3 | use weasel::entropy::ResetEntropy; 4 | use weasel::event::{EventId, EventKind, EventTrigger}; 5 | use weasel::round::EndTurn; 6 | use weasel::{battle_rules, rules::empty::*}; 7 | 8 | const TEAM_1_ID: u32 = 1; 9 | const CREATURE_1_ID: u32 = 1; 10 | 11 | battle_rules! {} 12 | 13 | #[test] 14 | fn timeline_populated() { 15 | // Create a server with a creature. 16 | let mut server = util::server(CustomRules::new()); 17 | util::team(&mut server, TEAM_1_ID); 18 | util::creature(&mut server, CREATURE_1_ID, TEAM_1_ID, ()); 19 | // Create some more faulty events. 20 | assert!(EndTurn::trigger(&mut server).fire().is_err()); 21 | // Create some more good events. 22 | assert_eq!(ResetEntropy::trigger(&mut server).fire().err(), None); 23 | // Verify if the events are in the timeline. 24 | let events = server.battle().history().events(); 25 | let len: EventId = events.len().try_into().unwrap(); 26 | assert_eq!(len, 3); 27 | assert_eq!(server.battle().history().len(), 3); 28 | assert_eq!(events[0].kind(), EventKind::CreateTeam); 29 | assert_eq!(events[1].kind(), EventKind::CreateCreature); 30 | assert_eq!(events[2].kind(), EventKind::ResetEntropy); 31 | assert_eq!(events[2].id(), len - 1); 32 | } 33 | -------------------------------------------------------------------------------- /tests/space_test.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use weasel::battle::{BattleController, BattleRules}; 3 | use weasel::battle_rules_with_space; 4 | use weasel::creature::CreateCreature; 5 | use weasel::entity::{Entities, Entity, EntityId}; 6 | use weasel::event::{EventQueue, EventTrigger}; 7 | use weasel::metric::WriteMetrics; 8 | use weasel::round::Rounds; 9 | use weasel::server::Server; 10 | use weasel::space::{AlterSpace, MoveEntity, PositionClaim, ResetSpace, SpaceRules}; 11 | use weasel::{battle_rules, rules::empty::*, WeaselError, WeaselResult}; 12 | 13 | const TEAM_1_ID: u32 = 1; 14 | const CREATURE_1_ID: u32 = 1; 15 | const ENTITY_1_ID: EntityId = EntityId::Creature(CREATURE_1_ID); 16 | const CREATURE_2_ID: u32 = 2; 17 | const OBJECT_1_ID: u32 = 1; 18 | const POSITION_1: u32 = 1; 19 | const POSITION_2: u32 = 2; 20 | const POSITION_T: u32 = 99; 21 | 22 | #[derive(Default)] 23 | struct CustomSpaceRules {} 24 | 25 | impl SpaceRules for CustomSpaceRules { 26 | type Position = u32; 27 | type SpaceSeed = (); 28 | type SpaceModel = HashSet; 29 | type SpaceAlteration = Self::Position; 30 | 31 | fn generate_model(&self, _: &Option) -> Self::SpaceModel { 32 | HashSet::new() 33 | } 34 | 35 | fn check_move( 36 | &self, 37 | model: &Self::SpaceModel, 38 | _claim: PositionClaim, 39 | position: &Self::Position, 40 | ) -> WeaselResult<(), CustomRules> { 41 | if !model.contains(position) { 42 | Ok(()) 43 | } else { 44 | Err(WeaselError::GenericError) 45 | } 46 | } 47 | 48 | fn move_entity( 49 | &self, 50 | model: &mut Self::SpaceModel, 51 | claim: PositionClaim, 52 | position: Option<&Self::Position>, 53 | _metrics: &mut WriteMetrics, 54 | ) { 55 | if let Some(position) = position { 56 | if let PositionClaim::Movement(entity) = claim { 57 | model.remove(entity.position()); 58 | } 59 | model.insert(*position); 60 | } 61 | } 62 | 63 | fn translate_entity( 64 | &self, 65 | _model: &Self::SpaceModel, 66 | new_model: &mut Self::SpaceModel, 67 | entity: &mut dyn Entity, 68 | _event_queue: &mut Option>, 69 | _metrics: &mut WriteMetrics, 70 | ) { 71 | // All entities go to POSITION_T when changing from one space to another. 72 | new_model.insert(POSITION_T); 73 | entity.set_position(POSITION_T); 74 | } 75 | 76 | fn alter_space( 77 | &self, 78 | _entities: &Entities, 79 | _rounds: &Rounds, 80 | model: &mut Self::SpaceModel, 81 | alteration: &Self::SpaceAlteration, 82 | _event_queue: &mut Option>, 83 | _metrics: &mut WriteMetrics, 84 | ) { 85 | // Make the position inside 'alteration' inaccessible. 86 | model.insert(*alteration); 87 | } 88 | } 89 | 90 | battle_rules_with_space! { CustomSpaceRules } 91 | 92 | fn init_custom_game() -> Server { 93 | let mut server = util::server(CustomRules::new()); 94 | util::team(&mut server, TEAM_1_ID); 95 | // Create a first creature in position 1. 96 | util::creature(&mut server, CREATURE_1_ID, TEAM_1_ID, POSITION_1); 97 | assert!(server 98 | .battle() 99 | .entities() 100 | .creature(&CREATURE_1_ID) 101 | .is_some()); 102 | server 103 | } 104 | 105 | #[test] 106 | fn position_verified() { 107 | let mut server = init_custom_game(); 108 | // Try to create a second creature again in position 1. 109 | assert_eq!( 110 | CreateCreature::trigger(&mut server, CREATURE_2_ID, TEAM_1_ID, POSITION_1) 111 | .fire() 112 | .err() 113 | .map(|e| e.unfold()), 114 | Some(WeaselError::PositionError( 115 | None, 116 | POSITION_1, 117 | Box::new(WeaselError::GenericError) 118 | )) 119 | ); 120 | assert!(server 121 | .battle() 122 | .entities() 123 | .creature(&CREATURE_2_ID) 124 | .is_none()); 125 | } 126 | 127 | #[test] 128 | fn move_entity() { 129 | let mut server = init_custom_game(); 130 | // Move the creature into an invalid position. 131 | assert_eq!( 132 | MoveEntity::trigger(&mut server, ENTITY_1_ID, POSITION_1) 133 | .fire() 134 | .err() 135 | .map(|e| e.unfold()), 136 | Some(WeaselError::PositionError( 137 | Some(POSITION_1), 138 | POSITION_1, 139 | Box::new(WeaselError::GenericError) 140 | )) 141 | ); 142 | assert_eq!( 143 | *server 144 | .battle() 145 | .entities() 146 | .entity(&ENTITY_1_ID) 147 | .unwrap() 148 | .position(), 149 | POSITION_1 150 | ); 151 | // Move the creature into a valid position. 152 | assert_eq!( 153 | MoveEntity::trigger(&mut server, ENTITY_1_ID, POSITION_2) 154 | .fire() 155 | .err(), 156 | None 157 | ); 158 | assert_eq!( 159 | *server 160 | .battle() 161 | .entities() 162 | .entity(&ENTITY_1_ID) 163 | .unwrap() 164 | .position(), 165 | POSITION_2 166 | ); 167 | assert_eq!(server.battle().space().model().len(), 1); 168 | } 169 | 170 | #[test] 171 | fn move_object() { 172 | let mut server = init_custom_game(); 173 | // Create an object. 174 | util::object(&mut server, OBJECT_1_ID, POSITION_2); 175 | // Move the object into an invalid position. 176 | assert_eq!( 177 | MoveEntity::trigger(&mut server, EntityId::Object(OBJECT_1_ID), POSITION_2) 178 | .fire() 179 | .err() 180 | .map(|e| e.unfold()), 181 | Some(WeaselError::PositionError( 182 | Some(POSITION_2), 183 | POSITION_2, 184 | Box::new(WeaselError::GenericError) 185 | )) 186 | ); 187 | // Move the object into a valid position. 188 | assert_eq!( 189 | MoveEntity::trigger(&mut server, EntityId::Object(OBJECT_1_ID), POSITION_T) 190 | .fire() 191 | .err(), 192 | None 193 | ); 194 | } 195 | 196 | #[test] 197 | fn reset_space() { 198 | // Create a scenario. 199 | let mut server = init_custom_game(); 200 | // Change the space model. 201 | assert_eq!(ResetSpace::trigger(&mut server).fire().err(), None); 202 | // Check that entity's position changed. 203 | assert_eq!( 204 | *server 205 | .battle() 206 | .entities() 207 | .entity(&ENTITY_1_ID) 208 | .unwrap() 209 | .position(), 210 | POSITION_T 211 | ); 212 | assert_eq!(server.battle().space().model().len(), 1); 213 | } 214 | 215 | #[test] 216 | fn alter_space() { 217 | // Create a scenario. 218 | let mut server = init_custom_game(); 219 | // Alter the space model, invalidating position 2. 220 | assert_eq!( 221 | AlterSpace::trigger(&mut server, POSITION_2).fire().err(), 222 | None 223 | ); 224 | // Check that the creature can't move into position 2 anymore. 225 | assert_eq!( 226 | MoveEntity::trigger(&mut server, ENTITY_1_ID, POSITION_2) 227 | .fire() 228 | .err() 229 | .map(|e| e.unfold()), 230 | Some(WeaselError::PositionError( 231 | Some(POSITION_1), 232 | POSITION_2, 233 | Box::new(WeaselError::GenericError) 234 | )) 235 | ); 236 | } 237 | -------------------------------------------------------------------------------- /utilities/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "util" 3 | version = "0.1.0" 4 | authors = ["trisfald"] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | weasel = { path = "../" } 9 | -------------------------------------------------------------------------------- /utilities/src/lib.rs: -------------------------------------------------------------------------------- 1 | use weasel::battle::{Battle, BattleRules}; 2 | use weasel::client::Client; 3 | use weasel::creature::{CreateCreature, CreatureId}; 4 | use weasel::entity::EntityId; 5 | use weasel::event::{DefaultOutput, DummyEvent, EventProcessor, EventTrigger, ServerSink}; 6 | use weasel::object::{CreateObject, ObjectId}; 7 | use weasel::round::{EndTurn, StartTurn}; 8 | use weasel::server::Server; 9 | use weasel::space::Position; 10 | use weasel::team::{CreateTeam, TeamId}; 11 | 12 | /// Creates a server from the given battlerules. 13 | pub fn server(rules: R) -> Server { 14 | let battle = Battle::builder(rules).build(); 15 | Server::builder(battle).build() 16 | } 17 | 18 | /// Creates a client from the given battlerules. 19 | pub fn client(rules: R, server_sink: S) -> Client 20 | where 21 | R: BattleRules + 'static, 22 | S: ServerSink + 'static + Send, 23 | { 24 | let battle = Battle::builder(rules).build(); 25 | Client::builder(battle, Box::new(server_sink)).build() 26 | } 27 | 28 | /// Creates a team with default arguments. 29 | pub fn team(processor: &mut P, id: TeamId) 30 | where 31 | R: BattleRules + 'static, 32 | P: EventProcessor, 33 | { 34 | assert_eq!(CreateTeam::trigger(processor, id).fire().err(), None); 35 | } 36 | 37 | /// Creates a creature with default arguments. 38 | pub fn creature<'a, R, P>( 39 | processor: &'a mut P, 40 | creature_id: CreatureId, 41 | team_id: TeamId, 42 | position: Position, 43 | ) where 44 | R: BattleRules + 'static, 45 | P: EventProcessor, 46 | { 47 | assert_eq!( 48 | CreateCreature::trigger(processor, creature_id, team_id, position) 49 | .fire() 50 | .err(), 51 | None 52 | ); 53 | } 54 | 55 | /// Creates an object with default arguments. 56 | pub fn object<'a, R: BattleRules + 'static>( 57 | server: &'a mut Server, 58 | object_id: ObjectId, 59 | position: Position, 60 | ) { 61 | assert_eq!( 62 | CreateObject::trigger(server, object_id, position) 63 | .fire() 64 | .err(), 65 | None 66 | ); 67 | } 68 | 69 | /// Starts a turn with the given entity. 70 | pub fn start_turn<'a, R, P>(processor: &'a mut P, id: &EntityId) 71 | where 72 | R: BattleRules + 'static, 73 | P: EventProcessor, 74 | { 75 | assert_eq!(StartTurn::trigger(processor, id.clone()).fire().err(), None); 76 | } 77 | 78 | /// Ends the turn. 79 | pub fn end_turn(processor: &mut P) 80 | where 81 | R: BattleRules + 'static, 82 | P: EventProcessor, 83 | { 84 | assert_eq!(EndTurn::trigger(processor).fire().err(), None); 85 | } 86 | 87 | /// Dummy event. 88 | pub fn dummy(processor: &mut P) 89 | where 90 | R: BattleRules + 'static, 91 | P: EventProcessor, 92 | { 93 | assert_eq!(DummyEvent::trigger(processor).fire().err(), None); 94 | } 95 | --------------------------------------------------------------------------------