inial impl
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
namespace Faraon.Core.AI;
|
||||
|
||||
using Faraon.Core.Models;
|
||||
using Faraon.Core.Engine;
|
||||
|
||||
public enum AIDecisionType
|
||||
{
|
||||
PlayCard,
|
||||
BurnCards,
|
||||
DrawOrAcknowledge
|
||||
}
|
||||
|
||||
public record AIDecision(
|
||||
AIDecisionType Type,
|
||||
Card? Card = null,
|
||||
List<Card>? Cards = null,
|
||||
Suit? ChosenSuit = null
|
||||
);
|
||||
|
||||
public interface IFaraonAI
|
||||
{
|
||||
AIDecision MakeDecision(GameState state, Player player);
|
||||
}
|
||||
|
||||
public class FaraonAI : IFaraonAI
|
||||
{
|
||||
private readonly Random _random = new();
|
||||
|
||||
public AIDecision MakeDecision(GameState state, Player player)
|
||||
{
|
||||
// 1. Kontrola Spálenia (4 rovnaké karty)
|
||||
if (state.Rules.FourOfAKindBurns && state.PendingDrawCount == 0 && !state.PendingSkip)
|
||||
{
|
||||
var fourOfAKindGroup = player.Hand
|
||||
.GroupBy(c => c.Rank)
|
||||
.FirstOrDefault(g => g.Count() == 4);
|
||||
|
||||
if (fourOfAKindGroup != null)
|
||||
{
|
||||
var burnList = fourOfAKindGroup.ToList();
|
||||
if (ActionValidator.CanBurnCards(burnList, state.PendingDrawCount, state.PendingSkip, state.Rules))
|
||||
{
|
||||
return new AIDecision(AIDecisionType.BurnCards, Cards: burnList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Nájdi všetky zahrateľné karty
|
||||
var playableCards = player.Hand
|
||||
.Where(c => ActionValidator.CanPlayCard(
|
||||
c,
|
||||
state.TopCard,
|
||||
state.ActiveSuit,
|
||||
state.PendingDrawCount,
|
||||
state.PendingSkip,
|
||||
state.Rules))
|
||||
.ToList();
|
||||
|
||||
// Ak nemá čo zahrať -> musí si potiahnuť alebo akceptovať trest
|
||||
if (playableCards.Count == 0)
|
||||
{
|
||||
return new AIDecision(AIDecisionType.DrawOrAcknowledge);
|
||||
}
|
||||
|
||||
// 3. Rozhodovanie podľa obtiažnosti
|
||||
Card selectedCard;
|
||||
if (player.Difficulty == AIDifficulty.Easy)
|
||||
{
|
||||
selectedCard = playableCards[_random.Next(playableCards.Count)];
|
||||
}
|
||||
else if (player.Difficulty == AIDifficulty.Hard)
|
||||
{
|
||||
selectedCard = ChooseBestCardSmart(playableCards, player, state);
|
||||
}
|
||||
else // Medium
|
||||
{
|
||||
selectedCard = ChooseBestCardMedium(playableCards, player, state);
|
||||
}
|
||||
|
||||
// Zozbierame všetky karty rovnakej hodnoty v ruke (combo vyloženie)
|
||||
var multiCards = player.Hand.Where(c => c.Rank == selectedCard.Rank).ToList();
|
||||
|
||||
// 4. Ak je vybraná karta Horník, vyber najvýhodnejšiu farbu
|
||||
Suit? chosenSuit = null;
|
||||
if (selectedCard.IsHornik)
|
||||
{
|
||||
chosenSuit = PickBestSuit(player.Hand.Where(c => !multiCards.Contains(c)).ToList());
|
||||
}
|
||||
|
||||
return new AIDecision(AIDecisionType.PlayCard, Card: selectedCard, Cards: multiCards, ChosenSuit: chosenSuit);
|
||||
}
|
||||
|
||||
private Card ChooseBestCardMedium(List<Card> playable, Player player, GameState state)
|
||||
{
|
||||
// Ak sa musí brániť proti sedme, zahrá sedmu
|
||||
if (state.PendingDrawCount > 0)
|
||||
{
|
||||
var seven = playable.FirstOrDefault(c => c.IsSeven);
|
||||
if (seven != null) return seven;
|
||||
}
|
||||
|
||||
// Ak sa musí brániť proti esu, zahrá eso
|
||||
if (state.PendingSkip)
|
||||
{
|
||||
var ace = playable.FirstOrDefault(c => c.IsAce);
|
||||
if (ace != null) return ace;
|
||||
}
|
||||
|
||||
// Ak má v ruke len 1 kartu navyše a môže vyhrať
|
||||
if (player.Hand.Count <= 2)
|
||||
{
|
||||
return playable[0];
|
||||
}
|
||||
|
||||
// Radšej šetrí Horníka ako žolíka, ak má inú normálnu kartu
|
||||
var nonHornik = playable.Where(c => !c.IsHornik).ToList();
|
||||
if (nonHornik.Count > 0)
|
||||
{
|
||||
return nonHornik[_random.Next(nonHornik.Count)];
|
||||
}
|
||||
|
||||
return playable[0];
|
||||
}
|
||||
|
||||
private Card ChooseBestCardSmart(List<Card> playable, Player player, GameState state)
|
||||
{
|
||||
// 1. Ak čelíme sedme, prioritne zahráme obyčajnú sedmu pred Červenou sedmou (ak je niekto vonku, šetríme ju)
|
||||
if (state.PendingDrawCount > 0)
|
||||
{
|
||||
var regularSeven = playable.FirstOrDefault(c => c.IsSeven && !c.IsRedSeven);
|
||||
if (regularSeven != null) return regularSeven;
|
||||
var redSeven = playable.FirstOrDefault(c => c.IsRedSeven);
|
||||
if (redSeven != null) return redSeven;
|
||||
}
|
||||
|
||||
// 2. Ak čelíme esu, prebijeme esom
|
||||
if (state.PendingSkip)
|
||||
{
|
||||
var ace = playable.FirstOrDefault(c => c.IsAce);
|
||||
if (ace != null) return ace;
|
||||
}
|
||||
|
||||
// 3. Ak môžeme touto kartou hru ukončiť (posledná karta v ruke)
|
||||
if (player.Hand.Count == 1)
|
||||
{
|
||||
return playable[0];
|
||||
}
|
||||
|
||||
// 4. Ak má Červenú sedmu a na stole je niekto na oživenie (LastFinishedPlayer), oživíme ho!
|
||||
if (state.Rules.RedSevenRevives && state.LastFinishedPlayer != null)
|
||||
{
|
||||
var redSeven = playable.FirstOrDefault(c => c.IsRedSeven);
|
||||
if (redSeven != null) return redSeven;
|
||||
}
|
||||
|
||||
// 5. Ak máme iné možnosti, šetríme si Horníka na núdzu
|
||||
var nonHornikCards = playable.Where(c => !c.IsHornik).ToList();
|
||||
if (nonHornikCards.Count > 0)
|
||||
{
|
||||
// Zvoľ kartu farby, z ktorej máme najviac kariet
|
||||
var suitCounts = player.Hand
|
||||
.GroupBy(c => c.Suit)
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
return nonHornikCards
|
||||
.OrderByDescending(c => suitCounts.GetValueOrDefault(c.Suit, 0))
|
||||
.First();
|
||||
}
|
||||
|
||||
return playable[0];
|
||||
}
|
||||
|
||||
public static Suit PickBestSuit(List<Card> remainingHand)
|
||||
{
|
||||
if (remainingHand.Count == 0) return Suit.Hearts;
|
||||
|
||||
// Nájdi farbu, ktorej máme najviac v ruke
|
||||
var mostFrequentSuit = remainingHand
|
||||
.GroupBy(c => c.Suit)
|
||||
.OrderByDescending(g => g.Count())
|
||||
.First()
|
||||
.Key;
|
||||
|
||||
return mostFrequentSuit;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,129 @@
|
||||
namespace Faraon.Core.Models;
|
||||
|
||||
public enum Suit
|
||||
{
|
||||
Hearts = 0, // Červeň / Srdce ❤️
|
||||
Leaves = 1, // Zeleň / List 🍃
|
||||
Acorns = 2, // Žaluď 🌰
|
||||
Bells = 3 // Guľa 🔔
|
||||
}
|
||||
|
||||
public enum Rank
|
||||
{
|
||||
Seven = 7,
|
||||
Eight = 8,
|
||||
Nine = 9,
|
||||
Ten = 10,
|
||||
UnderKnave = 11, // Dolník / Spodok
|
||||
OverKnave = 12, // Horník / Filak / Menič
|
||||
King = 13, // Kráľ
|
||||
Ace = 14 // Eso
|
||||
}
|
||||
|
||||
public record Card(Suit Suit, Rank Rank)
|
||||
{
|
||||
public string Id => $"{Suit}_{Rank}";
|
||||
|
||||
public string Name => $"{RankName} ({SuitName})";
|
||||
|
||||
public string ShortName => Rank switch
|
||||
{
|
||||
Rank.Seven => "VII",
|
||||
Rank.Eight => "VIII",
|
||||
Rank.Nine => "IX",
|
||||
Rank.Ten => "X",
|
||||
Rank.UnderKnave => "D",
|
||||
Rank.OverKnave => "H",
|
||||
Rank.King => "K",
|
||||
Rank.Ace => "E",
|
||||
_ => Rank.ToString()
|
||||
};
|
||||
|
||||
public string NumericShortName => Rank switch
|
||||
{
|
||||
Rank.Seven => "7",
|
||||
Rank.Eight => "8",
|
||||
Rank.Nine => "9",
|
||||
Rank.Ten => "10",
|
||||
Rank.UnderKnave => "D",
|
||||
Rank.OverKnave => "H",
|
||||
Rank.King => "K",
|
||||
Rank.Ace => "E",
|
||||
_ => Rank.ToString()
|
||||
};
|
||||
|
||||
public string RankName => Rank switch
|
||||
{
|
||||
Rank.Seven => "Sedma",
|
||||
Rank.Eight => "Osma",
|
||||
Rank.Nine => "Deviatka",
|
||||
Rank.Ten => "Desiatka",
|
||||
Rank.UnderKnave => "Dolník",
|
||||
Rank.OverKnave => "Horník",
|
||||
Rank.King => "Kráľ",
|
||||
Rank.Ace => "Eso",
|
||||
_ => Rank.ToString()
|
||||
};
|
||||
|
||||
public string SuitName => Suit switch
|
||||
{
|
||||
Suit.Hearts => "Červeň",
|
||||
Suit.Leaves => "Zeleň",
|
||||
Suit.Acorns => "Žaluď",
|
||||
Suit.Bells => "Guľa",
|
||||
_ => Suit.ToString()
|
||||
};
|
||||
|
||||
public string SuitSymbol => Suit switch
|
||||
{
|
||||
Suit.Hearts => "♥",
|
||||
Suit.Leaves => "🍃",
|
||||
Suit.Acorns => "🌰",
|
||||
Suit.Bells => "🔔",
|
||||
_ => "?"
|
||||
};
|
||||
|
||||
public string SuitColor => Suit switch
|
||||
{
|
||||
Suit.Hearts => "#d62828", // Tradičná sýta červeň
|
||||
Suit.Leaves => "#2d6a4f", // Tradičná lesná zeleň
|
||||
Suit.Acorns => "#8b5a2b", // Hnedo-zlatý žaluď
|
||||
Suit.Bells => "#d48b1a", // Zlatistá rolničková guľa
|
||||
_ => "#ffffff"
|
||||
};
|
||||
|
||||
public string SeasonName => (Rank == Rank.Ace) ? Suit switch
|
||||
{
|
||||
Suit.Hearts => "Jar",
|
||||
Suit.Leaves => "Leto",
|
||||
Suit.Acorns => "Jeseň",
|
||||
Suit.Bells => "Zima",
|
||||
_ => ""
|
||||
} : "";
|
||||
|
||||
public string CharacterName => Rank switch
|
||||
{
|
||||
Rank.OverKnave => Suit switch
|
||||
{
|
||||
Suit.Hearts => "Wilhelm Tell",
|
||||
Suit.Leaves => "Walter Fürst",
|
||||
Suit.Acorns => "Ulrich Rudenz",
|
||||
Suit.Bells => "Stauffacher",
|
||||
_ => "Horník"
|
||||
},
|
||||
Rank.UnderKnave => Suit switch
|
||||
{
|
||||
Suit.Hearts => "Kuoni",
|
||||
Suit.Leaves => "Itel Reding",
|
||||
Suit.Acorns => "Stüssi",
|
||||
Suit.Bells => "Harrass",
|
||||
_ => "Dolník"
|
||||
},
|
||||
_ => ""
|
||||
};
|
||||
|
||||
public bool IsSeven => Rank == Rank.Seven;
|
||||
public bool IsAce => Rank == Rank.Ace;
|
||||
public bool IsHornik => Rank == Rank.OverKnave;
|
||||
public bool IsRedSeven => Rank == Rank.Seven && Suit == Suit.Hearts;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace Faraon.Core.Models;
|
||||
|
||||
public class Deck
|
||||
{
|
||||
private readonly Random _random;
|
||||
|
||||
public Deck(int? seed = null)
|
||||
{
|
||||
_random = seed.HasValue ? new Random(seed.Value) : new Random();
|
||||
}
|
||||
|
||||
public static List<Card> CreateStandardDeck()
|
||||
{
|
||||
var cards = new List<Card>(32);
|
||||
foreach (Suit suit in Enum.GetValues<Suit>())
|
||||
{
|
||||
foreach (Rank rank in Enum.GetValues<Rank>())
|
||||
{
|
||||
cards.Add(new Card(suit, rank));
|
||||
}
|
||||
}
|
||||
return cards;
|
||||
}
|
||||
|
||||
public List<Card> CreateShuffledDeck()
|
||||
{
|
||||
var deck = CreateStandardDeck();
|
||||
Shuffle(deck);
|
||||
return deck;
|
||||
}
|
||||
|
||||
public void Shuffle<T>(List<T> list)
|
||||
{
|
||||
int n = list.Count;
|
||||
while (n > 1)
|
||||
{
|
||||
n--;
|
||||
int k = _random.Next(n + 1);
|
||||
(list[k], list[n]) = (list[n], list[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Faraon.Core.Models;
|
||||
|
||||
public enum GameEventType
|
||||
{
|
||||
GameStarted,
|
||||
CardPlayed,
|
||||
CardsBurned,
|
||||
CardDrawn,
|
||||
SevenStacked,
|
||||
AceSkipped,
|
||||
AceCountered,
|
||||
SuitChanged,
|
||||
PlayerRevived,
|
||||
PlayerFinished,
|
||||
GameOver,
|
||||
DeckReshuffled
|
||||
}
|
||||
|
||||
public record GameEvent(
|
||||
GameEventType Type,
|
||||
Player? SourcePlayer,
|
||||
Player? TargetPlayer = null,
|
||||
Card? Card = null,
|
||||
List<Card>? Cards = null,
|
||||
Suit? ChosenSuit = null,
|
||||
int ExtraData = 0,
|
||||
string Message = ""
|
||||
);
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace Faraon.Core.Models;
|
||||
|
||||
public enum TurnState
|
||||
{
|
||||
Playing,
|
||||
SelectingSuit,
|
||||
PassScreen,
|
||||
GameOver
|
||||
}
|
||||
|
||||
public class GameState
|
||||
{
|
||||
public List<Player> Players { get; set; } = new();
|
||||
public int CurrentPlayerIndex { get; set; } = 0;
|
||||
public List<Card> DrawDeck { get; set; } = new();
|
||||
public List<Card> DiscardPile { get; set; } = new();
|
||||
public Suit? ActiveSuit { get; set; }
|
||||
public int PendingDrawCount { get; set; } = 0;
|
||||
public bool PendingSkip { get; set; } = false;
|
||||
public RuleOptions Rules { get; set; } = new();
|
||||
public bool IsGameOver { get; set; } = false;
|
||||
public Player? LastFinishedPlayer { get; set; }
|
||||
public int TurnCount { get; set; } = 0;
|
||||
public TurnState TurnState { get; set; } = TurnState.Playing;
|
||||
public Player? PendingHornikPlayer { get; set; }
|
||||
public Card? TopCard => DiscardPile.Count > 0 ? DiscardPile[^1] : null;
|
||||
|
||||
public Player CurrentPlayer => Players.Count > 0 ? Players[CurrentPlayerIndex % Players.Count] : new Player();
|
||||
|
||||
public int ActivePlayersCount => Players.Count(p => !p.HasFinished);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace Faraon.Core.Models;
|
||||
|
||||
public enum PlayerType
|
||||
{
|
||||
Human = 0,
|
||||
AI = 1
|
||||
}
|
||||
|
||||
public enum AIDifficulty
|
||||
{
|
||||
Easy = 0,
|
||||
Medium = 1,
|
||||
Hard = 2
|
||||
}
|
||||
|
||||
public class Player
|
||||
{
|
||||
public string Id { get; init; } = Guid.NewGuid().ToString("N");
|
||||
public string Name { get; set; } = "Hráč";
|
||||
public PlayerType Type { get; set; } = PlayerType.Human;
|
||||
public AIDifficulty Difficulty { get; set; } = AIDifficulty.Medium;
|
||||
public string Avatar { get; set; } = "👑";
|
||||
public List<Card> Hand { get; } = new();
|
||||
|
||||
public int? FinishedPosition { get; set; }
|
||||
public bool HasFinished => FinishedPosition.HasValue;
|
||||
|
||||
public bool IsSkipped { get; set; }
|
||||
|
||||
public Player() { }
|
||||
|
||||
public Player(string name, PlayerType type, string avatar = "👤", AIDifficulty difficulty = AIDifficulty.Medium)
|
||||
{
|
||||
Name = name;
|
||||
Type = type;
|
||||
Avatar = avatar;
|
||||
Difficulty = difficulty;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Hand.Clear();
|
||||
FinishedPosition = null;
|
||||
IsSkipped = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace Faraon.Core.Models;
|
||||
|
||||
public class RuleOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Počet kariet rozdaných každému hráčovi na začiatku (zvyčajne 4 alebo 5).
|
||||
/// </summary>
|
||||
public int InitialCardCount { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Ak je zapnuté, sedma na sedmu pridáva ďalšie +2 karty (kumulácia trestu).
|
||||
/// </summary>
|
||||
public bool StackSevens { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Ak je zapnuté, eso možno prebiť vlastným esom a preniesť státie na ďalšieho hráča.
|
||||
/// </summary>
|
||||
public bool CounterAceWithAce { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Červená sedma vracia do hry hráča, ktorý sa už zbavil všetkých kariet (posledného víťaza).
|
||||
/// </summary>
|
||||
public bool RedSevenRevives { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Zahranie 4 rovnakých kariet naraz ("Spálenie"). Spáli kôpku a hráč hrá znova.
|
||||
/// </summary>
|
||||
public bool FourOfAKindBurns { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Režim Pass & Play so súkromnou clonou medzi ťahmi viacerých živých hráčov.
|
||||
/// </summary>
|
||||
public bool PassAndPlayPrivacyScreen { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Rýchlosť AI ťahov v milisekundách (pre plynulé sledovanie animácií).
|
||||
/// </summary>
|
||||
public int AIDelayMs { get; set; } = 900;
|
||||
|
||||
/// <summary>
|
||||
/// Zvolený vizuálny štýl balíčka ("original" alebo "spongebob").
|
||||
/// </summary>
|
||||
public string SelectedDeck { get; set; } = "original";
|
||||
}
|
||||
Reference in New Issue
Block a user