AI human mode
This commit is contained in:
+310
-37
@@ -1,4 +1,5 @@
|
||||
@page "/"
|
||||
@using Faraon.Core.AI
|
||||
@inject FxService Fx
|
||||
@inject IFaraonAI AI
|
||||
@inject IJSRuntime JS
|
||||
@@ -24,30 +25,46 @@ else
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="turn-header-info">
|
||||
Ťah #@Engine.State.TurnCount • Na rade:
|
||||
<span class="active-turn-header-player">
|
||||
@Engine.State.CurrentPlayer.Avatar @Engine.State.CurrentPlayer.Name
|
||||
</span>
|
||||
</div>
|
||||
<div class="game-controls">
|
||||
<!-- Rýchly prepínač rýchlosti hry (5 úrovní) -->
|
||||
<button class="btn-game-icon btn-speed-control" title="Rýchlosť hry: @GetSpeedDescription(Engine.State.Rules.SpeedLevel) (Klikni pre zmenu)" @onclick="CycleSpeed">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<!-- Rýchlosť hry -->
|
||||
<button class="btn-speed-control" @onclick="CycleSpeed" title="Zmeniť rýchlosť hry">
|
||||
@GetSpeedButtonText(Engine.State.Rules.SpeedLevel)
|
||||
</button>
|
||||
<!-- Rýchly prepínač balíčka kariet -->
|
||||
<button class="btn-game-icon" title="Zmeniť balíček kariet (@Engine.State.Rules.SelectedDeck)" @onclick="ToggleDeck">
|
||||
@GetDeckIcon(Engine.State.Rules.SelectedDeck)
|
||||
</button>
|
||||
<button class="btn-game-icon" title="Stlmiť / Zapnúť zvuk" @onclick="ToggleMute">
|
||||
|
||||
<!-- Zvuk -->
|
||||
<button class="btn-header-action" @onclick="ToggleMute" title="@(IsMuted ? "Zapnúť zvuk" : "Stlmiť zvuk")">
|
||||
@(IsMuted ? "🔇" : "🔊")
|
||||
</button>
|
||||
<button class="btn-game-icon" title="Návrat do Lobby" @onclick="ReturnToLobby">
|
||||
🏠
|
||||
|
||||
<!-- Nová hra -->
|
||||
<button class="btn-header-action" @onclick="ReturnToLobby" title="Späť do menu">
|
||||
⚙️ Menu
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Špeciálny banner kartového vtipu (AI Human Mode) -->
|
||||
@if (CurrentJoke != null)
|
||||
{
|
||||
<div class="ai-joke-banner @(CurrentJoke.IsFadingOut ? "fading-out" : "")">
|
||||
<div class="joke-header">
|
||||
<div class="joke-header-badge">
|
||||
<span>@CurrentJoke.Joke.Icon</span>
|
||||
<span>🎭 @CurrentJoke.Joke.Category</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span class="joke-storyteller">
|
||||
<span>@CurrentJoke.Storyteller.Avatar</span>
|
||||
<span>@CurrentJoke.Storyteller.Name</span>
|
||||
</span>
|
||||
<button class="joke-close-btn" @onclick="DismissJoke" title="Zavrieť vtip">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="joke-setup">@CurrentJoke.Joke.Setup</div>
|
||||
<div class="joke-punchline">@CurrentJoke.Joke.Punchline</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Herný stôl -->
|
||||
<div class="poker-table-surface">
|
||||
<!-- Súperi na stole -->
|
||||
@@ -55,12 +72,15 @@ else
|
||||
{
|
||||
var isOppFrozen = Engine.State.PendingSkip && Engine.State.CurrentPlayer == opp.Player;
|
||||
var canOppBeRevived = Engine.State.Rules.RedSevenRevives && Engine.State.LastFinishedPlayer == opp.Player && Engine.State.ReviveTurnsRemaining > 0;
|
||||
var thought = GetThoughtState(opp.Player);
|
||||
<PlayerSlot Player="@opp.Player"
|
||||
IsCurrentTurn="@(Engine.State.CurrentPlayer == opp.Player)"
|
||||
IsFrozen="@isOppFrozen"
|
||||
CanBeRevived="@canOppBeRevived"
|
||||
ReviveRoundsRemaining="@Engine.State.ReviveRoundsRemaining"
|
||||
ReviveTurnsRemaining="@Engine.State.ReviveTurnsRemaining"
|
||||
ThoughtBubble="@thought?.Text"
|
||||
IsThoughtFadingOut="@(thought?.IsFadingOut ?? false)"
|
||||
IsOlympicMode="@Engine.State.Rules.OlympicMode"
|
||||
PositionClass="@opp.PositionClass" />
|
||||
}
|
||||
@@ -193,6 +213,17 @@ else
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@{
|
||||
var bottomThought = GetThoughtState(ViewingPlayer);
|
||||
}
|
||||
@if (bottomThought != null && !string.IsNullOrEmpty(bottomThought.Text))
|
||||
{
|
||||
<div class="ai-thought-bubble slot-bottom @(bottomThought.IsFadingOut ? "fading-out" : "")">
|
||||
<span class="thought-bubble-prefix">💭</span>
|
||||
<span class="thought-bubble-text">@bottomThought.Text</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Interaktívna ruka hráča -->
|
||||
@@ -416,6 +447,34 @@ else
|
||||
break;
|
||||
}
|
||||
|
||||
if (Engine != null && Engine.State.Rules.AIHumanMode)
|
||||
{
|
||||
// Kartový vtip pri významných udalostiach (veľa sediem, spálenie, oživenie)
|
||||
var joke = AIDialogueService.GetContextualJoke(e.Type, e.ExtraData, Engine.State.TurnCount);
|
||||
if (joke != null)
|
||||
{
|
||||
var storyteller = Engine.State.Players.FirstOrDefault(p => p.Type == PlayerType.AI) ?? e.SourcePlayer ?? Engine.State.CurrentPlayer;
|
||||
TriggerCardJoke(joke, storyteller);
|
||||
}
|
||||
|
||||
if (e.SourcePlayer != null && e.SourcePlayer.Type == PlayerType.AI)
|
||||
{
|
||||
var reaction = AIDialogueService.GetEventReaction(e, e.SourcePlayer);
|
||||
if (!string.IsNullOrEmpty(reaction))
|
||||
{
|
||||
SetThoughtBubble(e.SourcePlayer, reaction, 2800);
|
||||
}
|
||||
}
|
||||
if (e.TargetPlayer != null && e.TargetPlayer.Type == PlayerType.AI && e.TargetPlayer != e.SourcePlayer)
|
||||
{
|
||||
var reaction = AIDialogueService.GetEventReaction(e, e.TargetPlayer);
|
||||
if (!string.IsNullOrEmpty(reaction))
|
||||
{
|
||||
SetThoughtBubble(e.TargetPlayer, reaction, 2800);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Engine != null && Engine.State.CurrentPlayer.Hand.Count == 1 && !Engine.State.CurrentPlayer.HasFinished)
|
||||
{
|
||||
await Fx.PlayHeartbeatAsync();
|
||||
@@ -457,17 +516,25 @@ else
|
||||
|
||||
private int MultiCardDelayMs => Engine?.State.Rules.SpeedLevel switch
|
||||
{
|
||||
1 => 130,
|
||||
2 => 105,
|
||||
4 => 50,
|
||||
5 => 25,
|
||||
_ => 80
|
||||
-5 => 350,
|
||||
-4 => 280,
|
||||
-3 => 220,
|
||||
-2 => 180,
|
||||
-1 => 140,
|
||||
0 => 110,
|
||||
1 => 85,
|
||||
2 => 65,
|
||||
3 => 45,
|
||||
4 => 30,
|
||||
5 => 15,
|
||||
_ => 110
|
||||
};
|
||||
|
||||
private async Task CycleSpeed()
|
||||
{
|
||||
if (Engine == null) return;
|
||||
int next = (Engine.State.Rules.SpeedLevel % 5) + 1;
|
||||
int current = Engine.State.Rules.SpeedLevel;
|
||||
int next = current >= 5 ? -5 : current + 1;
|
||||
Engine.State.Rules.SpeedLevel = next;
|
||||
await Fx.SetSpeedLevelAsync(next);
|
||||
StateHasChanged();
|
||||
@@ -475,22 +542,34 @@ else
|
||||
|
||||
private string GetSpeedButtonText(int level) => level switch
|
||||
{
|
||||
1 => "🐢 0.5x",
|
||||
2 => "🚶 0.75x",
|
||||
3 => "⚡ 1x",
|
||||
4 => "🔥 1.5x",
|
||||
5 => "🚀 2.5x",
|
||||
_ => "⚡ 1x"
|
||||
-5 => "🐢 -5 (0.2x)",
|
||||
-4 => "🐢 -4 (0.35x)",
|
||||
-3 => "🚶 -3 (0.5x)",
|
||||
-2 => "🚶 -2 (0.65x)",
|
||||
-1 => "🍃 -1 (0.8x)",
|
||||
0 => "⚡ 0 (1x)",
|
||||
1 => "🔥 +1 (1.25x)",
|
||||
2 => "🔥 +2 (1.5x)",
|
||||
3 => "🚀 +3 (2x)",
|
||||
4 => "🚀 +4 (2.75x)",
|
||||
5 => "⚡ +5 (4x)",
|
||||
_ => "⚡ 0 (1x)"
|
||||
};
|
||||
|
||||
private string GetSpeedDescription(int level) => level switch
|
||||
{
|
||||
1 => "0.5x (Pomalá)",
|
||||
2 => "0.75x (Pokojná)",
|
||||
3 => "1x (Normálna - stredná)",
|
||||
4 => "1.5x (Rýchla)",
|
||||
5 => "2.5x (Turbo / Blesková)",
|
||||
_ => "1x (Normálna)"
|
||||
-5 => "-5 (0.2x Zen / Extra pomalá)",
|
||||
-4 => "-4 (0.35x Veľmi pomalá)",
|
||||
-3 => "-3 (0.5x Pomalá)",
|
||||
-2 => "-2 (0.65x Pokojná)",
|
||||
-1 => "-1 (0.8x Zvoľnená)",
|
||||
0 => "0 (1x Normálna / Vyvážená)",
|
||||
1 => "+1 (1.25x Svižná)",
|
||||
2 => "+2 (1.5x Rýchla)",
|
||||
3 => "+3 (2x Veľmi rýchla)",
|
||||
4 => "+4 (2.75x Turbo)",
|
||||
5 => "+5 (4x Blesková)",
|
||||
_ => "0 (1x Normálna)"
|
||||
};
|
||||
|
||||
private void ToggleDeck()
|
||||
@@ -647,6 +726,15 @@ else
|
||||
|
||||
if (Engine.State.TurnState == TurnState.Playing && Engine.State.CurrentPlayer.Type == PlayerType.AI)
|
||||
{
|
||||
if (Engine.State.Rules.AIHumanMode && Engine.State.TurnCount >= 28 && Engine.State.TurnCount % 16 == 0 && CurrentJoke == null)
|
||||
{
|
||||
var longJoke = AIDialogueService.GetContextualJoke(null, 0, Engine.State.TurnCount);
|
||||
if (longJoke != null)
|
||||
{
|
||||
TriggerCardJoke(longJoke, Engine.State.CurrentPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
_aiCts?.Cancel();
|
||||
_aiCts = new System.Threading.CancellationTokenSource();
|
||||
var token = _aiCts.Token;
|
||||
@@ -655,12 +743,63 @@ else
|
||||
{
|
||||
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;
|
||||
|
||||
if (Engine.State.Rules.AIHumanMode)
|
||||
{
|
||||
string thought = AIDialogueService.GetThinkingDialogue(aiPlayer, Engine.State);
|
||||
int charDelay = Engine.State.Rules.SpeedLevel switch
|
||||
{
|
||||
-5 => 55,
|
||||
-4 => 45,
|
||||
-3 => 38,
|
||||
-2 => 30,
|
||||
-1 => 24,
|
||||
0 => 18,
|
||||
1 => 14,
|
||||
2 => 10,
|
||||
3 => 7,
|
||||
4 => 4,
|
||||
5 => 2,
|
||||
_ => 18
|
||||
};
|
||||
|
||||
// Postupné písanie myšlienky po písmenkách (Typewriter effect)
|
||||
for (int i = 1; i <= thought.Length; i++)
|
||||
{
|
||||
if (token.IsCancellationRequested || Engine == null) return;
|
||||
string partial = thought.Substring(0, i) + (i < thought.Length ? " ▌" : "");
|
||||
await InvokeAsync(() => SetThoughtBubble(aiPlayer, partial, 5000));
|
||||
await Task.Delay(charDelay, token);
|
||||
}
|
||||
|
||||
// Dokončený text a krátka prirodzená pauza na prečítanie
|
||||
await InvokeAsync(() => SetThoughtBubble(aiPlayer, thought, 4000));
|
||||
int readPause = Engine.State.Rules.SpeedLevel switch
|
||||
{
|
||||
-5 => 1200,
|
||||
-4 => 1000,
|
||||
-3 => 800,
|
||||
-2 => 650,
|
||||
-1 => 500,
|
||||
0 => 400,
|
||||
1 => 300,
|
||||
2 => 220,
|
||||
3 => 150,
|
||||
4 => 90,
|
||||
5 => 40,
|
||||
_ => 400
|
||||
};
|
||||
await Task.Delay(readPause, token);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(Engine.State.Rules.AIDelayMs, token);
|
||||
}
|
||||
|
||||
if (token.IsCancellationRequested || Engine == null) return;
|
||||
|
||||
var decision = AI.MakeDecision(Engine.State, aiPlayer);
|
||||
|
||||
await InvokeAsync(async () =>
|
||||
@@ -719,6 +858,12 @@ else
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Engine.State.Rules.AIHumanMode && Engine.State.PendingDrawCount == 0 && !Engine.State.PendingSkip)
|
||||
{
|
||||
var drawThought = AIDialogueService.GetMustDrawDialogue(aiPlayer);
|
||||
SetThoughtBubble(aiPlayer, drawThought, 1800);
|
||||
}
|
||||
|
||||
if (Engine.State.PendingDrawCount > 0)
|
||||
{
|
||||
int drawCount = Engine.State.PendingDrawCount;
|
||||
@@ -752,6 +897,71 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private class PlayerThoughtInfo
|
||||
{
|
||||
public string Text { get; set; } = "";
|
||||
public DateTime FadeOutAt { get; set; }
|
||||
public DateTime Expiry { get; set; }
|
||||
public bool IsFadingOut => DateTime.UtcNow >= FadeOutAt && DateTime.UtcNow <= Expiry;
|
||||
public bool IsExpired => DateTime.UtcNow > Expiry;
|
||||
}
|
||||
|
||||
private readonly Dictionary<Player, PlayerThoughtInfo> PlayerThoughts = new();
|
||||
|
||||
private PlayerThoughtInfo? GetThoughtState(Player? player)
|
||||
{
|
||||
if (player == null || Engine == null || !Engine.State.Rules.AIHumanMode) return null;
|
||||
if (PlayerThoughts.TryGetValue(player, out var info))
|
||||
{
|
||||
if (!info.IsExpired)
|
||||
{
|
||||
return info;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void SetThoughtBubble(Player player, string text, int durationMs = 3200, int fadeDurationMs = 600)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
var now = DateTime.UtcNow;
|
||||
var fadeAt = now.AddMilliseconds(Math.Max(200, durationMs - fadeDurationMs));
|
||||
var expireAt = now.AddMilliseconds(durationMs);
|
||||
|
||||
var info = new PlayerThoughtInfo
|
||||
{
|
||||
Text = text,
|
||||
FadeOutAt = fadeAt,
|
||||
Expiry = expireAt
|
||||
};
|
||||
PlayerThoughts[player] = info;
|
||||
StateHasChanged();
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
int delayToFade = (int)(fadeAt - DateTime.UtcNow).TotalMilliseconds;
|
||||
if (delayToFade > 0)
|
||||
{
|
||||
await Task.Delay(delayToFade);
|
||||
if (PlayerThoughts.TryGetValue(player, out var cur) && cur == info)
|
||||
{
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
int delayToExpire = (int)(expireAt - DateTime.UtcNow).TotalMilliseconds;
|
||||
if (delayToExpire > 0)
|
||||
{
|
||||
await Task.Delay(delayToExpire);
|
||||
if (PlayerThoughts.TryGetValue(player, out var cur) && cur == info)
|
||||
{
|
||||
PlayerThoughts.Remove(player);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private IEnumerable<(Player Player, string PositionClass)> GetOpponents()
|
||||
{
|
||||
if (Engine == null) return Enumerable.Empty<(Player, string)>();
|
||||
@@ -778,6 +988,65 @@ else
|
||||
return result;
|
||||
}
|
||||
|
||||
private class ActiveJokeInfo
|
||||
{
|
||||
public AIDialogueService.CardJoke Joke { get; set; } = default!;
|
||||
public Player Storyteller { get; set; } = default!;
|
||||
public DateTime FadeOutAt { get; set; }
|
||||
public DateTime Expiry { get; set; }
|
||||
public bool IsFadingOut => DateTime.UtcNow >= FadeOutAt && DateTime.UtcNow <= Expiry;
|
||||
public bool IsExpired => DateTime.UtcNow > Expiry;
|
||||
}
|
||||
|
||||
private ActiveJokeInfo? CurrentJoke;
|
||||
|
||||
private void TriggerCardJoke(AIDialogueService.CardJoke joke, Player storyteller, int durationMs = 7000, int fadeDurationMs = 700)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var fadeAt = now.AddMilliseconds(Math.Max(200, durationMs - fadeDurationMs));
|
||||
var expireAt = now.AddMilliseconds(durationMs);
|
||||
|
||||
var info = new ActiveJokeInfo
|
||||
{
|
||||
Joke = joke,
|
||||
Storyteller = storyteller,
|
||||
FadeOutAt = fadeAt,
|
||||
Expiry = expireAt
|
||||
};
|
||||
CurrentJoke = info;
|
||||
StateHasChanged();
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
int delayToFade = (int)(fadeAt - DateTime.UtcNow).TotalMilliseconds;
|
||||
if (delayToFade > 0)
|
||||
{
|
||||
await Task.Delay(delayToFade);
|
||||
if (CurrentJoke == info)
|
||||
{
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
int delayToExpire = (int)(expireAt - DateTime.UtcNow).TotalMilliseconds;
|
||||
if (delayToExpire > 0)
|
||||
{
|
||||
await Task.Delay(delayToExpire);
|
||||
if (CurrentJoke == info)
|
||||
{
|
||||
CurrentJoke = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void DismissJoke()
|
||||
{
|
||||
CurrentJoke = null;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task ToggleMute()
|
||||
{
|
||||
IsMuted = await Fx.ToggleMuteAsync();
|
||||
@@ -785,12 +1054,16 @@ else
|
||||
|
||||
private void HandlePlayAgain()
|
||||
{
|
||||
PlayerThoughts.Clear();
|
||||
CurrentJoke = null;
|
||||
StartNewGame();
|
||||
}
|
||||
|
||||
private void ReturnToLobby()
|
||||
{
|
||||
_aiCts?.Cancel();
|
||||
PlayerThoughts.Clear();
|
||||
CurrentJoke = null;
|
||||
IsInLobby = true;
|
||||
Engine = null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user