inial impl
This commit is contained in:
@@ -0,0 +1,495 @@
|
||||
@page "/"
|
||||
@inject FxService Fx
|
||||
@inject IFaraonAI AI
|
||||
@inject IJSRuntime JS
|
||||
|
||||
@if (IsInLobby || Engine == null)
|
||||
{
|
||||
<Lobby OnStartGame="HandleStartGame" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div id="game-table-container">
|
||||
<!-- Canvas pre časticové efekty (oheň, ľad, konfety) -->
|
||||
<canvas id="particle-canvas"></canvas>
|
||||
|
||||
<!-- Horná lišta -->
|
||||
<header class="game-header">
|
||||
<h1 class="game-title">FARAÓN</h1>
|
||||
<div style="font-size: 0.95rem; font-weight: 700; color: #a3b899;">
|
||||
Ťah #@Engine.State.TurnCount • Na rade:
|
||||
<span style="color: var(--gold); font-weight: 900;">
|
||||
@Engine.State.CurrentPlayer.Avatar @Engine.State.CurrentPlayer.Name
|
||||
</span>
|
||||
</div>
|
||||
<div class="game-controls">
|
||||
<!-- Rýchly prepínač balíčka kariet -->
|
||||
<button class="btn-game-icon" title="Zmeniť balíček kariet (Original / SpongeBob)" @onclick="ToggleDeck">
|
||||
@(Engine.State.Rules.SelectedDeck == "spongebob" ? "🧽" : "🎴")
|
||||
</button>
|
||||
<button class="btn-game-icon" title="Stlmiť / Zapnúť zvuk" @onclick="ToggleMute">
|
||||
@(IsMuted ? "🔇" : "🔊")
|
||||
</button>
|
||||
<button class="btn-game-icon" title="Návrat do Lobby" @onclick="ReturnToLobby">
|
||||
🏠
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Herný stôl -->
|
||||
<div class="poker-table-surface">
|
||||
<!-- Súperi na stole -->
|
||||
@foreach (var opp in GetOpponents())
|
||||
{
|
||||
<PlayerSlot Player="@opp.Player"
|
||||
IsCurrentTurn="@(Engine.State.CurrentPlayer == opp.Player)"
|
||||
PositionClass="@opp.PositionClass" />
|
||||
}
|
||||
|
||||
<!-- Stred stola: Lízací balíček a Odhadzovacia kôpka -->
|
||||
<div class="slot-center">
|
||||
<!-- Lízací balíček -->
|
||||
<div class="deck-card-container" @onclick="HandleDeckClick" title="Potiahnuť kartu">
|
||||
<CardView IsBack="true" Deck="@Engine.State.Rules.SelectedDeck" CustomStyle="box-shadow: 0 10px 25px rgba(0,0,0,0.6);" />
|
||||
<div class="deck-count-badge">
|
||||
@Engine.State.DrawDeck.Count
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Odhadzovacia kôpka -->
|
||||
<div class="discard-pile-container">
|
||||
@if (Engine.State.TopCard != null)
|
||||
{
|
||||
<CardView Card="@Engine.State.TopCard"
|
||||
Deck="@Engine.State.Rules.SelectedDeck"
|
||||
CustomStyle="box-shadow: 0 10px 25px rgba(0,0,0,0.7);" />
|
||||
}
|
||||
|
||||
<!-- Indikátor trestu zo sedmy -->
|
||||
@if (Engine.State.PendingDrawCount > 0)
|
||||
{
|
||||
<div class="seven-penalty-badge">
|
||||
🔥 +@Engine.State.PendingDrawCount Kariet!
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Indikátor zvolenej farby z Horníka -->
|
||||
@if (Engine.State.ActiveSuit.HasValue)
|
||||
{
|
||||
var dummy = new Card(Engine.State.ActiveSuit.Value, Rank.OverKnave);
|
||||
<div class="active-suit-badge" style="color: @dummy.SuitColor;">
|
||||
<span>Farba:</span>
|
||||
<div style="width: 20px; height: 20px; display: inline-flex; vertical-align: middle;">
|
||||
<MariasPip Suit="@Engine.State.ActiveSuit.Value" />
|
||||
</div>
|
||||
<span>@dummy.SuitName</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spodný slot: Aktívny hráč v zobrazení -->
|
||||
<div class="slot-bottom">
|
||||
<div class="avatar-badge @(IsViewingPlayerTurn ? "active-turn" : "") @(ViewingPlayer.IsSkipped ? "is-skipped" : "")"
|
||||
style="margin-bottom: 8px;">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span class="avatar-icon">@ViewingPlayer.Avatar</span>
|
||||
<span class="avatar-name">@ViewingPlayer.Name</span>
|
||||
<span class="avatar-card-count">(@ViewingPlayer.Hand.Count kariet)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interaktívna ruka hráča -->
|
||||
<PlayerHandView Player="@ViewingPlayer"
|
||||
Deck="@Engine.State.Rules.SelectedDeck"
|
||||
IsMyTurn="@IsViewingPlayerTurn"
|
||||
CanPlayCardFunc="@(c => Engine.CanPlayCard(ViewingPlayer, c))"
|
||||
CanPlayCardsFunc="@(cards => Engine.CanPlayCards(ViewingPlayer, cards))"
|
||||
OnPlayCards="HandlePlayCards" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Akčné tlačidlá pre aktívneho živého hráča -->
|
||||
@if (IsViewingPlayerTurn && Engine.State.TurnState == TurnState.Playing)
|
||||
{
|
||||
<div style="position: absolute; bottom: 18px; right: 24px; z-index: 60; display: flex; gap: 10px;">
|
||||
@if (CheckFourOfAKindRank(ViewingPlayer) is Rank burnRank)
|
||||
{
|
||||
<button class="btn-primary-glow" style="background: linear-gradient(135deg, #ff4500, #ff8c00); box-shadow: 0 0 20px #ff4500;"
|
||||
@onclick="() => HandleBurn(burnRank)">
|
||||
💥 Spáliť 4x @burnRank!
|
||||
</button>
|
||||
}
|
||||
|
||||
<button class="btn-pill" style="background: rgba(0,0,0,0.6); border: 2px solid var(--gold);"
|
||||
@onclick="HandleDeckClick">
|
||||
@if (Engine.State.PendingDrawCount > 0)
|
||||
{
|
||||
<span>Ťahať +@Engine.State.PendingDrawCount 🃏</span>
|
||||
}
|
||||
else if (Engine.State.PendingSkip)
|
||||
{
|
||||
<span>Stáť toto kolo ❄️</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Líznúť kartu 📥</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Toast / Ticker poslednej akcie -->
|
||||
@if (!string.IsNullOrEmpty(LastEventMessage))
|
||||
{
|
||||
<div class="event-ticker">
|
||||
@LastEventMessage
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Modálne okná -->
|
||||
@if (Engine.State.TurnState == TurnState.SelectingSuit && Engine.State.PendingHornikPlayer == ViewingPlayer)
|
||||
{
|
||||
<SuitPickerModal OnSuitSelected="HandleSuitSelected" />
|
||||
}
|
||||
|
||||
@if (Engine.State.TurnState == TurnState.PassScreen)
|
||||
{
|
||||
<PassScreenModal Player="@Engine.State.CurrentPlayer" OnContinue="HandlePassContinue" />
|
||||
}
|
||||
|
||||
@if (Engine.State.IsGameOver)
|
||||
{
|
||||
<GameOverModal Players="@Engine.State.Players"
|
||||
OnPlayAgain="HandlePlayAgain"
|
||||
OnBackToLobby="ReturnToLobby" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private bool IsInLobby = true;
|
||||
private bool IsMuted = false;
|
||||
private GameEngine? Engine;
|
||||
private string LastEventMessage = "";
|
||||
private System.Threading.CancellationTokenSource? _aiCts;
|
||||
|
||||
private List<Player> ConfiguredPlayers = new();
|
||||
private RuleOptions ConfiguredRules = new();
|
||||
|
||||
private Player ViewingPlayer
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Engine == null || Engine.State.Players.Count == 0) return new Player();
|
||||
|
||||
// Ak je Pass & Play a aktuálny hráč je človek, zobrazujeme jeho ruku
|
||||
if (Engine.State.CurrentPlayer.Type == PlayerType.Human)
|
||||
{
|
||||
return Engine.State.CurrentPlayer;
|
||||
}
|
||||
|
||||
// Inak primárne prvého živého hráča v hre
|
||||
var firstHuman = Engine.State.Players.FirstOrDefault(p => p.Type == PlayerType.Human);
|
||||
return firstHuman ?? Engine.State.CurrentPlayer;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsViewingPlayerTurn =>
|
||||
Engine != null &&
|
||||
Engine.State.TurnState == TurnState.Playing &&
|
||||
Engine.State.CurrentPlayer == ViewingPlayer;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await Fx.InitParticlesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleStartGame((List<Player> players, RuleOptions rules) setup)
|
||||
{
|
||||
ConfiguredPlayers = setup.players;
|
||||
ConfiguredRules = setup.rules;
|
||||
|
||||
StartNewGame();
|
||||
}
|
||||
|
||||
private void StartNewGame()
|
||||
{
|
||||
// Klony hráčov pre čistú hru
|
||||
var gamePlayers = ConfiguredPlayers.Select(p => new Player(p.Name, p.Type, p.Avatar, p.Difficulty)).ToList();
|
||||
|
||||
Engine = new GameEngine();
|
||||
Engine.OnGameEvent += HandleGameEvent;
|
||||
Engine.StartGame(gamePlayers, ConfiguredRules);
|
||||
|
||||
IsInLobby = false;
|
||||
LastEventMessage = "Hra začala! Veľa šťastia.";
|
||||
|
||||
StateHasChanged();
|
||||
CheckTriggerAI();
|
||||
}
|
||||
|
||||
private void HandleGameEvent(GameEvent e)
|
||||
{
|
||||
LastEventMessage = e.Message;
|
||||
|
||||
_ = InvokeAsync(async () =>
|
||||
{
|
||||
switch (e.Type)
|
||||
{
|
||||
case GameEventType.CardPlayed:
|
||||
await Fx.PlayCardSnapAsync();
|
||||
break;
|
||||
case GameEventType.CardDrawn:
|
||||
await Fx.PlayCardDrawAsync();
|
||||
break;
|
||||
case GameEventType.SevenStacked:
|
||||
await Fx.PlaySevenStackAsync(Math.Max(1, e.ExtraData / 2));
|
||||
break;
|
||||
case GameEventType.AceSkipped:
|
||||
case GameEventType.AceCountered:
|
||||
await Fx.PlayAceFreezeAsync();
|
||||
break;
|
||||
case GameEventType.SuitChanged:
|
||||
await Fx.PlayHornikChimeAsync();
|
||||
break;
|
||||
case GameEventType.CardsBurned:
|
||||
await Fx.PlayBurnExplosionAsync();
|
||||
break;
|
||||
case GameEventType.PlayerFinished:
|
||||
case GameEventType.GameOver:
|
||||
await Fx.PlayVictoryAsync();
|
||||
break;
|
||||
}
|
||||
|
||||
if (Engine != null && Engine.State.CurrentPlayer.Hand.Count == 1 && !Engine.State.CurrentPlayer.HasFinished)
|
||||
{
|
||||
await Fx.PlayHeartbeatAsync();
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private string GetPlayerPosition(Player player)
|
||||
{
|
||||
if (player == ViewingPlayer) return "bottom";
|
||||
var opp = GetOpponents().FirstOrDefault(o => o.Player == player);
|
||||
if (!string.IsNullOrEmpty(opp.PositionClass))
|
||||
{
|
||||
return opp.PositionClass.Replace("slot-", "");
|
||||
}
|
||||
return "top";
|
||||
}
|
||||
|
||||
private void ToggleDeck()
|
||||
{
|
||||
if (Engine == null) return;
|
||||
Engine.State.Rules.SelectedDeck = Engine.State.Rules.SelectedDeck == "spongebob" ? "original" : "spongebob";
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task HandlePlayCards(List<Card> cards)
|
||||
{
|
||||
if (Engine == null || cards.Count == 0) return;
|
||||
string pos = GetPlayerPosition(ViewingPlayer);
|
||||
string deck = Engine.State.Rules.SelectedDeck;
|
||||
|
||||
foreach (var card in cards)
|
||||
{
|
||||
_ = Fx.AnimateCardPlayAsync(pos, card, deck);
|
||||
if (cards.Count > 1) await Task.Delay(90);
|
||||
}
|
||||
|
||||
Engine.PlayCards(ViewingPlayer, cards);
|
||||
StateHasChanged();
|
||||
CheckTriggerAI();
|
||||
}
|
||||
|
||||
private async Task HandleDeckClick()
|
||||
{
|
||||
if (Engine == null || !IsViewingPlayerTurn) return;
|
||||
string pos = GetPlayerPosition(ViewingPlayer);
|
||||
string deck = Engine.State.Rules.SelectedDeck;
|
||||
|
||||
if (Engine.State.PendingDrawCount > 0)
|
||||
{
|
||||
for (int i = 0; i < Math.Min(Engine.State.PendingDrawCount, 4); i++)
|
||||
{
|
||||
_ = Fx.AnimateCardDrawAsync(pos, deck);
|
||||
await Task.Delay(90);
|
||||
}
|
||||
}
|
||||
else if (!Engine.State.PendingSkip)
|
||||
{
|
||||
await Fx.AnimateCardDrawAsync(pos, deck);
|
||||
}
|
||||
|
||||
Engine.DrawOrAcknowledge(ViewingPlayer);
|
||||
StateHasChanged();
|
||||
CheckTriggerAI();
|
||||
}
|
||||
|
||||
private void HandleSuitSelected(Suit suit)
|
||||
{
|
||||
if (Engine == null) return;
|
||||
Engine.ChooseSuit(ViewingPlayer, suit);
|
||||
StateHasChanged();
|
||||
CheckTriggerAI();
|
||||
}
|
||||
|
||||
private void HandlePassContinue()
|
||||
{
|
||||
if (Engine == null) return;
|
||||
Engine.ContinueFromPassScreen();
|
||||
StateHasChanged();
|
||||
CheckTriggerAI();
|
||||
}
|
||||
|
||||
private async Task HandleBurn(Rank rank)
|
||||
{
|
||||
if (Engine == null) return;
|
||||
var cards = ViewingPlayer.Hand.Where(c => c.Rank == rank).ToList();
|
||||
if (cards.Count == 4)
|
||||
{
|
||||
string pos = GetPlayerPosition(ViewingPlayer);
|
||||
string deck = Engine.State.Rules.SelectedDeck;
|
||||
foreach (var c in cards)
|
||||
{
|
||||
_ = Fx.AnimateCardPlayAsync(pos, c, deck);
|
||||
await Task.Delay(75);
|
||||
}
|
||||
Engine.BurnCards(ViewingPlayer, cards);
|
||||
StateHasChanged();
|
||||
CheckTriggerAI();
|
||||
}
|
||||
}
|
||||
|
||||
private Rank? CheckFourOfAKindRank(Player player)
|
||||
{
|
||||
if (Engine == null || !Engine.State.Rules.FourOfAKindBurns) return null;
|
||||
var group = player.Hand.GroupBy(c => c.Rank).FirstOrDefault(g => g.Count() == 4);
|
||||
return group?.Key;
|
||||
}
|
||||
|
||||
private void CheckTriggerAI()
|
||||
{
|
||||
if (Engine == null || Engine.State.IsGameOver) return;
|
||||
|
||||
if (Engine.State.TurnState == TurnState.Playing && Engine.State.CurrentPlayer.Type == PlayerType.AI)
|
||||
{
|
||||
_aiCts?.Cancel();
|
||||
_aiCts = new System.Threading.CancellationTokenSource();
|
||||
var token = _aiCts.Token;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(Engine.State.Rules.AIDelayMs, token);
|
||||
if (token.IsCancellationRequested || Engine == null) return;
|
||||
|
||||
var aiPlayer = Engine.State.CurrentPlayer;
|
||||
if (aiPlayer.Type != PlayerType.AI) return;
|
||||
|
||||
var decision = AI.MakeDecision(Engine.State, aiPlayer);
|
||||
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
string pos = GetPlayerPosition(aiPlayer);
|
||||
string deck = Engine.State.Rules.SelectedDeck;
|
||||
|
||||
if (decision.Type == AIDecisionType.PlayCard)
|
||||
{
|
||||
var cardsToPlay = decision.Cards ?? (decision.Card != null ? new List<Card> { decision.Card } : new List<Card>());
|
||||
if (cardsToPlay.Count > 0)
|
||||
{
|
||||
foreach (var c in cardsToPlay)
|
||||
{
|
||||
_ = Fx.AnimateCardPlayAsync(pos, c, deck);
|
||||
if (cardsToPlay.Count > 1) await Task.Delay(85);
|
||||
}
|
||||
Engine.PlayCards(aiPlayer, cardsToPlay, decision.ChosenSuit);
|
||||
}
|
||||
}
|
||||
else if (decision.Type == AIDecisionType.BurnCards && decision.Cards != null)
|
||||
{
|
||||
foreach (var c in decision.Cards)
|
||||
{
|
||||
_ = Fx.AnimateCardPlayAsync(pos, c, deck);
|
||||
await Task.Delay(75);
|
||||
}
|
||||
Engine.BurnCards(aiPlayer, decision.Cards);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Engine.State.PendingDrawCount > 0)
|
||||
{
|
||||
for (int i = 0; i < Math.Min(Engine.State.PendingDrawCount, 4); i++)
|
||||
{
|
||||
_ = Fx.AnimateCardDrawAsync(pos, deck);
|
||||
await Task.Delay(90);
|
||||
}
|
||||
}
|
||||
else if (!Engine.State.PendingSkip)
|
||||
{
|
||||
await Fx.AnimateCardDrawAsync(pos, deck);
|
||||
}
|
||||
Engine.DrawOrAcknowledge(aiPlayer);
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
CheckTriggerAI();
|
||||
});
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
}, token);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<(Player Player, string PositionClass)> GetOpponents()
|
||||
{
|
||||
if (Engine == null) return Enumerable.Empty<(Player, string)>();
|
||||
|
||||
var opponents = Engine.State.Players.Where(p => p != ViewingPlayer).ToList();
|
||||
var result = new List<(Player, string)>();
|
||||
|
||||
if (opponents.Count == 1)
|
||||
{
|
||||
result.Add((opponents[0], "slot-top"));
|
||||
}
|
||||
else if (opponents.Count == 2)
|
||||
{
|
||||
result.Add((opponents[0], "slot-left"));
|
||||
result.Add((opponents[1], "slot-right"));
|
||||
}
|
||||
else if (opponents.Count >= 3)
|
||||
{
|
||||
result.Add((opponents[0], "slot-left"));
|
||||
result.Add((opponents[1], "slot-top"));
|
||||
result.Add((opponents[2], "slot-right"));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task ToggleMute()
|
||||
{
|
||||
IsMuted = await Fx.ToggleMuteAsync();
|
||||
}
|
||||
|
||||
private void HandlePlayAgain()
|
||||
{
|
||||
StartNewGame();
|
||||
}
|
||||
|
||||
private void ReturnToLobby()
|
||||
{
|
||||
_aiCts?.Cancel();
|
||||
IsInLobby = true;
|
||||
Engine = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@page "/not-found"
|
||||
@layout MainLayout
|
||||
|
||||
<h3>Not Found</h3>
|
||||
<p>Sorry, the content you are looking for does not exist.</p>
|
||||
Reference in New Issue
Block a user