Extend campaign_01 to 9 missions with a finale cathedral

Renames mission 7 from "Le Couronnement" to "Le Comptoir" (it only sets
up the tools→gold chain — the coronation is now the final mission) and
adds:

  - Mission 8 "L'Expansion Finale" (12×10): a Forge Est (wood→tools) and
    an Armurerie Est (stone→arms) on new rightmost columns, plus walls
    that force pieces to route around them. An Entrepôt Est demand on
    (11,9) gives the mission its own goal without depending on the old
    demands.
  - Mission 9 "Le Couronnement" (12×12): the Cathédrale occupies row 11
    as three adjacent demands — outils, armes, and or — so the player
    must keep all three transformation chains running simultaneously to
    complete the campaign.

Existing file tests updated for the new count and rename; new
Campaign01Tests asserts structure and non-regressive terrain across all
nine missions.
This commit is contained in:
Samuel Bouchet 2026-04-17 22:34:11 +02:00
parent 480c783bd6
commit e3eb10570b
4 changed files with 170 additions and 10 deletions

View file

@ -204,8 +204,8 @@
}, },
{ {
"id": 7, "id": 7,
"name": "Le Couronnement", "name": "Le Comptoir",
"description": "Le Comptoir transforme les outils en or. Livrez l'or au Trésor Royal pour couronner le Roi.", "description": "Le Comptoir transforme les outils en or. Livrez l'or au Trésor Royal — bientôt le Roi.",
"flavor": "« De l'or ! Le Roi sera content. Enfin… s'il reste du budget pour nous payer. » — Dame sceptique", "flavor": "« De l'or ! Le Roi sera content. Enfin… s'il reste du budget pour nous payer. » — Dame sceptique",
"terrainPatch": { "terrainPatch": {
"newWidth": 10, "newWidth": 10,
@ -222,6 +222,91 @@
{ "kind": "rook", "count": 4 }, { "kind": "rook", "count": 4 },
{ "kind": "bishop", "count": 2 } { "kind": "bishop", "count": 2 }
] ]
},
{
"id": 8,
"name": "L'Expansion Finale",
"description": "Deux nouveaux transformateurs s'ajoutent pour soutenir la production. Les routes existantes doivent tenir.",
"flavor": "« On double l'équipe, on double la paie ? Non. D'accord, double le boulot alors. » — Pion pragmatique",
"terrainPatch": {
"newWidth": 12,
"newHeight": 10,
"cells": [
{ "col": 10, "row": 0, "type": "empty" },
{ "col": 10, "row": 1, "type": "empty" },
{ "col": 10, "row": 2, "type": "empty" },
{ "col": 10, "row": 3, "type": "empty" },
{ "col": 10, "row": 4, "type": "empty" },
{ "col": 10, "row": 5, "type": "transformer", "transformer": { "name": "Forge Est", "inputCargo": "wood", "inputRequired": 2, "outputCargo": "tools", "outputAmount": 1 } },
{ "col": 10, "row": 6, "type": "wall" },
{ "col": 10, "row": 7, "type": "empty" },
{ "col": 10, "row": 8, "type": "empty" },
{ "col": 10, "row": 9, "type": "empty" },
{ "col": 11, "row": 0, "type": "empty" },
{ "col": 11, "row": 1, "type": "empty" },
{ "col": 11, "row": 2, "type": "empty" },
{ "col": 11, "row": 3, "type": "transformer", "transformer": { "name": "Armurerie Est", "inputCargo": "stone", "inputRequired": 2, "outputCargo": "arms", "outputAmount": 1 } },
{ "col": 11, "row": 4, "type": "empty" },
{ "col": 11, "row": 5, "type": "empty" },
{ "col": 11, "row": 6, "type": "empty" },
{ "col": 11, "row": 7, "type": "wall" },
{ "col": 11, "row": 8, "type": "empty" },
{ "col": 11, "row": 9, "type": "demand", "demand": { "name": "Entrepôt Est", "cargo": "tools", "amount": 2 } }
]
},
"unlockedPieces": [],
"unlockedLevels": [],
"stock": [
{ "kind": "rook", "count": 4 },
{ "kind": "bishop", "count": 2 },
{ "kind": "knight", "count": 2 },
{ "kind": "pawn", "count": 4 }
]
},
{
"id": 9,
"name": "Le Couronnement",
"description": "La Cathédrale est la dernière étape. Elle réclame outils, armes et or simultanément pour couronner le Roi.",
"flavor": "« Trois offrandes, un Roi. Et après, c'est lui qui nous couronne ? Ou qui nous exécute ? » — Cavalier inquiet",
"terrainPatch": {
"newWidth": 12,
"newHeight": 12,
"cells": [
{ "col": 0, "row": 10, "type": "empty" },
{ "col": 0, "row": 11, "type": "empty" },
{ "col": 1, "row": 10, "type": "empty" },
{ "col": 1, "row": 11, "type": "empty" },
{ "col": 2, "row": 10, "type": "empty" },
{ "col": 2, "row": 11, "type": "empty" },
{ "col": 3, "row": 10, "type": "empty" },
{ "col": 3, "row": 11, "type": "empty" },
{ "col": 4, "row": 10, "type": "empty" },
{ "col": 4, "row": 11, "type": "empty" },
{ "col": 5, "row": 10, "type": "empty" },
{ "col": 5, "row": 11, "type": "demand", "demand": { "name": "Cathédrale (Outils)", "cargo": "tools", "amount": 2 } },
{ "col": 6, "row": 10, "type": "empty" },
{ "col": 6, "row": 11, "type": "demand", "demand": { "name": "Cathédrale (Armes)", "cargo": "arms", "amount": 2 } },
{ "col": 7, "row": 10, "type": "empty" },
{ "col": 7, "row": 11, "type": "demand", "demand": { "name": "Cathédrale (Or)", "cargo": "gold", "amount": 2 } },
{ "col": 8, "row": 10, "type": "empty" },
{ "col": 8, "row": 11, "type": "empty" },
{ "col": 9, "row": 10, "type": "empty" },
{ "col": 9, "row": 11, "type": "empty" },
{ "col": 10, "row": 10, "type": "empty" },
{ "col": 10, "row": 11, "type": "empty" },
{ "col": 11, "row": 10, "type": "empty" },
{ "col": 11, "row": 11, "type": "empty" }
]
},
"unlockedPieces": [],
"unlockedLevels": [],
"stock": [
{ "kind": "queen", "count": 2 },
{ "kind": "rook", "count": 4 },
{ "kind": "bishop", "count": 2 },
{ "kind": "knight", "count": 2 },
{ "kind": "pawn", "count": 4 }
]
} }
] ]
} }

