#nullable enable
// Portage C# de solve_cpsat (wfc_cpsat.py) : meme modele CP-SAT que le jumeau Python.
// Reutilise Tileset / Tile / ts definis en section 1. Les contraintes globales qui
// manquent au WFC pur (section 3) sont exprimees directement sur les variables CP-SAT.
#r "nuget: Google.OrTools, 9.11.4210"
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Google.OrTools.Sat;
public sealed record CpsatResult(
string Status, double SolveSeconds, int[,] Grid, int[,] ObjGrid,
int FloorCells, int EnemyCount, int KeyCount, int ChestCount);
public static class WfcCpsat
{
public static CpsatResult Solve(Tileset ts, int rows, int cols, int seed,
double minFloorRatio = 0.30, double maxFloorRatio = 0.60,
double minEnemyRatio = 0.05, double maxEnemyRatio = 0.20,
int nKeys = 1, int nChests = 1, bool addConnectivity = true, int timeoutSeconds = 20)
{
int nTiles = ts.Count;
int N = rows * cols;
int floorId = ts.Tiles.FindIndex(t => t.Name == "floor");
if (floorId < 0) throw new InvalidOperationException("Tuile 'floor' absente du tileset.");
var model = new CpModel();
// -- Variables : 1 IntVar tuile + 1 IntVar objet par cellule (0 rien, 1 ennemi, 2 cle, 3 coffre) --
var cells = new IntVar[rows, cols];
var objs = new IntVar[rows, cols];
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
{
cells[r, c] = model.NewIntVar(0, nTiles - 1, $"c_{r}_{c}");
objs[r, c] = model.NewIntVar(0, 3, $"o_{r}_{c}");
}
// -- Contrainte 1 : adjacence (paires autorisees de la table Rules) --
// Le binding C# n'expose pas les tuples de AddAllowedAssignments (API Python) ;
// on encode la disjonction via AddBoolOr + implications binaires.
void AddAdjacency(IntVar x, IntVar y, string tag)
{
var validPairs = new List<ILiteral>();
for (int a = 0; a < nTiles; a++)
for (int b = 0; b < nTiles; b++)
if (ts.CanAdjoin(a, b))
{
var p = model.NewBoolVar($"{tag}_{a}_{b}");
model.Add(x == a).OnlyEnforceIf(p);
model.Add(y == b).OnlyEnforceIf(p);
validPairs.Add(p);
}
model.AddBoolOr(validPairs);
}
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
{
if (c + 1 < cols)
AddAdjacency(cells[r, c], cells[r, c + 1], $"h_{r}_{c}");
if (r + 1 < rows)
AddAdjacency(cells[r, c], cells[r + 1, c], $"v_{r}_{c}");
}
// -- is_floor : cell == floorId <-> BoolVar --
var isFloor = new BoolVar[rows, cols];
var floorList = new List<IntVar>();
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
{
isFloor[r, c] = model.NewBoolVar($"fl_{r}_{c}");
model.Add(cells[r, c] == floorId).OnlyEnforceIf(isFloor[r, c]);
model.Add(cells[r, c] != floorId).OnlyEnforceIf(isFloor[r, c].Not());
floorList.Add(isFloor[r, c]);
}
// -- Contrainte 2 : ratio de sol (30-60 %) --
int minFloor = (int)Math.Round(minFloorRatio * N);
int maxFloor = (int)Math.Round(maxFloorRatio * N);
model.Add(LinearExpr.Sum(floorList) >= minFloor);
model.Add(LinearExpr.Sum(floorList) <= maxFloor);
// -- Contrainte 3 : les objets ne peuvent etre que sur du sol --
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
{
var onFloor = model.NewBoolVar($"onz_{r}_{c}");
model.Add(objs[r, c] > 0).OnlyEnforceIf(onFloor);
model.Add(objs[r, c] == 0).OnlyEnforceIf(onFloor.Not());
model.AddImplication(onFloor, isFloor[r, c]);
}
// -- Contrainte 4 : densite d'ennemis (5-20 % du nombre de cases sol) --
var enemies = new List<IntVar>();
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
{
var en = model.NewBoolVar($"en_{r}_{c}");
model.Add(objs[r, c] == 1).OnlyEnforceIf(en);
model.Add(objs[r, c] != 1).OnlyEnforceIf(en.Not());
enemies.Add(en);
}
model.Add(LinearExpr.Sum(enemies) * 100 >= (long)Math.Round(minEnemyRatio * 100) * LinearExpr.Sum(floorList));
model.Add(LinearExpr.Sum(enemies) * 100 <= (long)Math.Round(maxEnemyRatio * 100) * LinearExpr.Sum(floorList));
// -- Contrainte 5 : comptes exacts de cles et coffres --
var keyVars = new List<IntVar>();
var chestVars = new List<IntVar>();
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
{
var k = model.NewBoolVar($"k_{r}_{c}");
var ch = model.NewBoolVar($"ch_{r}_{c}");
model.Add(objs[r, c] == 2).OnlyEnforceIf(k);
model.Add(objs[r, c] != 2).OnlyEnforceIf(k.Not());
model.Add(objs[r, c] == 3).OnlyEnforceIf(ch);
model.Add(objs[r, c] != 3).OnlyEnforceIf(ch.Not());
keyVars.Add(k);
chestVars.Add(ch);
}
model.Add(LinearExpr.Sum(keyVars) == nKeys);
model.Add(LinearExpr.Sum(chestVars) == nChests);
// -- Contrainte 6 : connectivite (relaxation flow, comme le jumeau Python) --
if (addConnectivity)
{
model.Add(cells[0, 0] == floorId);
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
{
if (r == 0 && c == 0) continue;
var incoming = new List<IntVar>();
foreach (var (dr, dc) in new[] { (-1, 0), (1, 0), (0, -1), (0, 1) })
{
int nr = r + dr, nc = c + dc;
if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;
var arc = model.NewBoolVar($"arc_{nr}_{nc}_{r}_{c}");
model.AddImplication(arc, isFloor[r, c]);
model.AddImplication(arc, isFloor[nr, nc]);
incoming.Add(arc);
}
model.Add(LinearExpr.Sum(incoming) >= 1).OnlyEnforceIf(isFloor[r, c]);
}
}
// -- Objectif : maximiser la somme de bruits pseudo-aleatoires (variete) --
var rng = new Random(seed + 1);
const int noiseBase = 200, noiseAmp = 150;
var scoreVars = new List<IntVar>();
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
{
var noise = new long[nTiles];
for (int t = 0; t < nTiles; t++)
noise[t] = Math.Max(1, noiseBase + rng.Next(-noiseAmp, noiseAmp + 1));
var w = model.NewIntVar(0, noiseBase + noiseAmp, $"w_{r}_{c}");
model.AddElement(cells[r, c], noise, w);
scoreVars.Add(w);
}
model.Maximize(LinearExpr.Sum(scoreVars));
// -- Resolution --
var solver = new CpSolver();
solver.StringParameters = $"max_time_in_seconds:{timeoutSeconds} random_seed:{seed} num_search_workers:4";
var sw = Stopwatch.StartNew();
var status = solver.Solve(model);
sw.Stop();
if (status != CpSolverStatus.Optimal && status != CpSolverStatus.Feasible)
throw new InvalidOperationException($"CP-SAT n'a pas trouve de solution (statut {status}).");
var grid = new int[rows, cols];
var objGrid = new int[rows, cols];
int nFloor = 0, nEnemy = 0, nKey = 0, nChest = 0;
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
{
grid[r, c] = (int)solver.Value(cells[r, c]);
objGrid[r, c] = (int)solver.Value(objs[r, c]);
if (solver.Value(isFloor[r, c]) == 1) nFloor++;
switch (objGrid[r, c])
{
case 1: nEnemy++; break;
case 2: nKey++; break;
case 3: nChest++; break;
}
}
return new CpsatResult(status.ToString(), sw.Elapsed.TotalSeconds, grid, objGrid,
nFloor, nEnemy, nKey, nChest);
}
}