namespace Akwaier.Engine;
///
/// De volledige spelstand van een partij Akwaier, met alle regels uit
/// AQUIRE.BBC (BBC Micro, "aquire 23/05/87"). Er zit geen schermcode in.
///
/// De regels zijn één op één overgenomen; de aantoonbare programmeerfouten uit
/// het origineel zijn hersteld en staan gemarkeerd met "FIX:".
///
public sealed class Spel
{
private readonly Random _toeval;
private readonly int[,] _bord = new int[Regels.Grootte + 2, Regels.Grootte + 2];
private readonly int[] _groottes = new int[Regels.AantalKetens + 1];
private readonly int[,] _handen; // [speler 1..6, plaats 0..7], 0 = geen steen
private int _rondePassen; // kanniet%
public Spel(int mensPlaats = 0, string mensNaam = "", int? zaad = null)
{
_toeval = zaad.HasValue ? new Random(zaad.Value) : new Random();
for (int x = 1; x <= Regels.Grootte; x++)
for (int y = 1; y <= Regels.Grootte; y++)
_bord[x, y] = Regels.Leeg;
Spelers = new Speler[Regels.AantalSpelers + 1];
for (int i = 1; i <= Regels.AantalSpelers; i++)
Spelers[i] = new Speler(i, Taal.Spelernamen[i - 1]);
MensPlaats = mensPlaats;
if (mensPlaats > 0)
{
var naam = (mensNaam ?? "").Trim().ToUpperInvariant();
if (naam.Length > 8) naam = naam[..8];
Spelers[mensPlaats].Mens = true;
Spelers[mensPlaats].Naam = naam.Length > 0 ? naam : Taal.NaamloosMens;
}
for (int n = 1; n <= Regels.Grootte * Regels.Grootte; n++) Zak.Add(n);
for (int i = Zak.Count - 1; i > 0; i--)
{
int j = _toeval.Next(i + 1);
(Zak[i], Zak[j]) = (Zak[j], Zak[i]);
}
// PROCsteen_pakken stopte zodra er nog één steen over was; die bleef
// ongebruikt liggen. FIX: de zak wordt nu netjes leeggespeeld.
_handen = new int[Regels.AantalSpelers + 1, Regels.HandGrootte];
for (int p = 1; p <= Regels.AantalSpelers; p++)
for (int s = 0; s < Regels.HandGrootte; s++)
_handen[p, s] = Pak();
}
public Speler[] Spelers { get; }
public List Zak { get; } = new();
/// Op welke plaats de mens speelt; 0 betekent kijken.
public int MensPlaats { get; }
/// W%: het aantal gespeelde beurten.
public int Beurt { get; private set; }
public int AanDeBeurt { get; private set; }
public bool Klaar { get; private set; }
public string Eindreden { get; private set; } = "";
// ------------------------------------------------------------ bord en zak
private int Pak()
{
if (Zak.Count == 0) return 0;
int t = Zak[^1];
Zak.RemoveAt(Zak.Count - 1);
return t;
}
private void ZetVak(int x, int y, int waarde)
{
int oud = _bord[x, y];
if (oud == waarde) return;
if (oud >= 1) _groottes[oud]--;
_bord[x, y] = waarde;
if (waarde >= 1) _groottes[waarde]++;
}
/// De inhoud van een vakje; buiten het bord telt als leeg.
public int Vak(int x, int y) =>
x >= 1 && x <= Regels.Grootte && y >= 1 && y <= Regels.Grootte ? _bord[x, y] : Regels.Leeg;
/// FNmax(): het aantal tegels van een keten.
public int KetenGrootte(int keten) =>
keten >= 1 && keten <= Regels.AantalKetens ? _groottes[keten] : 0;
/// De steen op een handplaats; 0 als de plaats leeg is.
public int Hand(int speler, int plaats) => _handen[speler, plaats];
/// sticht%: de ketens die nog gesticht kunnen worden.
public List TeStichten()
{
var uit = new List();
for (int c = 1; c <= Regels.AantalKetens; c++)
if (_groottes[c] == 0) uit.Add(c);
return uit;
}
public List BestaandeKetens()
{
var uit = new List();
for (int c = 1; c <= Regels.AantalKetens; c++)
if (_groottes[c] > 0) uit.Add(c);
return uit;
}
// ------------------------------------------------------------- geldzaken
/// PROCbereken_prijs_keten (regel 1900).
public int Prijs(int keten)
{
int n = KetenGrootte(keten);
if (n == 0) return 0;
int p = Regels.Basisprijs[keten];
foreach (int drempel in Regels.Prijsdrempels)
if (n > drempel) p += 100;
int uitstaand = 0;
for (int q = 1; q <= Regels.AantalSpelers; q++) uitstaand += Spelers[q].Aandelen[keten];
return p + uitstaand * 10;
}
/// PROCvermogen: banksaldo plus de waarde van alle aandelen.
public int Vermogen(int speler)
{
int t = Spelers[speler].Geld;
for (int c = 1; c <= Regels.AantalKetens; c++) t += Prijs(c) * Spelers[speler].Aandelen[c];
return t;
}
///
/// PROCsort_aandelen: de spelers gesorteerd op aandeelbezit in een keten,
/// aflopend; bij gelijk bezit wint het hoogste spelernummer, net als de
/// sleutel 10*bezit+nummer uit het origineel.
///
public int[] Rangorde(int keten)
{
var uit = new[] { 1, 2, 3, 4, 5, 6 };
Array.Sort(uit, (p, q) =>
{
int vp = Spelers[p].Aandelen[keten] * 10 + p;
int vq = Spelers[q].Aandelen[keten] * 10 + q;
return vq.CompareTo(vp);
});
return uit;
}
// -------------------------------------------------------- zetten bekijken
///
/// PROCcontroleer_aanliggen (regel 1200), maar zonder het bord aan te raken.
///
/// FIX: het origineel las de buur uit vóór de randcontrole en las daarbij
/// geheugen buiten de bordarray.
/// FIX: kiezen% werd nooit teruggezet en bleef tussen beurten staan.
/// FIX: twee buren van dezelfde keten telden dubbel mee.
///
public Zetkans Bekijk(int x, int y)
{
var aan = new List();
var los = new List<(int, int)>();
foreach (var (dx, dy) in Regels.Buren)
{
int nx = x + dx, ny = y + dy;
if (nx < 1 || nx > Regels.Grootte || ny < 1 || ny > Regels.Grootte) continue;
int v = _bord[nx, ny];
if (v >= 1 && v <= Regels.AantalKetens) { if (!aan.Contains(v)) aan.Add(v); }
else if (v == Regels.Los) los.Add((nx, ny));
}
int besteGrootte = 0, winnaar = 0;
var gelijken = new List();
foreach (int c in aan)
{
int n = _groottes[c];
if (n > besteGrootte) { besteGrootte = n; winnaar = c; gelijken = new List { c }; }
else if (n == besteGrootte) gelijken.Add(c);
}
// kanniet%: naast een losse steen leggen mag alleen als je ook een
// bestaande keten raakt, anders zou er ongemerkt een keten ontstaan.
return new Zetkans
{
X = x,
Y = y,
Mag = aan.Count > 0 || los.Count == 0,
Aanliggend = aan,
Losse = los,
Winnaar = winnaar,
Gelijkspel = gelijken.Count > 1,
Gelijken = gelijken,
};
}
/// PROCcontroleer_vrij (regel 2500): stichten mag op een vrij liggend vakje.
public bool KanStichten(int x, int y)
{
if (_bord[x, y] != Regels.Leeg) return false;
foreach (var (dx, dy) in Regels.Buren)
if (Vak(x + dx, y + dy) != Regels.Leeg) return false;
return true;
}
/// De handplaatsen waarmee deze speler een geldige zet kan doen.
public List GeldigePlaatsen(int speler)
{
var uit = new List();
for (int s = 0; s < Regels.HandGrootte; s++)
{
int t = _handen[speler, s];
if (t == 0) continue;
var (x, y) = Regels.TegelPositie(t);
if (Bekijk(x, y).Mag) uit.Add(s);
}
return uit;
}
public List StichtbarePlaatsen(int speler)
{
var uit = new List();
if (TeStichten().Count == 0) return uit;
for (int s = 0; s < Regels.HandGrootte; s++)
{
int t = _handen[speler, s];
if (t == 0) continue;
var (x, y) = Regels.TegelPositie(t);
if (KanStichten(x, y)) uit.Add(s);
}
return uit;
}
// ------------------------------------------------------------ zetten doen
private void Bijvullen(int speler, int plaats) => _handen[speler, plaats] = Pak();
///
/// PROCstichten: een losse steen wordt meteen een keten van één tegel, en de
/// stichter krijgt één gratis aandeel.
///
public Zetverslag Sticht(int speler, int plaats, int keten)
{
int t = _handen[speler, plaats];
if (t == 0) throw new OngeldigeZet("geen steen op die handplaats");
var (x, y) = Regels.TegelPositie(t);
if (!KanStichten(x, y)) throw new OngeldigeZet("dit vakje ligt niet vrij");
if (KetenGrootte(keten) != 0) throw new OngeldigeZet("die keten bestaat al");
ZetVak(x, y, keten);
Spelers[speler].Aandelen[keten]++; // oprichtersaandeel
Bijvullen(speler, plaats);
return new Zetverslag { X = x, Y = y, Keten = keten };
}
///
/// Een steen neerleggen; regels 3174-3210 (mens) en 3680-3710 (computer).
/// geldt alleen bij twee even grote ketens.
///
public Zetverslag Leg(int speler, int plaats, int gekozenWinnaar = 0)
{
int t = _handen[speler, plaats];
if (t == 0) throw new OngeldigeZet("geen steen op die handplaats");
var (x, y) = Regels.TegelPositie(t);
var kans = Bekijk(x, y);
if (!kans.Mag) throw new OngeldigeZet("daar mag je niet leggen");
var verslag = new Zetverslag { X = x, Y = y };
if (kans.Aanliggend.Count == 0)
{
ZetVak(x, y, Regels.Los); // losse steen, nog geen keten
}
else
{
// FIX: in het origineel overschreef PROCcontroleer_aanliggen de door
// de speler gekozen keten weer met de eigen berekening (regel 3202).
int w = kans.Gelijken.Contains(gekozenWinnaar) ? gekozenWinnaar : kans.Winnaar;
var verliezers = kans.Aanliggend.Where(c => c != w).ToList();
// PROCverdeel_geld draait vóór het omkleuren, zodat de bonus met de
// ketengroottes van vóór de fusie gerekend wordt.
foreach (int lo in verliezers) verslag.Uitkeringen.Add(Ontbind(lo, w));
ZetVak(x, y, w);
foreach (var (lx, ly) in kans.Losse) ZetVak(lx, ly, w);
if (verliezers.Count > 0)
for (int xx = 1; xx <= Regels.Grootte; xx++)
for (int yy = 1; yy <= Regels.Grootte; yy++)
if (verliezers.Contains(_bord[xx, yy])) ZetVak(xx, yy, w);
verslag.Keten = w;
verslag.Opgeslokt.AddRange(verliezers);
}
Bijvullen(speler, plaats);
return verslag;
}
///
/// PROCverdeel (regel 2800): de bonus uitkeren en de aandelen voor de helft
/// omruilen in de winnende keten.
///
/// FIX: de rangschikking kwam uit een tabel die pas na de koopfase van de
/// vorige speler was bijgewerkt; hier wordt hij vers berekend.
///
private Uitkering Ontbind(int verliezer, int winnaar)
{
int grootte = _groottes[verliezer];
int prijs = Prijs(verliezer);
int[] rang = Rangorde(verliezer);
var bezit = rang.Select(q => Spelers[q].Aandelen[verliezer]).ToArray();
int pot = (grootte + bezit.Sum()) * prijs;
var uit = new Uitkering
{
Keten = verliezer, Naar = winnaar, Grootte = grootte, Prijs = prijs, Pot = pot,
};
double[] deel = Bonus.Verdeling(bezit);
for (int i = 0; i < Regels.AantalSpelers; i++)
{
int bedrag = (int)Math.Round(deel[i] * pot, MidpointRounding.AwayFromZero);
if (bedrag == 0) continue;
Spelers[rang[i]].Geld += bedrag;
uit.Bedragen[rang[i]] = bedrag;
}
for (int q = 1; q <= Regels.AantalSpelers; q++)
{
Spelers[q].Aandelen[winnaar] += Spelers[q].Aandelen[verliezer] / 2;
Spelers[q].Aandelen[verliezer] = 0;
}
return uit;
}
///
/// PROCkoopt (regel 9390): hoogstens drie aandelen van een bestaande keten.
///
/// FIX: de lus in het origineel kon eindeloos doordraaien bij een saldo van
/// precies 0, en gebruikte > waar >= bedoeld was.
///
public int Koop(int speler, int keten, int aantal)
{
if (keten < 1 || keten > Regels.AantalKetens || aantal <= 0) return 0;
if (KetenGrootte(keten) == 0) return 0;
var p = Spelers[speler];
int prijs = Prijs(keten);
aantal = Math.Min(aantal, 3);
while (aantal > 0 && p.Geld < prijs * aantal) aantal--;
if (aantal <= 0) return 0;
p.Aandelen[keten] += aantal;
p.Geld -= prijs * aantal;
return aantal;
}
/// De speler kan niets kwijt en slaat de beurt over.
public void Past() => _rondePassen++;
// ------------------------------------------------------------------- de AI
///
/// PROCbekijk_fusie (regel 7000): hoe aantrekkelijk is deze fusie voor mij?
///
/// FIX: het origineel vergeleek een ketennummer met een ketengrootte
/// (kleinere(N%) <> MAX%) en gaf ook punten aan spelers zonder aandelen.
///
private int Fusiewaarde(Zetkans kans, int speler)
{
int beste = 0;
foreach (int c in kans.Aanliggend)
{
int[] rang = Rangorde(c);
int eigen = Spelers[speler].Aandelen[c];
int w;
if (eigen > 0 && (rang[0] == speler || rang[1] == speler))
w = c == kans.Winnaar ? 5 : 10; // opgeslokte keten levert bonus op
else if (eigen > 0)
w = 2; // klein belang, weinig winst
else
w = 4;
if (w > beste) beste = w;
}
return beste;
}
/// PROCbepaal_gunstigste_legsteen (regel 5200). -1 als er niets kan.
public int KiesPlaats(int speler)
{
var punten = new List<(int Waarde, int Plaats)>();
for (int s = 0; s < Regels.HandGrootte; s++)
{
int t = _handen[speler, s];
if (t == 0) { punten.Add((0, s)); continue; }
var (x, y) = Regels.TegelPositie(t);
var kans = Bekijk(x, y);
int w = !kans.Mag ? 0 : kans.Aanliggend.Count == 0 ? 2 : Fusiewaarde(kans, speler);
punten.Add((w, s));
}
punten.Sort((a, b) => a.Waarde != b.Waarde ? b.Waarde.CompareTo(a.Waarde)
: a.Plaats.CompareTo(b.Plaats));
if (punten[0].Waarde == 0) return -1;
// FIX: regel 5332 wilde voorkomen dat een al erg grote keten nog groter
// gemaakt wordt, maar gebruikte het handnummer als ketennummer.
for (int i = 0; i < punten.Count; i++)
{
var (waarde, plaats) = punten[i];
if (waarde == 0) break;
var (x, y) = Regels.TegelPositie(_handen[speler, plaats]);
var kans = Bekijk(x, y);
bool restWaarde = punten.Skip(i + 1).Any(q => q.Waarde > 0);
if (kans.Winnaar > 0 && _groottes[kans.Winnaar] > 15 && waarde < 9 && restWaarde)
continue;
return plaats;
}
return punten[0].Plaats;
}
///
/// PROCkopen (regel 9180): koop daar waar je positie bedreigd wordt.
///
/// FIX: de rangordebewerking zat in het origineel in een decimaal ingepakt
/// getal, en regel 9275 had een operatorvolgorde-fout waardoor ook
/// niet-bestaande ketens gekocht konden worden.
///
public int KiesAankoop(int speler)
{
var bestaand = BestaandeKetens();
if (bestaand.Count == 0) return 0;
var kandidaten = new List();
foreach (int c in bestaand)
{
int[] rang = Rangorde(c);
var bezit = rang.Select(q => Spelers[q].Aandelen[c]).ToArray();
for (int n = 0; n < 3; n++)
{
if (rang[n] != speler) continue;
if (bezit[n + 1] + 4 > bezit[n]) kandidaten.Add(c); // iemand zit vlak achter me
else if (n + 2 < Regels.AantalSpelers && bezit[n + 1] == bezit[n + 2]) kandidaten.Add(c);
break;
}
if (kandidaten.Count >= 4) break;
}
foreach (int c in kandidaten)
{
int[] rang = Rangorde(c);
var bezit = rang.Select(q => Spelers[q].Aandelen[c]).ToArray();
if (rang[0] == speler && bezit[0] - bezit[1] > 4) continue; // onbedreigd eerste
return c;
}
return bestaand[_toeval.Next(bestaand.Count)];
}
/// PROCcomp (regel 3582): de volledige beurt van een computerspeler.
public List ComputerBeurt(int speler)
{
var p = Spelers[speler];
var regels = new List();
bool gesticht = false;
var vrij = StichtbarePlaatsen(speler);
if (vrij.Count > 0)
{
var keuze = TeStichten();
int keten = keuze[_toeval.Next(keuze.Count)];
var r = Sticht(speler, vrij[0], keten);
regels.Add(Taal.StichtKeten(p.Naam, Regels.KetenLetter(keten),
Regels.Naam(r.X, r.Y)));
gesticht = true;
}
if (!gesticht)
{
int plaats = KiesPlaats(speler);
if (plaats < 0)
{
Past();
regels.Add(Taal.KanNiet(p.Naam));
return regels;
}
regels.AddRange(Beschrijf(p.Naam, Leg(speler, plaats)));
}
int koop = KiesAankoop(speler);
int prijs = Prijs(koop);
int n = Koop(speler, koop, 3);
regels.Add(n > 0
? Taal.Koopt(p.Naam, n, Regels.KetenLetter(koop), prijs)
: Taal.KooptNiets(p.Naam));
return regels;
}
/// Een zetverslag omzetten naar leesbare regels voor het meldingenvak.
public List Beschrijf(string naam, Zetverslag r)
{
string plek = Regels.Naam(r.X, r.Y);
var uit = new List
{
r.Keten == 0
? Taal.LegtLos(naam, plek)
: Taal.LegtBij(naam, plek, Regels.KetenLetter(r.Keten)),
};
foreach (var u in r.Uitkeringen)
{
uit.Add(Taal.GaatOpIn(Regels.KetenLetter(u.Keten), u.Grootte,
Regels.KetenLetter(u.Naar), u.Pot));
foreach (var b in u.Bedragen.OrderBy(b => b.Key))
uit.Add(Taal.Uitkering(Spelers[b.Key].Naam, b.Value));
}
return uit;
}
// -------------------------------------------------------------- beurtloop
///
/// PROCspelen (regel 4000). Levert de speler die aan de beurt is, of 0 als
/// het spel afgelopen is.
///
/// FIX: de test "iedereen kon niet" stond in het origineel boven in de lus
/// en werd elke ronde teruggezet voordat hij ooit 6 kon bereiken.
///
public int VolgendeSpeler()
{
if (Klaar) return 0;
if (AanDeBeurt == 0) { AanDeBeurt = 1; _rondePassen = 0; }
else if (AanDeBeurt >= Regels.AantalSpelers)
{
if (_rondePassen >= Regels.AantalSpelers)
return Beeindig(Taal.NiemandKanNog);
AanDeBeurt = 1; _rondePassen = 0;
}
else AanDeBeurt++;
Beurt++;
if (Beurt >= Regels.MaxBeurten) return Beeindig(Taal.BeurtenGespeeld(Regels.MaxBeurten));
for (int c = 1; c <= Regels.AantalKetens; c++)
if (_groottes[c] > Regels.EindKetenGrootte)
return Beeindig(Taal.KetenTeGroot(Regels.KetenLetter(c),
Regels.EindKetenGrootte));
return AanDeBeurt;
}
private int Beeindig(string reden)
{
Klaar = true;
Eindreden = reden;
return 0;
}
/// De eindstand, van hoog naar laag vermogen.
public List<(int Nummer, string Naam, int Vermogen)> Eindstand() =>
Enumerable.Range(1, Regels.AantalSpelers)
.Select(i => (i, Spelers[i].Naam, Vermogen(i)))
.OrderByDescending(r => r.Item3)
.ToList();
}