diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c0e8e9d1..766bc8f1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,8 +1,12 @@
# This action continuously checks all pushes and Pull requests
# for validity, integrity and bugs in datafiles.
# For details, visit https://github.com/BSData/check-datafiles
-name: CI
-on: [ push, pull_request ]
+name: Datafile Basic Validity
+on:
+ push:
+ branches:
+ - main
+ pull_request:
jobs:
build:
runs-on: ubuntu-latest
diff --git a/.github/workflows/test-in-new-recruit.yml b/.github/workflows/test-in-new-recruit.yml
new file mode 100644
index 00000000..e2ae82e2
--- /dev/null
+++ b/.github/workflows/test-in-new-recruit.yml
@@ -0,0 +1,28 @@
+name: Test in New Recruit
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checking out game system to horus-heresy
+ uses: actions/checkout@v4
+ with:
+ path: horus-heresy
+ - name: Setting up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Installing package list
+ run: apt list --installed
+ # Need to fetch reqs if needed
+ - name: Installing all necessary packages
+ run: pip install webdriver-manager selenium
+ - name: Run tests
+ run: python3 tests.py
+ working-directory: horus-heresy/tests/
+ env:
+ DEFAULT_DATA_DIRECTORY: ${{ github.workspace }}
diff --git a/.gitignore b/.gitignore
index d50b6d4c..6ed22b59 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,5 +16,9 @@
!/.github
!/.github/**
+# Don't ignore our tests directory
+!/tests
+!/tests/**
+
# Don't ignore .yml for CI build definitions
!*.yml
diff --git a/README.md b/README.md
index 6754cace..000f1fa4 100644
--- a/README.md
+++ b/README.md
@@ -55,6 +55,24 @@ A .cattemplate file is a .cat file, renamed to .cattemplate, used by [BSCOPY](ht
We used bscopy to copy all 18 legions after implementing the first one.
We didn't maintain the template so it's not recommended to re-run bscopy
+## Tests
+GitHub actions will load configured lists in [tests](tests) and ensure they produce the expected outcome.
+To add a new test:
+1. Export a roster from NewRecruit or BattleScribe
+2. Rename that roster from .ros to .test and place it in [tests](tests)
+3. Add a new case to [tests.py](tests/tests.py):
+ ```python
+ def test_NameOfTest(self):
+ self.load_list('Name of Roster file with no extension')
+ errors = self.get_error_list()
+ self.assertEqual(0, len(errors), "This list has validation errors")
+ ```
+ * There are other tests, such as checking for points on a specific unit. Look through the code for examples.
+4. Run the unit tests with python, or create a pull request to have GitHub run them automatically.
+ * To run them locally, install python and the packages `selenium` and `webdriver-manager`, and Google Chrome.
+
+
+
## References
* Horus Heresy: Age of Darkness Rulebook
diff --git a/tests/.gitignore b/tests/.gitignore
new file mode 100644
index 00000000..3b045699
--- /dev/null
+++ b/tests/.gitignore
@@ -0,0 +1,6 @@
+
+# Ignore .ros files as they break appspot
+*.ros
+
+# Ignore pycache
+__pycache__
\ No newline at end of file
diff --git a/tests/Basic Marines Validate.test b/tests/Basic Marines Validate.test
new file mode 100644
index 00000000..e22ca307
--- /dev/null
+++ b/tests/Basic Marines Validate.test
@@ -0,0 +1,12 @@
+
+Some events/group choose not to allow official rules in "Legacies of The Age of Darkness" download pdf (which are not playtest). This option is included to make it easier for users for those events/groups.The Strength of Wisdom: When rolling To Hit for a model with this special rule as part of a Shooting Attack, add +1 to the result of the roll if the enemy unit targeted by the attack has already been the target of another friendly unit composed entirely of models with this special rule in the same Shooting phase, and if the attacking model is within 6" of a model from that friendly unit. This does not affect attacks made with the Blast or Barrage special rules.This Advanced Reaction is available only to units composed entirely of models with the Legiones Astartes (Ultramarines) special rule. Unlike Core Reactions, Advanced Reactions are activated in unique and specific circumstances, as noted in their descriptions, and can often have game changing effects. Advanced Reactions use up points of a Reactive player’s Reaction Allotment as normal and obey all other restrictions placed upon Reactions, unless it is specifically noted otherwise in their description.
+
+Unity of Purpose – This Advanced Reaction may be made once per battle during the opposing player’s Shooting phase when any enemy player declares a Shooting Attack targeting a friendly unit under the Reactive player’s control composed entirely of models with the Legiones Astartes (Ultramarines) special rule. Once the Active player has resolved all To Hit rolls, To Wound rolls, and Armour Saves are made, but before any Damage Mitigation rolls are made or casualties removed, the Reactive player may choose to expend one of their Reactions for that Phase to have both the unit targeted by the Shooting Attack and one other unit composed entirely of models with the Legiones Astartes (Ultramarines) special rule make a Shooting Attack, targeting the unit that triggered this Reaction and following all the usual rules for Shooting Attacks. Any unit that makes a Shooting Attack as part of a Unity of Purpose Reaction may not make any attacks indirectly (without line of sight) including Barrage weapons or other weapons or special rules that otherwise ignore line of sight, and models with the Vehicle Unit Type may only fire Defensive weapons. Template weapons may only be used as part of a Unity of Purpose Reaction if the target unit is within 8" and must use the Wall of Death special rule instead of firing normally. Both units that make Shooting Attacks as part of this Reaction are considered to have made a Reaction this Phase and as such may not make any further Reactions.An Infantry unit may only include or be joined by models of the Infantry or Primarch Unit Type, unless a special rule states otherwise.Relentless models can shoot with Heavy or Ordnance weapons, counting as Stationary, even if they moved in the previous Movement phase. They are also allowed to Charge in the same turn they fire Heavy, Ordnance, or Rapid Fire weapons.An Infantry unit may only include or be joined by models of the Infantry or Primarch Unit Type, unless a special rule states otherwise.Infantry (Character)7554425392+Grenades are represented in battle as Wargear items with a specific effect rather than as weapons. Using grenades does not count as a Shooting Attack and their effects are entirely covered by the rules presented here. Note that grenade launchers do not use these rules and are Shooting Weapons governed by the standard Shooting rules.A unit that includes at least one model with frag grenades makes attacks at its normal Initiative Step during an Assault after it has successfully Charged through Difficult Terrain or Dangerous Terrain, but still suffers any penalties to Charge rolls imposed by Difficult Terrain or Dangerous Terrain when resolving a Charge through Difficult Terrain or Dangerous Terrain.Grenades are represented in battle as Wargear items with a specific effect rather than as weapons. Using grenades does not count as a Shooting Attack and their effects are entirely covered by the rules presented here. Note that grenade launchers do not use these rules and are Shooting Weapons governed by the standard Shooting rules.The controlling player may choose to have a model with krak grenades that is Engaged or otherwise in base contact during the Assault phase with a Building or Fortification, or a model with the Vehicle, Dreadnought or Automata Unit Type, inflict one automatic Str 6, AP 3 Hit on the target in Initiative Step 1 instead of attacking normally. Any model in a unit that is chosen to inflict Hits using krak grenades may not otherwise attack or make use of any other special rule or item of Wargear that inflicts Hits or Wounds on a model in the same Assault phase (but may participate in Sweeping Advances as normal).A model with a refractor field gains a 5+ Invulnerable Save.
+Invulnerable Saves granted by a refractor field or iron halo do not stack with other Invulnerable Saves, but can benefit from rules (such as cyber-familiar) that specifically increase existing saves. If a model has another Invulnerable Save then the controlling player must choose which one to use.12"45Pistol 1If a model has the Shred special rule, or is attacking with a Melee weapon that has the Shred rule, it re-rolls failed To Wound rolls in close combat.
+
+Similarly, if a model makes a Shooting Attack with a weapon that has the Shred rule, it re-rolls its failed To Wound rolls.-User-Melee, ShredArtificer armour confers a 2+ Armour Save.Any combat with at least one friendly model within 12" of this Warlord, or a combat which includes this Warlord, gains a bonus of +1 to the number of Wounds caused for the purposes of combat resolution. In addition, an army whose Warlord has this Trait may make an additional Reaction during their opponent’s Assault phase as long as the Warlord has not been removed as a casualty.If a model with this special rule has not moved or Run during the Movement phase of its controlling player’s turn then that model may add one to the number of shots fired when making a Shooting Attack with a bolter (this does not include combi-bolters, bolt pistols or other bolt weapons).When a unit that includes at least one model with this special rule has at least half of its models within 6" of an Objective then all models in the unit gain the Feel No Pain (6+) and Stubborn special rules. If any model in the unit already has a variant of the Feel No Pain special rule then instead increase the value in brackets of one of those rules by +1 while the unit has at least half of its models within 6" of an Objective (for example, a model that already had Feel No Pain (5+) could choose to increase this to Feel No Pain (4+) while it fulfils the conditions of this special rule).• A unit that includes at least one model with the Line sub-type counts as both a Scoring and Denial unit.• A unit that includes at least one model with the Line sub-type counts as both a Scoring and Denial unit.Infantry (Character, Line)7444414283+12"45Pistol 124"45Rapid FirePower armour provides a 3+ Armour Save.Power armour provides a 3+ Armour Save.Grenades are represented in battle as Wargear items with a specific effect rather than as weapons. Using grenades does not count as a Shooting Attack and their effects are entirely covered by the rules presented here. Note that grenade launchers do not use these rules and are Shooting Weapons governed by the standard Shooting rules.A unit that includes at least one model with frag grenades makes attacks at its normal Initiative Step during an Assault after it has successfully Charged through Difficult Terrain or Dangerous Terrain, but still suffers any penalties to Charge rolls imposed by Difficult Terrain or Dangerous Terrain when resolving a Charge through Difficult Terrain or Dangerous Terrain.Grenades are represented in battle as Wargear items with a specific effect rather than as weapons. Using grenades does not count as a Shooting Attack and their effects are entirely covered by the rules presented here. Note that grenade launchers do not use these rules and are Shooting Weapons governed by the standard Shooting rules.The controlling player may choose to have a model with krak grenades that is Engaged or otherwise in base contact during the Assault phase with a Building or Fortification, or a model with the Vehicle, Dreadnought or Automata Unit Type, inflict one automatic Str 6, AP 3 Hit on the target in Initiative Step 1 instead of attacking normally. Any model in a unit that is chosen to inflict Hits using krak grenades may not otherwise attack or make use of any other special rule or item of Wargear that inflicts Hits or Wounds on a model in the same Assault phase (but may participate in Sweeping Advances as normal).• A unit that includes at least one model with the Line sub-type counts as both a Scoring and Denial unit.Infantry (Line)7444414173+12"45Pistol 124"45Rapid FireIf a model with this special rule has not moved or Run during the Movement phase of its controlling player’s turn then that model may add one to the number of shots fired when making a Shooting Attack with a bolter (this does not include combi-bolters, bolt pistols or other bolt weapons).When a unit that includes at least one model with this special rule has at least half of its models within 6" of an Objective then all models in the unit gain the Feel No Pain (6+) and Stubborn special rules. If any model in the unit already has a variant of the Feel No Pain special rule then instead increase the value in brackets of one of those rules by +1 while the unit has at least half of its models within 6" of an Objective (for example, a model that already had Feel No Pain (5+) could choose to increase this to Feel No Pain (4+) while it fulfils the conditions of this special rule).• A unit that includes at least one model with the Line sub-type counts as both a Scoring and Denial unit.• A unit that includes at least one model with the Line sub-type counts as both a Scoring and Denial unit.Infantry (Character, Line)7444414283+12"45Pistol 124"45Rapid FirePower armour provides a 3+ Armour Save.Power armour provides a 3+ Armour Save.Grenades are represented in battle as Wargear items with a specific effect rather than as weapons. Using grenades does not count as a Shooting Attack and their effects are entirely covered by the rules presented here. Note that grenade launchers do not use these rules and are Shooting Weapons governed by the standard Shooting rules.A unit that includes at least one model with frag grenades makes attacks at its normal Initiative Step during an Assault after it has successfully Charged through Difficult Terrain or Dangerous Terrain, but still suffers any penalties to Charge rolls imposed by Difficult Terrain or Dangerous Terrain when resolving a Charge through Difficult Terrain or Dangerous Terrain.Grenades are represented in battle as Wargear items with a specific effect rather than as weapons. Using grenades does not count as a Shooting Attack and their effects are entirely covered by the rules presented here. Note that grenade launchers do not use these rules and are Shooting Weapons governed by the standard Shooting rules.The controlling player may choose to have a model with krak grenades that is Engaged or otherwise in base contact during the Assault phase with a Building or Fortification, or a model with the Vehicle, Dreadnought or Automata Unit Type, inflict one automatic Str 6, AP 3 Hit on the target in Initiative Step 1 instead of attacking normally. Any model in a unit that is chosen to inflict Hits using krak grenades may not otherwise attack or make use of any other special rule or item of Wargear that inflicts Hits or Wounds on a model in the same Assault phase (but may participate in Sweeping Advances as normal).• A unit that includes at least one model with the Line sub-type counts as both a Scoring and Denial unit.Infantry (Line)7444414173+12"45Pistol 124"45Rapid FireTo add Lords of War you now need to add the additional detachment to your list. To do this:
+
+A - On Mobile, after adding your initial detachment, press the + sign at the bottom left
+B - On Desktop after adding your first force then just press add force again
+Then choose the army you wish to have a lord of war from, then pick "Lord of War Detachment". This allows the choice of any LoW from any army as per the rules of HH2 (apart from the new Ruinstorm Deamons one can only be taken in a Lord of War Detachment for Ruinstorm Daemons).
+THIS IS A TEMPORARY NOTIFICATION THAT WILL BE REMOVED IN A FEW MONTHS WHEN HOPEFULLY EVERYONE WILL BE USED TO WHERE THE NEW LOCATION IS, AND I DON’T GET 100S OF BUG REPORTS FROM PEOPLE NOT BEING ABLE TO FIND THEIR LOW
\ No newline at end of file
diff --git a/tests/Dedicated Transport Squad Costs.test b/tests/Dedicated Transport Squad Costs.test
new file mode 100644
index 00000000..4e818506
--- /dev/null
+++ b/tests/Dedicated Transport Squad Costs.test
@@ -0,0 +1,22 @@
+
+Cult Arcana: All models with the Infantry or Cavalry Unit Type (but not those with the Artillery or Automated Artillery Sub-type) with this special rule gain the Psyker Sub-type (this does not grant any Disciplines, but does not otherwise remove any Discipline a model already has access to). In addition, all models with the Infantry or Cavalry Unit Type and and the Character Unit Sub-type that have this special rule must select one Minor Arcana option ( See the Prosperine Arcana special rule). Any model with the Infantry or or Cavalry Unit Types and both the independent Character and Legiones astartes (Thousand Sons) special rule that does not already have one or more Psychic Disciplines may be upgraded for +15 additional points to gain a single Psychic Discipline from the Core Psychic Discipline list (see the Horus Heresy Age of Darkness rulebook, page 322).This Advanced Reaction is available only to units composed entirely of models with both the Legiones Astartes (Thousand Sons) special rule and the Psyker Sub-type. Unlike Core Reactions, Advanced Reactions are activated in unique and specific circumstances, as noted in their descriptions, and can often have game changing effects. Advanced Reactions use up points of a Reactive player’s Reaction Allotment as normal and obey all other restrictions placed upon Reactions, unless it is specifically noted otherwise in their description.
+
+Fortress of the Mind – This Advanced Reaction may be made once per battle during the opposing player’s Shooting phase when any enemy unit declares a Shooting Attack targeting a unit under the Reactive player’s control, made up entirely of models with the Legiones Astartes (Thousand Sons) special rule and the Psyker Sub-type. Once the Active player has resolved all To Hit and To Wound rolls, but before any Armour Saves are made, the Reactive player must make a Psychic check. If the Check is passed, the Reacting unit gains a 3+ Invulnerable Save against all Wounds inflicted as part of the Shooting Attack that triggered the Reaction. If the Check is failed then the Reacting unit gains only a 5+ Invulnerable Save and both the attacking unit and the reacting unit suffer Perils of the Warp, removing any casualties immediately before resolving any unsaved Wounds inflicted by the Shooting Attack that triggered this Reaction.This Advanced Reaction may be made once per battle during the opposing player’s Shooting phase when any enemy unit declares a Shooting Attack targeting a unit under the Reactive player’s control, made up entirely of models with the Legiones Astartes (Thousand Sons) special rule and the Psyker Subtype. Once the Active player has resolved all To Hit and To Wound rolls, but before any Armour Saves are made, the Reactive player must make a Psychic check. If the Check is passed, the Reacting unit gains a 3+ Invulnerable Save against all Wounds inflicted as part of the Shooting Attack that triggered the Reaction. If the Check is failed then the Reacting unit gains only a 5+ Invulnerable Save and both the attacking unit and the reacting unit suffer Perils of the Warp, removing any casualties immediately before resolving any unsaved Wounds inflicted by the Shooting Attack that triggered this Reaction.A unit with this special rule may not be chosen as a compulsory choice for the army as part of the Force Organisation chart.Infantry (Character) (Psyker)7"444414283+When a unit that includes a Psyker with this power makes a Shooting Attack, the controlling player may make a Psychic check for the Psyker with this power before any rolls To Hit are made. or Reactions declared. If the Psychic check is successful, then the enemy unit targeted by the Shooting Attack must reduce the Leadership Characteristic of all models in the unit by 1 for any Pinning or Morale check caused by the Shooting Attack. If the Psychic check is failed then the Psyker suffers Perils of the Warp.12"45Pistol 1Power armour provides a 3+ Armour Save.Power armour provides a 3+ Armour Save.Grenades are represented in battle as Wargear items with a specific effect rather than as weapons. Using grenades does not count as a Shooting Attack and their effects are entirely covered by the rules presented here. Note that grenade launchers do not use these rules and are Shooting Weapons governed by the standard Shooting rules.The controlling player may choose to have a model with krak grenades that is Engaged or otherwise in base contact during the Assault phase with a Building or Fortification, or a model with the Vehicle, Dreadnought or Automata Unit Type, inflict one automatic Str 6, AP 3 Hit on the target in Initiative Step 1 instead of attacking normally. Any model in a unit that is chosen to inflict Hits using krak grenades may not otherwise attack or make use of any other special rule or item of Wargear that inflicts Hits or Wounds on a model in the same Assault phase (but may participate in Sweeping Advances as normal).Grenades are represented in battle as Wargear items with a specific effect rather than as weapons. Using grenades does not count as a Shooting Attack and their effects are entirely covered by the rules presented here. Note that grenade launchers do not use these rules and are Shooting Weapons governed by the standard Shooting rules.A unit that includes at least one model with frag grenades makes attacks at its normal Initiative Step during an Assault after it has successfully Charged through Difficult Terrain or Dangerous Terrain, but still suffers any penalties to Charge rolls imposed by Difficult Terrain or Dangerous Terrain when resolving a Charge through Difficult Terrain or Dangerous Terrain.If a model has the Rending special rule, or is attacking with a Melee weapon that has the Rending special rule, there is a chance that their close combat attacks will strike a critical blow. For each To Wound roll equal to or higher than the value listed, the target automatically suffers a Wound, regardless of its Toughness. The controlling player may choose to resolve these Wounds at AP 2 instead of the weapon’s normal AP value.
+Similarly, if a model makes a Shooting Attack with a weapon that has the Rending special rule, a To Wound roll of equal to or greater than the listed value wounds automatically, regardless of Toughness, and is resolved at AP 2.
+In either case, against Vehicles each Armour Penetration roll of equal to or greater than the listed value allows a further D3 to be rolled, with the result added to the total Strength of the attack. These Hits are not resolved at AP 2, but are instead resolved using the weapon’s AP value.
+For example, a model with the Rending (5+) special rule that rolls To Wound against a non-Vehicle model will wound automatically on the roll of a 5+, and the attacking player has the choice of using an AP value of 2 instead of the AP value of their weapon.The controlling player of any unit that includes one or more models with the Psyker Unit Sub-type and a weapon or ability with this special rule, may choose to activate this special rule before making any attacks with that weapon or resolving the ability. To activate this special rule, the controlling player must make a single Psychic check using the Leadership Characteristic of any model in the unit that does not have the Independent Character special rule (unless the unit
+is entirely composed of models with the Independent Character special rule, in which case the controlling player can select which of those model’s Leadership
+Characteristics is used). If the Check is successful, then the Strength value of all attacks made with weapons or abilites with this special rule is increased by +2 (In addition to any Modifiers the attack or weapon may already posses), this benefit is applied only in the Phase in which these attack are made and ends immediately after that phase is resolved. If the check is failed then no benefit is gained, but the models in that unit may attack as normal.
+
+All 'Achean Force' weapons are counted as 'Force" weapons for those rules that affect such weapons18"64Assault 2, Rending (6+), Achean ForceIn any of the controlling player’s Shooting phases, instead of making any Shooting Attacks with a model with this special rule, the controlling player may instead roll a D6. On the roll of a ‘4’ or more, a damage result of Immobilised that has been inflicted on this model may be removed but no Hull Points are returned.No model with any versions of the Bulky special rule may Embark on a model that has this special rule.Vehicle (Transport)144111110312A Legion Rhino Transport has one Access Point on each side of the hull and one at the rear.When attacking with a weapon that has this special rule, the controlling player may re-roll all failed To Hit rolls.
+
+Twin-linked Blast Weapons
+If the Scatter dice does not roll a Hit, you can choose to re-roll the dice when making a Shooting Attack with a Twin-linked Blast weapon. If you choose to do so, you must re-roll both the 2D6 and the Scatter dice.
+
+Twin-linked Template Weapons
+Twin-linked Template weapons are fired just like a single weapon, but must re-roll failed To Wound rolls and Armour Penetration rolls.24"45Rapid-fire, Twin-linkedThe controlling player may choose to trigger smoke launchers once a model with them has completed its movement in the Movement phase, and may only choose to trigger them if the model has moved no faster than Combat Speed that turn. Once triggered, the model with smoke launchers counts as being more than 25% obscured, regardless of terrain, until the start of the controlling player’s next turn and gains a 6+ Cover Save. A model whose smoke launchers have been triggered may not make any Shooting Attacks, except as part of a Reaction, in the same turn. Smoke launchers may only be used once per battle, and once triggered may not be further used – in addition, they do not count as a weapon and may not be targeted by Weapon Destroyed results on the Vehicle Damage table.Legionary (Infantry) (Psyker)744414173+12"45Pistol 1To add Lords of War you now need to add the additional detachment to your list. To do this:
+
+A - On Mobile, after adding your initial detachment, press the + sign at the bottom left
+B - On Desktop after adding your first force then just press add force again
+Then choose the army you wish to have a lord of war from, then pick "Lord of War Detachment". This allows the choice of any LoW from any army as per the rules of HH2 (apart from the new Ruinstorm Deamons one can only be taken in a Lord of War Detachment for Ruinstorm Daemons).
+THIS IS A TEMPORARY NOTIFICATION THAT WILL BE REMOVED IN A FEW MONTHS WHEN HOPEFULLY EVERYONE WILL BE USED TO WHERE THE NEW LOCATION IS, AND I DON’T GET 100S OF BUG REPORTS FROM PEOPLE NOT BEING ABLE TO FIND THEIR LOW
\ No newline at end of file
diff --git a/tests/Empty Validation Test.test b/tests/Empty Validation Test.test
new file mode 100644
index 00000000..f0f37a18
--- /dev/null
+++ b/tests/Empty Validation Test.test
@@ -0,0 +1,9 @@
+
+Cult Arcana: All models with the Infantry or Cavalry Unit Type (but not those with the Artillery or Automated Artillery Sub-type) with this special rule gain the Psyker Sub-type (this does not grant any Disciplines, but does not otherwise remove any Discipline a model already has access to). In addition, all models with the Infantry or Cavalry Unit Type and and the Character Unit Sub-type that have this special rule must select one Minor Arcana option ( See the Prosperine Arcana special rule). Any model with the Infantry or or Cavalry Unit Types and both the independent Character and Legiones astartes (Thousand Sons) special rule that does not already have one or more Psychic Disciplines may be upgraded for +15 additional points to gain a single Psychic Discipline from the Core Psychic Discipline list (see the Horus Heresy Age of Darkness rulebook, page 322).This Advanced Reaction is available only to units composed entirely of models with both the Legiones Astartes (Thousand Sons) special rule and the Psyker Sub-type. Unlike Core Reactions, Advanced Reactions are activated in unique and specific circumstances, as noted in their descriptions, and can often have game changing effects. Advanced Reactions use up points of a Reactive player’s Reaction Allotment as normal and obey all other restrictions placed upon Reactions, unless it is specifically noted otherwise in their description.
+
+Fortress of the Mind – This Advanced Reaction may be made once per battle during the opposing player’s Shooting phase when any enemy unit declares a Shooting Attack targeting a unit under the Reactive player’s control, made up entirely of models with the Legiones Astartes (Thousand Sons) special rule and the Psyker Sub-type. Once the Active player has resolved all To Hit and To Wound rolls, but before any Armour Saves are made, the Reactive player must make a Psychic check. If the Check is passed, the Reacting unit gains a 3+ Invulnerable Save against all Wounds inflicted as part of the Shooting Attack that triggered the Reaction. If the Check is failed then the Reacting unit gains only a 5+ Invulnerable Save and both the attacking unit and the reacting unit suffer Perils of the Warp, removing any casualties immediately before resolving any unsaved Wounds inflicted by the Shooting Attack that triggered this Reaction.This Advanced Reaction may be made once per battle during the opposing player’s Shooting phase when any enemy unit declares a Shooting Attack targeting a unit under the Reactive player’s control, made up entirely of models with the Legiones Astartes (Thousand Sons) special rule and the Psyker Subtype. Once the Active player has resolved all To Hit and To Wound rolls, but before any Armour Saves are made, the Reactive player must make a Psychic check. If the Check is passed, the Reacting unit gains a 3+ Invulnerable Save against all Wounds inflicted as part of the Shooting Attack that triggered the Reaction. If the Check is failed then the Reacting unit gains only a 5+ Invulnerable Save and both the attacking unit and the reacting unit suffer Perils of the Warp, removing any casualties immediately before resolving any unsaved Wounds inflicted by the Shooting Attack that triggered this Reaction.To add Lords of War you now need to add the additional detachment to your list. To do this:
+
+A - On Mobile, after adding your initial detachment, press the + sign at the bottom left
+B - On Desktop after adding your first force then just press add force again
+Then choose the army you wish to have a lord of war from, then pick "Lord of War Detachment". This allows the choice of any LoW from any army as per the rules of HH2 (apart from the new Ruinstorm Deamons one can only be taken in a Lord of War Detachment for Ruinstorm Daemons).
+THIS IS A TEMPORARY NOTIFICATION THAT WILL BE REMOVED IN A FEW MONTHS WHEN HOPEFULLY EVERYONE WILL BE USED TO WHERE THE NEW LOCATION IS, AND I DON’T GET 100S OF BUG REPORTS FROM PEOPLE NOT BEING ABLE TO FIND THEIR LOW
\ No newline at end of file
diff --git a/tests/tests.py b/tests/tests.py
new file mode 100644
index 00000000..acd2d5cb
--- /dev/null
+++ b/tests/tests.py
@@ -0,0 +1,144 @@
+import os
+import shutil
+import time
+import unittest
+from pathlib import Path
+
+from selenium import webdriver
+from selenium.common import TimeoutException
+from selenium.webdriver.common.by import By
+import selenium.webdriver.support.ui as ui
+import selenium.webdriver.support.expected_conditions as EC
+
+from selenium.webdriver.chrome.service import Service as ChromeService
+from webdriver_manager.chrome import ChromeDriverManager
+
+
+class GameTests(unittest.TestCase):
+ debug = False
+
+ def setUp(self):
+ options = webdriver.ChromeOptions()
+ if not self.debug:
+ options.add_argument('--headless')
+
+ driver = webdriver.Chrome(
+ service=ChromeService(ChromeDriverManager().install()),
+ options=options)
+ driver.delete_all_cookies()
+ self.wait = ui.WebDriverWait(driver, 30) # timeout after 30 seconds
+ self.driver = driver
+ driver.get("https://www.newrecruit.eu/app/MySystems")
+ print("Loading NR")
+
+ driver.execute_script('localStorage.setItem("local", "true")')
+ # seems to end up running before the system initializes, so we don't need to refresh
+
+ print("Waiting up to 30 seconds for the theme pop-up")
+ try:
+ theme_button_elements = self.wait.until(lambda drv:
+ drv.find_elements(By.XPATH, "//*[text()='Close']"))
+ if len(theme_button_elements) > 0:
+ print("Skipping the theme pop-up")
+ theme_button_elements[0].click()
+ except TimeoutException:
+ print("No theme pop-up to skip")
+ self.load_system('horus-heresy')
+
+ def load_system(self, system_name):
+ default_data_directory = os.getenv("DEFAULT_DATA_DIRECTORY", os.path.expanduser("~/BattleScribe/data/"))
+ self.game_directory = str(os.path.join(default_data_directory, system_name))
+ # add game system by clicking import
+ print("Looking for system import")
+ import_system_buttons = self.wait.until(lambda drv:
+ drv.find_elements(By.XPATH, "//input[@type='file']"))
+ if len(import_system_buttons) > 0:
+ print("Found the system import button")
+ import_system_buttons[0].send_keys(self.game_directory)
+
+ # Load the 1st system.
+ import_buttons = self.wait.until(lambda drv:
+ drv.find_elements(By.CSS_SELECTOR,
+ "#mainContent > fieldset > div > div > div:nth-child(1)"))
+ if len(import_buttons) > 0:
+ print("Loading the first game system")
+ import_buttons[0].click()
+
+ def load_list(self, roster_name: str):
+ # add list by clicking import
+ test_list = os.path.join(self.game_directory, 'tests', roster_name)
+ shutil.copy(test_list + ".test", test_list + ".ros")
+
+ import_list_element = self.wait.until(lambda drv:
+ drv.find_elements(By.ID, "importBs")
+ )
+
+ if len(import_list_element) > 0:
+ print("Uploading list to the import list button")
+ import_list_element[0].send_keys(test_list + ".ros")
+
+ # Load the first list
+ self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, "listName"))).click()
+ print("Loading the first list")
+
+ # Wait until the list has loaded
+ print("Waiting for the list to load...")
+ self.wait.until(lambda drv:
+ drv.find_element(By.CLASS_NAME, 'titreRoster'))
+
+ def tearDown(self):
+ if self.debug:
+ # 60 seconds for me to mess around in
+ time.sleep(60)
+ self.driver.quit()
+
+ def get_error_list(self):
+ errors = self.driver.execute_script("return $debugOption.allErrors.map(error => ({"
+ "msg: error.msg,"
+ "constraint_id:error.constraint.id,"
+ "}))")
+ if self.debug:
+ print("$debugOption for list")
+ print(errors)
+ return errors
+
+ def get_squad_cost(self, primary_category, unit_name, force_index=0):
+ script_to_run = (f" $debugOption.state.getChilds()[{force_index}].getChilds()[0].getChilds()"
+ f".filter(entry => entry.name == '{primary_category}')[0].getChilds()"
+ f".filter(entry => entry.name == '{unit_name}')[0].totalCosts")
+ if self.debug:
+ print(script_to_run)
+ costs = self.driver.execute_script(f"return {script_to_run}")
+ if len(costs) == 1:
+ return list(costs.values())[0]
+ return costs
+
+ def test_verify_no_ros_files(self):
+ tests_dir = os.path.join(self.game_directory, 'tests')
+ for filename in os.listdir(tests_dir):
+ name, extension = os.path.splitext(filename)
+ if extension in ["ros", "rosz"]:
+ if not os.path.exists(
+ os.path.join(tests_dir, name, ".test")): # If this isn't a copy we made of a .test
+ self.fail(
+ "There is a .ros file in the tests directory, which will break appspot."
+ " Rename the file to .test")
+
+ def test_LA_5_errors(self):
+ self.load_list('Empty Validation Test')
+ errors = self.get_error_list()
+ self.assertEqual(5, len(errors), "There should be 5 errors in an empty space marine list")
+
+ def test_dt_does_not_affect_squad_cost(self):
+ self.load_list('Dedicated Transport Squad Costs')
+ squad_cost = self.get_squad_cost("Troops:", "Tactical Support Squad")
+ self.assertEqual(170, squad_cost, "TSS should not count the rhino as a model")
+
+ def test_NameOfTest(self):
+ self.load_list('Basic Marines Validate')
+ errors = self.get_error_list()
+ self.assertEqual(0, len(errors), "This list has validation errors")
+
+
+if __name__ == '__main__':
+ unittest.main()