inial impl

This commit is contained in:
Michal Vrba
2026-08-28 13:59:36 +02:00
parent efec4e62c5
commit 6abf0f91a0
180 changed files with 64651 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
namespace Faraon.Core.Engine;
using Faraon.Core.Models;
public static class ActionValidator
{
public static bool CanPlayCard(
Card card,
Card? topCard,
Suit? activeSuit,
int pendingDrawCount,
bool pendingSkip,
RuleOptions rules)
{
// 1. Ak je na stole trest zo sedmy (napr. +2, +4, +6)
if (pendingDrawCount > 0)
{
// Ak je povolené prebíjanie sediem, musí zahrať sedmu
return rules.StackSevens && card.IsSeven;
}
// 2. Ak je na stole eso (státie)
if (pendingSkip)
{
// Ak je povolené prebíjanie esa esom, musí zahrať eso
return rules.CounterAceWithAce && card.IsAce;
}
// 3. Ak je kôpka prázdna (začiatok hry)
if (topCard == null)
{
return true;
}
// 4. Horník (Menič) sa môže zahrať na čokoľvek (keď nie je aktívny trest/eso)
if (card.IsHornik)
{
return true;
}
// 5. Ak bola zmenená farba Horníkom
if (activeSuit.HasValue)
{
// Musí sedieť zvolená farba (alebo zahrať ďalšieho Horníka, čo rieši bod 4)
return card.Suit == activeSuit.Value;
}
// 6. Bežné pravidlo: rovnaká farba ALEBO rovnaká hodnota
return card.Suit == topCard.Suit || card.Rank == topCard.Rank;
}
public static bool CanPlayCards(
List<Card> cards,
Card? topCard,
Suit? activeSuit,
int pendingDrawCount,
bool pendingSkip,
RuleOptions rules)
{
if (cards == null || cards.Count == 0) return false;
// Všetky karty musia mať rovnakú hodnotu
var firstRank = cards[0].Rank;
if (!cards.All(c => c.Rank == firstRank)) return false;
// 1. Ak je na stole trest zo sedmy
if (pendingDrawCount > 0)
{
return rules.StackSevens && cards.All(c => c.IsSeven);
}
// 2. Ak je na stole eso
if (pendingSkip)
{
return rules.CounterAceWithAce && cards.All(c => c.IsAce);
}
// 3. Ak je kôpka prázdna
if (topCard == null)
{
return true;
}
// 4. Horníci sa dajú vyložiť kedykoľvek
if (firstRank == Rank.OverKnave)
{
return true;
}
// 5. Ak bola zmenená farba Horníkom
if (activeSuit.HasValue)
{
// Aspoň jedna karta musí mať požadovanú farbu (prvá vyložená)
return cards.Any(c => c.Suit == activeSuit.Value);
}
// 6. Bežné pravidlo: aspoň jedna karta musí sedieť farbou na topCard, ALEBO hodnota kariet sedí na topCard
return firstRank == topCard.Rank || cards.Any(c => c.Suit == topCard.Suit);
}
public static bool CanBurnCards(
List<Card> cards,
int pendingDrawCount,
bool pendingSkip,
RuleOptions rules)
{
if (!rules.FourOfAKindBurns) return false;
if (pendingDrawCount > 0 || pendingSkip) return false;
if (cards.Count != 4) return false;
// Všetky 4 karty musia mať rovnakú hodnotu
var firstRank = cards[0].Rank;
return cards.All(c => c.Rank == firstRank);
}
}
+601
View File
@@ -0,0 +1,601 @@
namespace Faraon.Core.Engine;
using Faraon.Core.Models;
public class GameEngine
{
private readonly Deck _deckManager;
public GameState State { get; }
public event Action<GameEvent>? OnGameEvent;
public GameEngine(int? seed = null)
{
_deckManager = new Deck(seed);
State = new GameState();
}
public void StartGame(List<Player> players, RuleOptions rules)
{
if (players.Count < 2 || players.Count > 4)
{
throw new ArgumentException("Hra Faraón vyžaduje 2 až 4 hráčov.", nameof(players));
}
State.Players = players;
State.Rules = rules;
State.IsGameOver = false;
State.LastFinishedPlayer = null;
State.TurnCount = 1;
State.PendingDrawCount = 0;
State.PendingSkip = false;
State.ActiveSuit = null;
State.PendingHornikPlayer = null;
State.TurnState = TurnState.Playing;
foreach (var player in State.Players)
{
player.Reset();
}
// Vytvorenie a zamiešanie balíčka
State.DrawDeck = _deckManager.CreateShuffledDeck();
State.DiscardPile.Clear();
// Rozdanie kariet každému hráčovi
for (int i = 0; i < State.Rules.InitialCardCount; i++)
{
foreach (var player in State.Players)
{
if (State.DrawDeck.Count > 0)
{
var card = State.DrawDeck[0];
State.DrawDeck.RemoveAt(0);
player.Hand.Add(card);
}
}
}
// Otočenie prvej karty na odhadzovaciu kôpku
// Ak je prvá karta sedma, eso alebo horník, v základnej hre ju berieme ako štartovaciu bez okamžitého trestu
var startCard = State.DrawDeck[0];
State.DrawDeck.RemoveAt(0);
State.DiscardPile.Add(startCard);
State.CurrentPlayerIndex = 0;
EmitEvent(new GameEvent(
GameEventType.GameStarted,
State.CurrentPlayer,
Card: startCard,
Message: "Hra sa začala!"
));
}
public bool CanPlayCard(Player player, Card card)
{
if (State.IsGameOver || State.TurnState != TurnState.Playing) return false;
if (player != State.CurrentPlayer) return false;
if (!player.Hand.Contains(card)) return false;
return ActionValidator.CanPlayCard(
card,
State.TopCard,
State.ActiveSuit,
State.PendingDrawCount,
State.PendingSkip,
State.Rules
);
}
public bool CanBurnCards(Player player, List<Card> cards)
{
if (State.IsGameOver || State.TurnState != TurnState.Playing) return false;
if (player != State.CurrentPlayer) return false;
if (cards.Any(c => !player.Hand.Contains(c))) return false;
return ActionValidator.CanBurnCards(
cards,
State.PendingDrawCount,
State.PendingSkip,
State.Rules
);
}
public bool CanPlayCards(Player player, List<Card> cards)
{
if (State.IsGameOver || State.TurnState != TurnState.Playing) return false;
if (player != State.CurrentPlayer) return false;
if (cards == null || cards.Count == 0) return false;
if (cards.Any(c => !player.Hand.Contains(c))) return false;
return ActionValidator.CanPlayCards(
cards,
State.TopCard,
State.ActiveSuit,
State.PendingDrawCount,
State.PendingSkip,
State.Rules
);
}
public bool PlayCards(Player player, List<Card> cards, Suit? chosenSuit = null)
{
if (!CanPlayCards(player, cards)) return false;
if (cards.Count == 1)
{
return PlayCard(player, cards[0], chosenSuit);
}
// 4 rovnaké karty pri pravidle spálenia
if (cards.Count == 4 && State.Rules.FourOfAKindBurns && State.PendingDrawCount == 0 && !State.PendingSkip)
{
return BurnCards(player, cards);
}
// Zoradíme karty tak, aby prvá vyložená karta legálne sadla na stôl a posledná ostala na vrchu
List<Card> orderedCards;
if (ActionValidator.CanPlayCard(cards[0], State.TopCard, State.ActiveSuit, State.PendingDrawCount, State.PendingSkip, State.Rules))
{
// Prvá karta zvolená hráčom priamo sedí na stôl -> zachováme presné poradie hráča
orderedCards = new List<Card>(cards);
}
else
{
// Nájdeme kartu, ktorá legálne viaže combo na stôl, a dáme ju na začiatok
var matching = cards.FirstOrDefault(c => ActionValidator.CanPlayCard(
c, State.TopCard, State.ActiveSuit, State.PendingDrawCount, State.PendingSkip, State.Rules)) ?? cards[0];
orderedCards = new List<Card> { matching };
foreach (var c in cards)
{
if (c != matching)
{
orderedCards.Add(c);
}
}
}
foreach (var c in orderedCards)
{
player.Hand.Remove(c);
State.DiscardPile.Add(c);
}
var firstRank = cards[0].Rank;
State.ActiveSuit = null;
// Efekt Horníkov
if (firstRank == Rank.OverKnave)
{
if (chosenSuit.HasValue)
{
State.ActiveSuit = chosenSuit.Value;
EmitEvent(new GameEvent(
GameEventType.SuitChanged,
player,
Cards: cards,
ChosenSuit: chosenSuit.Value,
Message: $"{player.Name} zahral {cards.Count}x Horníka a zmenil farbu!"
));
}
else
{
State.TurnState = TurnState.SelectingSuit;
State.PendingHornikPlayer = player;
EmitEvent(new GameEvent(
GameEventType.CardPlayed,
player,
Cards: cards,
Message: $"{player.Name} zahral {cards.Count}x Horníka a vyberá farbu..."
));
return true;
}
}
// Efekt Sediem
else if (firstRank == Rank.Seven)
{
int addedPenalty = cards.Count * 2;
State.PendingDrawCount += addedPenalty;
var redSeven = cards.FirstOrDefault(c => c.IsRedSeven);
if (redSeven != null && State.Rules.RedSevenRevives && State.LastFinishedPlayer != null && State.LastFinishedPlayer != player)
{
var revived = State.LastFinishedPlayer;
revived.FinishedPosition = null;
State.LastFinishedPlayer = null;
DrawCardsForPlayer(revived, 3);
EmitEvent(new GameEvent(
GameEventType.PlayerRevived,
player,
TargetPlayer: revived,
Cards: cards,
ExtraData: 3,
Message: $"🔥 {player.Name} zahral combo {cards.Count}x Sedma a vrátil hráča {revived.Name} späť do hry!"
));
}
else
{
EmitEvent(new GameEvent(
GameEventType.SevenStacked,
player,
Cards: cards,
ExtraData: State.PendingDrawCount,
Message: $"⚡ COMBO {cards.Count}x Sedma! Trest stúpol na {State.PendingDrawCount} kariet!"
));
}
}
// Efekt Es
else if (firstRank == Rank.Ace)
{
bool wasPending = State.PendingSkip;
State.PendingSkip = true;
if (wasPending)
{
EmitEvent(new GameEvent(
GameEventType.AceCountered,
player,
Cards: cards,
Message: $"❄️ {player.Name} prebil Eso {cards.Count}x Esom!"
));
}
else
{
EmitEvent(new GameEvent(
GameEventType.AceSkipped,
player,
Cards: cards,
Message: $"❄️ COMBO {cards.Count}x Eso! Ďalší hráč stojí."
));
}
}
// Ostatné karty
else
{
EmitEvent(new GameEvent(
GameEventType.CardPlayed,
player,
Cards: cards,
Message: $"{player.Name} zahral combo {cards.Count}x {cards[0].RankName}!"
));
}
CheckPlayerFinish(player);
AdvanceTurn();
return true;
}
public bool PlayCard(Player player, Card card, Suit? chosenSuit = null)
{
if (!CanPlayCard(player, card)) return false;
// Odstránenie z ruky
player.Hand.Remove(card);
State.DiscardPile.Add(card);
// Ak bola predtým zmenená farba, zrušíme ju (alebo prepíšeme novým horníkom)
State.ActiveSuit = null;
// Efekt Horníka
if (card.IsHornik)
{
if (chosenSuit.HasValue)
{
State.ActiveSuit = chosenSuit.Value;
EmitEvent(new GameEvent(
GameEventType.SuitChanged,
player,
Card: card,
ChosenSuit: chosenSuit.Value,
Message: $"{player.Name} zahral Horníka a zmenil farbu na {card.SuitName}!"
));
}
else
{
// Čakáme na výber farby
State.TurnState = TurnState.SelectingSuit;
State.PendingHornikPlayer = player;
EmitEvent(new GameEvent(
GameEventType.CardPlayed,
player,
Card: card,
Message: $"{player.Name} zahral Horníka a vyberá farbu..."
));
return true;
}
}
else
{
EmitEvent(new GameEvent(
GameEventType.CardPlayed,
player,
Card: card,
Message: $"{player.Name} zahral {card.Name}."
));
}
// Efekt Sedmy
if (card.IsSeven)
{
State.PendingDrawCount += 2;
// Červená sedma vracia do hry
if (card.IsRedSeven && State.Rules.RedSevenRevives && State.LastFinishedPlayer != null && State.LastFinishedPlayer != player)
{
var revived = State.LastFinishedPlayer;
revived.FinishedPosition = null;
State.LastFinishedPlayer = null;
// Doberie 3 karty
DrawCardsForPlayer(revived, 3);
EmitEvent(new GameEvent(
GameEventType.PlayerRevived,
player,
TargetPlayer: revived,
Card: card,
ExtraData: 3,
Message: $"🔥 {player.Name} zahral Červenú sedmu a vrátil hráča {revived.Name} späť do hry!"
));
}
else
{
EmitEvent(new GameEvent(
GameEventType.SevenStacked,
player,
Card: card,
ExtraData: State.PendingDrawCount,
Message: $"⚡ Sedma! Ďalší hráč berie {State.PendingDrawCount} karty."
));
}
}
// Efekt Esa
if (card.IsAce)
{
if (State.PendingSkip)
{
EmitEvent(new GameEvent(
GameEventType.AceCountered,
player,
Card: card,
Message: $"❄️ {player.Name} prebil Eso vlastným Esom!"
));
}
else
{
EmitEvent(new GameEvent(
GameEventType.AceSkipped,
player,
Card: card,
Message: $"❄️ Eso! Ďalší hráč jedno kolo stojí."
));
}
State.PendingSkip = true;
}
// Kontrola, či hráč vyhral / skončil
CheckPlayerFinish(player);
// Posun na ďalšieho hráča
AdvanceTurn();
return true;
}
public bool ChooseSuit(Player player, Suit suit)
{
if (State.TurnState != TurnState.SelectingSuit || State.PendingHornikPlayer != player)
{
return false;
}
State.ActiveSuit = suit;
State.TurnState = TurnState.Playing;
State.PendingHornikPlayer = null;
var dummyCard = new Card(suit, Rank.OverKnave);
EmitEvent(new GameEvent(
GameEventType.SuitChanged,
player,
ChosenSuit: suit,
Message: $"{player.Name} zmenil farbu na {dummyCard.SuitName} ({dummyCard.SuitSymbol})!"
));
// Kontrola, či hráč vyhodením Horníka skončil
CheckPlayerFinish(player);
AdvanceTurn();
return true;
}
public bool BurnCards(Player player, List<Card> cards)
{
if (!CanBurnCards(player, cards)) return false;
var rank = cards[0].Rank;
foreach (var c in cards)
{
player.Hand.Remove(c);
State.DiscardPile.Add(c);
}
State.ActiveSuit = null;
EmitEvent(new GameEvent(
GameEventType.CardsBurned,
player,
Cards: cards,
Message: $"💥 SPÁLENIE! {player.Name} spálil 4x {cards[0].RankName} a hrá znova!"
));
CheckPlayerFinish(player);
// Pri spálení hráč pokračuje vo svojom ťahu (nevolá sa AdvanceTurn)
return true;
}
public void DrawOrAcknowledge(Player player)
{
if (State.IsGameOver || State.TurnState != TurnState.Playing) return;
if (player != State.CurrentPlayer) return;
if (State.PendingDrawCount > 0)
{
int count = State.PendingDrawCount;
State.PendingDrawCount = 0;
DrawCardsForPlayer(player, count);
EmitEvent(new GameEvent(
GameEventType.CardDrawn,
player,
ExtraData: count,
Message: $"{player.Name} si potiahol {count} trestné karty zo sedmy."
));
AdvanceTurn();
}
else if (State.PendingSkip)
{
State.PendingSkip = false;
EmitEvent(new GameEvent(
GameEventType.AceSkipped,
player,
Message: $"{player.Name} stojí toto kolo kvôli Esu."
));
AdvanceTurn();
}
else
{
// Bežné potiahnutie 1 karty
DrawCardsForPlayer(player, 1);
EmitEvent(new GameEvent(
GameEventType.CardDrawn,
player,
ExtraData: 1,
Message: $"{player.Name} si potiahol kartu z balíčka."
));
AdvanceTurn();
}
}
public void ContinueFromPassScreen()
{
if (State.TurnState == TurnState.PassScreen)
{
State.TurnState = TurnState.Playing;
}
}
private void DrawCardsForPlayer(Player player, int count)
{
for (int i = 0; i < count; i++)
{
EnsureDrawDeckHasCards();
if (State.DrawDeck.Count > 0)
{
var card = State.DrawDeck[0];
State.DrawDeck.RemoveAt(0);
player.Hand.Add(card);
}
}
}
private void EnsureDrawDeckHasCards()
{
if (State.DrawDeck.Count > 0) return;
// Ak je balíček prázdny, vezmeme odhadzovaciu kôpku okrem vrchnej karty
if (State.DiscardPile.Count > 1)
{
var topCard = State.DiscardPile[^1];
var cardsToRecycle = State.DiscardPile.Take(State.DiscardPile.Count - 1).ToList();
State.DiscardPile.Clear();
State.DiscardPile.Add(topCard);
_deckManager.Shuffle(cardsToRecycle);
State.DrawDeck.AddRange(cardsToRecycle);
EmitEvent(new GameEvent(
GameEventType.DeckReshuffled,
null,
Message: "Lízací balíček bol premiešaný z odhadzovacej kôpky."
));
}
}
private void CheckPlayerFinish(Player player)
{
if (player.Hand.Count == 0 && !player.HasFinished)
{
int nextPos = State.Players.Count(p => p.HasFinished) + 1;
player.FinishedPosition = nextPos;
State.LastFinishedPlayer = player;
EmitEvent(new GameEvent(
GameEventType.PlayerFinished,
player,
ExtraData: nextPos,
Message: $"🏆 {player.Name} sa zbavil všetkých kariet a obsadil {nextPos}. miesto!"
));
CheckGameOver();
}
}
private void CheckGameOver()
{
int remaining = State.ActivePlayersCount;
if (remaining <= 1)
{
State.IsGameOver = true;
State.TurnState = TurnState.GameOver;
// Zostávajúcemu hráčovi priradíme posledné miesto
var lastPlayer = State.Players.FirstOrDefault(p => !p.HasFinished);
if (lastPlayer != null)
{
lastPlayer.FinishedPosition = State.Players.Count;
}
EmitEvent(new GameEvent(
GameEventType.GameOver,
null,
Message: "Koniec hry! Všetky pozície sú určené."
));
}
}
private void AdvanceTurn()
{
if (State.IsGameOver) return;
int attempts = 0;
do
{
State.CurrentPlayerIndex = (State.CurrentPlayerIndex + 1) % State.Players.Count;
attempts++;
}
while (State.CurrentPlayer.HasFinished && attempts <= State.Players.Count);
State.TurnCount++;
// Ak hráme Pass & Play a nasledujúci hráč je človek a je zapnutá súkromná clona
if (State.Rules.PassAndPlayPrivacyScreen && State.CurrentPlayer.Type == PlayerType.Human && State.Players.Count(p => p.Type == PlayerType.Human) > 1)
{
State.TurnState = TurnState.PassScreen;
}
else
{
State.TurnState = TurnState.Playing;
}
}
private void EmitEvent(GameEvent e)
{
OnGameEvent?.Invoke(e);
}
}