View file

@ -0,0 +1,81 @@
using System.IO;
using Chessistics.Engine.Loading;
using Chessistics.Engine.Model;
using Xunit;
namespace Chessistics.Tests.Loading;
public class Campaign01Tests
{
private static CampaignDef LoadRealCampaign()
{
var repoRoot = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..");
var path = Path.GetFullPath(Path.Combine(repoRoot, "Data", "campaigns", "campaign_01.json"));
return CampaignLoader.Load(File.ReadAllText(path));
}
[Fact]
public void Campaign_HasExpectedStructure()
{
var c = LoadRealCampaign();
Assert.Equal(9, c.Missions.Count);
Assert.Equal(4, c.InitialWidth);
Assert.Equal(4, c.InitialHeight);
}
[Fact]
public void Mission8_AddsTwoTransformersAndExpandsTo12x10()
{
var c = LoadRealCampaign();
var m8 = c.Missions[7];
Assert.Equal("L'Expansion Finale", m8.Name);
Assert.Equal(12, m8.TerrainPatch.NewWidth);
Assert.Equal(10, m8.TerrainPatch.NewHeight);
var transformers = m8.TerrainPatch.Cells.Where(p => p.Type == CellType.Transformer).ToList();
Assert.Equal(2, transformers.Count);
Assert.Contains(transformers, t => t.Transformer!.Name == "Forge Est");
Assert.Contains(transformers, t => t.Transformer!.Name == "Armurerie Est");
}
[Fact]
public void Mission9_CathedralDemandsAllThreeCargoTypes()
{
var c = LoadRealCampaign();
var m9 = c.Missions[8];
Assert.Equal("Le Couronnement", m9.Name);
Assert.Equal(12, m9.TerrainPatch.NewWidth);
Assert.Equal(12, m9.TerrainPatch.NewHeight);
var demands = m9.TerrainPatch.Cells.Where(p => p.Type == CellType.Demand).ToList();
Assert.Equal(3, demands.Count);
var types = demands.Select(d => d.Demand!.Cargo).ToHashSet();
Assert.Contains(CargoType.Tools, types);
Assert.Contains(CargoType.Arms, types);
Assert.Contains(CargoType.Gold, types);
}
[Fact]
public void Mission7_RenamedToComptoir()
{
var c = LoadRealCampaign();
var m7 = c.Missions[6];
Assert.Equal("Le Comptoir", m7.Name);
}
[Fact]
public void AllTerrainPatchesAreNonRegressive()
{
// Subsequent missions must only grow the board, never shrink it.
var c = LoadRealCampaign();
int w = c.InitialWidth, h = c.InitialHeight;
for (int i = 0; i < c.Missions.Count; i++)
{
var tp = c.Missions[i].TerrainPatch;
Assert.True(tp.NewWidth >= w, $"Mission {i}: width shrunk from {w} to {tp.NewWidth}");
Assert.True(tp.NewHeight >= h, $"Mission {i}: height shrunk from {h} to {tp.NewHeight}");
w = tp.NewWidth;
h = tp.NewHeight;
}
}
}

