Consolidate all .allium specs into specs/

- Move .pi/specs/ files into specs/ (healing, factions, merged magical-objects)
- Move src/*.allium files into specs/ (levels, changing-level)
- Delete .pi/specs/ directory
- Document specs/ convention in AGENTS.md
This commit is contained in:
Willem van den Ende 2026-06-15 08:16:12 +01:00
parent 39839dc594
commit 31984bbd9d
7 changed files with 7 additions and 175 deletions

View File

@ -1,174 +0,0 @@
-- allium: 3
-- allium: magical-objects
------------------------------------------------------------
-- External Entities
------------------------------------------------------------
external entity Character {
name: String
health: Health
status: alive | dead
level: Level
factions: Set<Faction>
}
external entity Health {
value: Integer
}
external entity Level {
value: Integer
}
external entity Faction {
name: String
}
------------------------------------------------------------
-- Entities and Variants
------------------------------------------------------------
entity MagicalWeapon {
health: Health
maxHealth: Integer
status: alive | destroyed
damage: Integer
owner: Character
}
entity HealingObject {
health: Health
maxHealth: Integer
status: alive | destroyed
}
------------------------------------------------------------
-- Rules
------------------------------------------------------------
rule WeaponDealsDamage {
when: MagicalWeapon.dealsDamage(weapon, target, attacker)
requires: weapon.status = alive
requires: attacker = weapon.owner
requires: attacker.status = alive
ensures: target.health.value = max(0, target.health.value - weapon.damage)
ensures: weapon.health.value = weapon.health.value - 1
ensures:
if weapon.health.value - 1 = 0:
weapon.status = destroyed
else:
weapon.status = alive
ensures:
if max(0, target.health.value - weapon.damage) = 0:
target.status = dead
else:
target.status = alive
}
rule DeadCannotUseWeapon {
when: MagicalWeapon.dealsDamage(weapon, target, attacker)
requires: attacker.status = dead
ensures:
target.health.value = target.health.value
weapon.health.value = weapon.health.value
weapon.status = weapon.status
target.status = target.status
}
rule NonOwnerCannotUseWeapon {
when: MagicalWeapon.dealsDamage(weapon, target, attacker)
requires: attacker != weapon.owner
ensures:
target.health.value = target.health.value
weapon.health.value = weapon.health.value
weapon.status = weapon.status
target.status = target.status
}
rule DestroyedWeaponCannotDealDamage {
when: MagicalWeapon.dealsDamage(weapon, target, attacker)
requires: weapon.status = destroyed
ensures:
target.health.value = target.health.value
weapon.health.value = weapon.health.value
weapon.status = weapon.status
target.status = target.status
}
rule HealingObjectHealsCharacter {
when: HealingObject.healsCharacter(object, character, amount)
requires: object.status = alive
requires: character.status = alive
ensures: healAmount = min(amount, object.maxHealth - object.health.value)
ensures: character.health.value = character.health.value + healAmount
ensures: object.health.value = object.health.value - healAmount
ensures:
if object.health.value - healAmount = 0:
object.status = destroyed
else:
object.status = alive
}
rule DeadCannotUseHealingObject {
when: HealingObject.healsCharacter(object, character, amount)
requires: character.status = dead
ensures:
character.health.value = character.health.value
object.health.value = object.health.value
object.status = object.status
}
rule DestroyedHealingObjectCannotHeal {
when: HealingObject.healsCharacter(object, character, amount)
requires: object.status = destroyed
ensures:
character.health.value = character.health.value
object.health.value = object.health.value
object.status = object.status
}
------------------------------------------------------------
-- Invariants
------------------------------------------------------------
invariant WeaponHealthNeverNegative {
for w in MagicalWeapons:
w.health.value >= 0
}
invariant WeaponDestroyedAtZeroHealth {
for w in MagicalWeapons:
w.health.value = 0 implies w.status = destroyed
}
invariant WeaponMaxHealthNeverExceeded {
for w in MagicalWeapons:
w.health.value <= w.maxHealth
}
invariant HealingObjectHealthNeverNegative {
for h in HealingObjects:
h.health.value >= 0
}
invariant HealingObjectDestroyedAtZeroHealth {
for h in HealingObjects:
h.health.value = 0 implies h.status = destroyed
}
invariant HealingObjectMaxHealthNeverExceeded {
for h in HealingObjects:
h.health.value <= h.maxHealth
}
invariant HealingObjectCannotDealDamage {
for h in HealingObjects:
not h.dealsDamage(_, _)
}
invariant WeaponCannotHeal {
for w in MagicalWeapons:
not w.healsCharacter(_, _)
}

View File

@ -16,7 +16,7 @@ An implementation of the RPG Combat rules engine. There are six user stories des
This project combines three practices:
1. **Allium** (`.allium` specs) — formal behavioural specifications that capture _what_ the system does
1. **Allium** (`.allium` specs) — formal behavioural specifications that capture _what_ the system does. All specs live in [specs/](specs/) — one file per story/domain area.
2. **fast-check** — property-based testing that verifies those properties hold across thousands of random inputs
3. **"I can't believe it's not Haskell"** — TypeScript with ADTs, value objects, and immutability

View File

@ -136,6 +136,8 @@ rule DeadCannotUseWeapon {
ensures:
target.health.value = target.health.value
weapon.health.value = weapon.health.value
weapon.status = weapon.status
target.status = target.status
}
rule NonOwnerCannotUseWeapon {
@ -146,6 +148,8 @@ rule NonOwnerCannotUseWeapon {
ensures:
target.health.value = target.health.value
weapon.health.value = weapon.health.value
weapon.status = weapon.status
target.status = target.status
}
rule WeaponDestroyedCannotDealDamage {
@ -156,6 +160,8 @@ rule WeaponDestroyedCannotDealDamage {
ensures:
target.health.value = target.health.value
weapon.health.value = weapon.health.value
weapon.status = weapon.status
target.status = target.status
}
------------------------------------------------------------