{"id":18306150,"url":"https://github.com/devbridie/wizardsandwarriors","last_synced_at":"2025-07-01T14:34:24.879Z","repository":{"id":72990079,"uuid":"116886660","full_name":"devbridie/wizardsandwarriors","owner":"devbridie","description":"Implementation and write-up of my main take-aways of Eric Lippert's article series Wizards and Warriors.","archived":false,"fork":false,"pushed_at":"2018-01-10T15:02:27.000Z","size":114,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-15T05:13:09.394Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://devbridie.github.io/wizardsandwarriors/","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/devbridie.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-01-10T00:24:00.000Z","updated_at":"2018-01-24T22:59:46.000Z","dependencies_parsed_at":null,"dependency_job_id":"7d54f69d-337b-42d8-93a4-700ab7209a97","html_url":"https://github.com/devbridie/wizardsandwarriors","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devbridie%2Fwizardsandwarriors","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devbridie%2Fwizardsandwarriors/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devbridie%2Fwizardsandwarriors/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devbridie%2Fwizardsandwarriors/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/devbridie","download_url":"https://codeload.github.com/devbridie/wizardsandwarriors/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248027112,"owners_count":21035586,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-05T15:37:55.849Z","updated_at":"2025-04-09T10:47:19.856Z","avatar_url":"https://github.com/devbridie.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# wizardsandwarriors\n[Documentation](https://devbridie.github.io/wizardsandwarriors/docs/)\n\n[Eric Lippert](https://ericlippert.com/) wrote an [article series called Wizards and Warriors](https://ericlippert.com/2015/04/27/wizards-and-warriors-part-one/)\nintroducing a problem in Object Orientated Programming that seems simple at first glance but quickly reveals itself to be\na complicated software engineering. I found the article very thought-provoking.\nIt inspired me to write this related work and code that [implements the rule system](https://devbridie.github.io/wizardsandwarriors/docs/com.devbridie.wizardsandwarriors.framework/) as proposed by Eric, as well as a [simple demonstration that uses this system](https://github.com/devbridie/wizardsandwarriors/blob/master/sample/src/main/kotlin/com/devbridie/wizardsandwarriors/sample/Main.kt) in a simple text adventure with two scenes. You can find the completed code [on GitHub](https://github.com/devbridie/wizardsandwarriors).\n\n## Introduction\n\n*If you've already read [Eric Lippert's Wizards and Warriors](https://ericlippert.com/2015/04/27/wizards-and-warriors-part-one/)\n(15 min.), you can probably skip to Implementation. Though I will attempt to summarize my main takeaways\nfrom the article below, I would still recommend that you read the original.*\n\nIt starts with a simple set of objects and is-a relationships (using a fantasy RPG genre game as an illustration).\n\n* A wizard is a kind of player.\n* A warrior is a kind of player.\n* A staff is a kind of weapon.\n* A sword is a kind of weapon.\n* A player has a weapon.\n\nIntuitively, you might think of the following code:\n\n```kotlin\nsealed class Weapon\nclass Staff : Weapon()\nclass Sword : Weapon()\n\nsealed class Player\nclass Wizard : Player()\nclass Warrior : Player()\n```\n\nThe relations listed above have been captured. Job well done.\n\nHowever, the following constraints are added to the system:\n\n* A Warrior can only use a Sword.\n* A Wizard can only use a Staff.\n\nEric then shows possible attempts at modeling such a system, of which I will summarize some.\n\n### 'Solution' with Subtyping\n```kotlin\nsealed class Weapon\nclass Staff : Weapon()\nclass Sword : Weapon()\n\nsealed class Player(open var weapon: Weapon)\nclass Wizard(override var weapon: Staff) : Player(weapon)\nclass Warrior(override var weapon: Sword) : Player(weapon)\n```\n\nThough this might seem sound, the Kotlin compiler will throw the following exception:\n`Kotlin: Type of 'weapon' doesn't match the type of the overridden var-property 'public open var weapon: Weapon defined in Player'`.\n\nIf the compiler did not throw this exception, then a `Wizard` casted to a `Player` can be assigned\na `Sword`, while the class definition for `Wizard` disallows this.\n\n### 'Solution' with Generics\n```kotlin\nsealed class Weapon\nclass Staff : Weapon()\nclass Sword : Weapon()\n\nsealed class Player\u003cT: Weapon\u003e(open var weapon: T)\nclass Wizard : Player\u003cStaff\u003e(weapon)\nclass Warrior : Player\u003cSword\u003e(weapon)\n```\n\nGreat! The compiler will protect us from assigning a `Sword` to a `Wizard`.\nBut this does not scale: what if we were to introduce class-specific armor?\nAnd what if our specifications add that everybody can also wield `Dagger`s?\n\n### Placement of Concerns\n\nEric then shows us a related problem: Suppose the problems above were solved in an ideal manner.\nOur game is extended with the ability to attack monsters given the following classes:\n\n```kotlin\nsealed class Player\nclass Warrior : Player()\n\nsealed class Monster\nclass Werewolf : Monster()\n```\n\n[To quote](https://ericlippert.com/2015/05/04/wizards-and-warriors-part-three/):\n\"Where exactly does the code go that determines whether the Warrior successfully hits the Werewolf?\"\n\"Somewhere there is a method, let’s call it Attack; what does that method look like?\"\n\nOr, more generally: *where should such logic belong?* Eric justly argues against the use of a\n[Visitor Pattern](https://github.com/dbacinski/Design-Patterns-In-Kotlin/blob/master/src/main/kotlin/Visitor.kt)\nin [his article](https://ericlippert.com/2015/05/04/wizards-and-warriors-part-three/): the notation is verbose and error-prone.\nAlso, it does not scale as the complexity of the system grows: one would have to continue to add parameters.\n\n## Rules\nIn [Part 5](https://ericlippert.com/2015/05/11/wizards-and-warriors-part-five/), Eric proposes a paradigm shift:\nthe business logic of this game can be modelled by a set of Rules.\nThis allows our examples to be expressed:\n\n* Warriors cannot wield Staffs.\n* Wizards cannot wield Swords.\n* Everybody can wield Daggers.\n\nWhat if a new weapon type is added? Then add new rules that represent the underlying business logic.\n\nFurthermore, the nature of exceptions are used to support the paradigm shift: they are used to handle errors and other exceptional events.\nIf a Wizard tries to wield a Sword, should that result in an Exception?\n\nEric proposes that the actual business logic is the following:\n\n* The core objects of the system are *users, commands, state,* and *rules*.\n* A user provides a sequence of *commands*.\n  * This models a request to perform an action in the system, possibly changing state.\n* These commands are evaluated in the context of a series of rules, possibly carrying parameters.\n  * Some are applicable to the current command (is logic concerning Daggers relevant to a Warrior wielding a Staff?)\n* These evaluations produce one or more *effects*.\n  * Examples of such an effect are 'do nothing', mutate state, display a message.\n  * Certain effects can be composited.\n\nAs I was curious to see how such a system could be implemented and used, I set out to create a framework utilizing these ideas.\n\n## Implementation\nI started with a simple goal in mind: implement the example game of Wizards and Warriors wielding Staffs, Swords, and Daggers. Using the [Inform7](http://inform7.com/) rules as in [part 5](https://ericlippert.com/2015/05/11/wizards-and-warriors-part-five/) as a starting point of what I would want the API to become.\n\n### Core Representation\nThe representation of the functionality that should be implemented would contain the following nouns: *Rule*, *Parameter*, *Effect*, *State*, and *System*.\n\n* A *Rule* is a small chunk of business logic. A rule is given some *Parameter*s and results in some *Effect*.\n* A *System* is the glue of the framework. It knows which *Rule*s exist, it handles commands, and it handles the resulting Effects. It also maintains a given *State*, dispatching mutations to this state where needed.\n\nThese resulted in [the rule framework](https://github.com/devbridie/wizardsandwarriors/tree/master/framework/src/main/kotlin/com/devbridie/wizardsandwarriors/framework).\n\n### Representing State\nI separated rules that represented definitions into classes. An example of this is the following:\n\n```Inform7\nA wizard is a kind of person.\nA warrior is a kind of person.\n```\n\nwould become:\n\n```kotlin\ndata class Person(var type: PersonType)\n\nsealed class PersonType(val name: String)\nobject Wizard : PersonType(\"Wizard\")\nobject Warrior : PersonType(\"Warrior\")\n```\n\nIn context of the game, these object definitions can be found in [sample objects](https://github.com/devbridie/wizardsandwarriors/tree/master/sample/src/main/kotlin/com/devbridie/wizardsandwarriors/sample/models).\n\n### Creating a WieldRule\nA concrete implementation of such a Rule is a [`WieldRule`](https://github.com/devbridie/wizardsandwarriors/blob/master/sample/src/main/kotlin/com/devbridie/wizardsandwarriors/sample/wield/WieldRulebook.kt). It will express what the effects of a command for a '`Person` to wield a `Weapon`' should be.\n\nThe notion of wielding a weapon concerns two instances: The wielder and the weapon being wielded. \nThen, abstractly, the applicability of the rule `When wielding a sword: if the wielder is not a warrior, it is too heavy.`\ncould be expressed as `weapon is Sword \u0026\u0026 wielder.type !is Warrior`. The effect of this rule would be `WeaponTooHeavyWieldEffect`.\nThis leads to the composition of the rule expressed in the following DSL [Type-Safe Builders](https://kotlinlang.org/docs/reference/type-safe-builders.html).:\n\n```kotlin\nwieldRule(\n    applicable = { weapon is Sword \u0026\u0026 person.type !is Warrior },\n    effect = { WeaponTooHeavyWieldEffect }\n)\n```\n\nAnd the 'normal' case of wielding a weapon:\n\n```kotlin\nwieldRule(\n    applicable = always(),\n    effect = { UpdateWeaponWieldEffect(this) }\n)\n```\n\nwhere `this` refers to the incoming `WieldParameter`s. The resolution of this Rule leads to an Effect that represents a `Wizard` wielding a `Staff`.\n\n### Rule Resolution\nHowever, resolution of these rules was not as simple as thought. Some Rules should be final in their chain;\nif one rule dictates that a `Wizard` may not wield a `Sword`, the default rule (wield the weapon) should not be resolved.\nThis led to the introduction of the `BreakChainEffect`, a tag that determines that no further rules should be resolved.\n\n[Rule resolution](https://github.com/devbridie/wizardsandwarriors/blob/90ffa9d4a9c12b3f4cfbdd2f7349db90f6b8ff4e/framework/src/main/kotlin/com/devbridie/wizardsandwarriors/framework/System.kt#L39) is done with using a modified functional reduce, mapping applicable Rules to Effects. When a `BreakChainEffect` is encountered, the reduce is stopped.\n\n### Stengths of the Implementation\nThe flexibility of the framework allows for arbitrarily complex rules and effects. \n\nEffects can be composed by creating Rules that take an Effect as Parameter. An example of this is displaying the result of a wield command: `WieldEffect -\u003e DisplayWieldEffectEffect`. In the context of a certain scene, a wield might result in a diffrerent display text. This allows the business logic (A warrior cannot wield a staff) to remain context-independent.\n\nRule composition can be created by creating new rules that combine other rules. An example of this is a conditional group that makes rules more specific:\n\n```kotlin\nconditional({ wieldParameters.person.type is Wizard }) {\n\t+shopSceneRule(\n\t\tapplicable = { wieldEffect is UpdateWeaponWieldEffect \u0026\u0026 wieldParameters.weapon is Staff },\n\t\teffect = { DisplayWieldEffectEffect(\"${wieldParameters.person} looks satisfied with his new ${wieldParameters.weapon}.\") }\n\t)\n\n\t+shopSceneRule(\n\t\tapplicable = { wieldEffect is UpdateWeaponWieldEffect \u0026\u0026 wieldParameters.weapon is Dagger },\n\t\teffect = { DisplayWieldEffectEffect(\"${wieldParameters.person} takes the ${wieldParameters.weapon} begrudgingly.\") }\n\t)\n}\n```\n\n### Demonstration\nCreative thinking led to the narrative shown in the [demonstration](https://github.com/devbridie/wizardsandwarriors/blob/master/sample/src/main/kotlin/com/devbridie/wizardsandwarriors/sample/Main.kt), demonstrating the basics of the rule system.\n\nWith this, the initial goal was completed.\n\n### Extensions\nSuppose I were a user of the framework: what features would I want? In context of game development, I immediately\nthought of expansion packs: What if a new class called Paladin were added to the game, Wizards and Warriors + Paladins? How could this new type of person and other related logic\nbe added to the system without disturbing existing logic?\n\nFor this, being able to nest rulebooks became a vital feature. In this way, related logic (What are Paladins allowed to wield? What special effects do they have when attacking?) can be kept together and added as one unit to an existing rulebook.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevbridie%2Fwizardsandwarriors","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdevbridie%2Fwizardsandwarriors","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevbridie%2Fwizardsandwarriors/lists"}