View file

@ -12,7 +12,7 @@ public class CampaignFileTests
var campaign = CampaignLoader.LoadFromFile("../../../../Data/campaigns/campaign_01.json"); var campaign = CampaignLoader.LoadFromFile("../../../../Data/campaigns/campaign_01.json");
Assert.Equal("La Quête du Roi", campaign.Name); Assert.Equal("La Quête du Roi", campaign.Name);
Assert.Equal(7, campaign.Missions.Count); Assert.Equal(9, campaign.Missions.Count);
} }
[Fact] [Fact]
@ -52,7 +52,7 @@ public class CampaignFileTests
var campaign = CampaignLoader.LoadFromFile("../../../../Data/campaigns/campaign_01.json"); var campaign = CampaignLoader.LoadFromFile("../../../../Data/campaigns/campaign_01.json");
var m7 = campaign.Missions[6]; var m7 = campaign.Missions[6];
Assert.Equal("Le Couronnement", m7.Name); Assert.Equal("Le Comptoir", m7.Name);
var transformerCell = m7.TerrainPatch.Cells var transformerCell = m7.TerrainPatch.Cells
.FirstOrDefault(c => c.Type == CellType.Transformer); .FirstOrDefault(c => c.Type == CellType.Transformer);

View file

@ -12,12 +12,6 @@ et l'extension de la campagne.
Le moteur expose deja les commandes et events requis ; cote Godot il manque Le moteur expose deja les commandes et events requis ; cote Godot il manque
les surfaces d'interaction et d'animation. les surfaces d'interaction et d'animation.
### 1.6 Visualisation des trajets
`TrajectView` existe. Manque :
- Fleches directionnelles sur le trait.
- Pulsation legere du trait quand la piece est en mouvement.
- Couleur unique par piece pour distinguer les chaines.
--- ---
## 2. Extension de la campagne ## 2. Extension de la campagne