AI human mode

This commit is contained in:
Michal Vrba
2026-09-03 21:11:25 +02:00
parent 035da18584
commit 9005b4b9be
8 changed files with 1220 additions and 247 deletions
+100 -95
View File
@@ -56,11 +56,30 @@
}
</div>
<!-- Nastavenia pravidiel -->
<div style="font-weight: 700; margin-bottom: 8px; color: var(--gold); font-size: 0.95rem;">
PRAVIDLÁ A NASTAVENIA:
<!-- Nastavenia pravidiel (Collapsible - by default schované) -->
<div style="margin-bottom: 18px;">
<div class="lobby-accordion-header @(ShowRules ? "open" : "")" @onclick="() => ShowRules = !ShowRules">
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
<span style="font-size: 1.15rem;">⚙️</span>
<span style="font-weight: 800; color: var(--gold); font-size: 0.95rem; letter-spacing: 0.5px;">
PRAVIDLÁ A NASTAVENIA HRY
</span>
@if (!ShowRules)
{
<span class="badge-rules-summary">
@(Rules.InitialCardCount) kariet • Sedmy (+3) • @(Rules.SpeedLevel)x Rýchlosť
</span>
}
</div>
<div class="rules-toggle-grid">
<div class="accordion-toggle-btn">
<span>@(ShowRules ? "Zbaliť" : "Rozbaliť")</span>
<span class="accordion-arrow">@(ShowRules ? "▲" : "▼")</span>
</div>
</div>
@if (ShowRules)
{
<div class="rules-toggle-grid" style="margin-top: 10px; animation: fadeIn 0.25s ease;">
<label class="rule-check-item">
<input type="checkbox" @bind="Rules.StackSevens" />
<span>Kumulácia sediem (+3, +6, +9...)</span>
@@ -106,6 +125,11 @@
<span style="font-weight: 800; color: var(--gold);">🏅 Olympijský Faraón (Turnajový režim)</span>
<span style="font-size: 0.8rem; color: #94a3b8; margin-left: 6px;">— porazený má v ďalšom kole o 1 kartu menej</span>
</label>
<label class="rule-check-item" style="grid-column: 1 / -1; background: rgba(56, 189, 248, 0.12); padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(56, 189, 248, 0.35);">
<input type="checkbox" @bind="Rules.AIHumanMode" />
<span style="font-weight: 800; color: #38bdf8;">🤖💭 AI Human Mode (Premýšľacie bubliny & hlášky)</span>
<span style="font-size: 0.8rem; color: #94a3b8; margin-left: 6px;">— boti hovoria svoje myšlienky, blufujú a reagujú na hru</span>
</label>
<div style="display: flex; align-items: center; gap: 8px; font-size: 0.9rem;">
<span>Počiatočný počet kariet:</span>
<select class="player-setup-select" style="width: auto; padding: 2px 8px;" @bind="Rules.InitialCardCount">
@@ -117,117 +141,75 @@
<div style="display: flex; align-items: center; gap: 8px; font-size: 0.9rem;">
<span>Rýchlosť hry:</span>
<select class="player-setup-select" style="width: auto; padding: 2px 8px;" @bind="Rules.SpeedLevel">
<option value="1">🐢 0.5x (Pomalá)</option>
<option value="2">🚶 0.75x (Pokojná)</option>
<option value="3">⚡ 1x (Normálna - štandard)</option>
<option value="4">🔥 1.5x (Rýchla)</option>
<option value="5">🚀 2.5x (Turbo / Blesková)</option>
<option value="-5">🐢 -5 (0.2x Zen / Extra pomalá)</option>
<option value="-4">🐢 -4 (0.35x Veľmi pomalá)</option>
<option value="-3">🚶 -3 (0.5x Pomalá)</option>
<option value="-2">🚶 -2 (0.65x Pokojná)</option>
<option value="-1">🍃 -1 (0.8x Zvoľnená)</option>
<option value="0">⚡ 0 (1x Normálna / Vyvážená)</option>
<option value="1">🔥 +1 (1.25x Svižná)</option>
<option value="2">🔥 +2 (1.5x Rýchla)</option>
<option value="3">🚀 +3 (2x Veľmi rýchla)</option>
<option value="4">🚀 +4 (2.75x Turbo)</option>
<option value="5">⚡ +5 (4x Blesková)</option>
</select>
</div>
</div>
}
</div>
<!-- Výber balíčka kariet (Deck Theme) -->
<!-- Výber balíčka kariet (Collapsible - by default iba zvolený deck) -->
<div style="margin-bottom: 24px;">
<h3 style="font-size: 1.05rem; color: var(--gold); margin-bottom: 12px; text-transform: uppercase; letter-spacing: 1px;">
🎨 Vizuálny štýl balíčka kariet
</h3>
<div class="deck-selector-grid">
<div class="deck-option-card @(Rules.SelectedDeck == "original" ? "selected-deck" : "")"
@onclick='() => Rules.SelectedDeck = "original"'>
<div class="deck-preview-img-box">
<img src="assets/cards/original/Hearts_Seven.webp" class="deck-preview-thumb" alt="Original Mariáš" />
<img src="assets/cards/original/Card_Back.webp" class="deck-preview-thumb-back" alt="Rub" />
<div class="lobby-accordion-header @(ShowDeckSelector ? "open" : "")" @onclick="() => ShowDeckSelector = !ShowDeckSelector">
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
<span style="font-size: 1.15rem;">🎨</span>
<span style="font-weight: 800; color: var(--gold); font-size: 0.95rem; letter-spacing: 0.5px;">
VIZUÁLNY ŠTÝL BALÍČKA
</span>
</div>
<div class="deck-info">
<div class="deck-name">Tradičný Mariáš 🎴</div>
<div class="deck-desc">Autentický dvojhlavý Tell vzor (Piatnik)</div>
<div class="accordion-toggle-btn">
<span>@(ShowDeckSelector ? "Zavrieť galériu" : "Zmeniť balíček")</span>
<span class="accordion-arrow">@(ShowDeckSelector ? "▲" : "▼")</span>
</div>
</div>
<div class="deck-option-card @(Rules.SelectedDeck == "janosik" ? "selected-deck" : "")"
@onclick='() => Rules.SelectedDeck = "janosik"'>
<!-- Aktuálne zvolený balíček (Default náhľad po kliknutí rozbalí výber) -->
<div class="selected-deck-summary-card" @onclick="() => ShowDeckSelector = !ShowDeckSelector" title="Klikni pre výber a zmenu balíčka kariet">
<div class="deck-preview-img-box">
<img src="assets/cards/janosik/Hearts_King.webp" class="deck-preview-thumb" alt="Zbojníci" />
<img src="assets/cards/janosik/Card_Back.webp" class="deck-preview-thumb-back" alt="Rub" />
<img src="@CurrentDeck.Thumb" class="deck-preview-thumb" alt="@CurrentDeck.Name" />
<img src="@CurrentDeck.Back" class="deck-preview-thumb-back" alt="Rub" />
</div>
<div class="deck-info">
<div class="deck-name">Zbojníci & Legendy 🪓</div>
<div class="deck-desc">Jánošík, Uhorčík, výšivky a tatranský folklór</div>
<div class="deck-info" style="flex: 1;">
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
<span class="deck-name" style="color: var(--gold); font-size: 0.98rem;">@CurrentDeck.Name</span>
<span class="badge-active-deck">Vybraný</span>
</div>
<div class="deck-desc">@CurrentDeck.Desc</div>
</div>
<div class="btn-deck-change-action">
@(ShowDeckSelector ? "▲ Skryť" : "🎨 Zmeniť štýl ▾")
</div>
</div>
<div class="deck-option-card @(Rules.SelectedDeck == "spongebob" ? "selected-deck" : "")"
@onclick='() => Rules.SelectedDeck = "spongebob"'>
@if (ShowDeckSelector)
{
<div class="deck-selector-grid" style="margin-top: 12px; animation: fadeIn 0.25s ease;">
@foreach (var d in AvailableDecks)
{
<div class="deck-option-card @(Rules.SelectedDeck == d.Id ? "selected-deck" : "")"
@onclick="() => SelectDeck(d.Id)">
<div class="deck-preview-img-box">
<img src="assets/cards/spongebob/Hearts_OverKnave.webp" class="deck-preview-thumb" alt="SpongeBob Cartoon" />
<img src="assets/cards/spongebob/Card_Back.webp" class="deck-preview-thumb-back" alt="Rub" />
<img src="@d.Thumb" class="deck-preview-thumb" alt="@d.Name" />
<img src="@d.Back" class="deck-preview-thumb-back" alt="Rub" />
</div>
<div class="deck-info">
<div class="deck-name">SpongeBob Cartoon 🧽</div>
<div class="deck-desc">Bikini Bottom edícia (postavičky a veľké symboly)</div>
</div>
</div>
<div class="deck-option-card @(Rules.SelectedDeck == "cyberpunk" ? "selected-deck" : "")"
@onclick='() => Rules.SelectedDeck = "cyberpunk"'>
<div class="deck-preview-img-box">
<img src="assets/cards/cyberpunk/Leaves_King.webp" class="deck-preview-thumb" alt="Cyberpunk 2077" />
<img src="assets/cards/cyberpunk/Card_Back.webp" class="deck-preview-thumb-back" alt="Rub" />
</div>
<div class="deck-info">
<div class="deck-name">Cyberpunk 2077 ⚡</div>
<div class="deck-desc">Neónový holografický štýl a kybernetickí hrdinovia</div>
</div>
</div>
<div class="deck-option-card @(Rules.SelectedDeck == "fantasy" ? "selected-deck" : "")"
@onclick='() => Rules.SelectedDeck = "fantasy"'>
<div class="deck-preview-img-box">
<img src="assets/cards/fantasy/Acorns_OverKnave.webp" class="deck-preview-thumb" alt="Fantasy RPG" />
<img src="assets/cards/fantasy/Card_Back.webp" class="deck-preview-thumb-back" alt="Rub" />
</div>
<div class="deck-info">
<div class="deck-name">Fantasy RPG 🐉</div>
<div class="deck-desc">Dračí lordi, mágovia, runy a rytieri</div>
</div>
</div>
<div class="deck-option-card @(Rules.SelectedDeck == "egypt" ? "selected-deck" : "")"
@onclick='() => Rules.SelectedDeck = "egypt"'>
<div class="deck-preview-img-box">
<img src="assets/cards/egypt/Bells_King.webp" class="deck-preview-thumb" alt="Staroveký Egypt" />
<img src="assets/cards/egypt/Card_Back.webp" class="deck-preview-thumb-back" alt="Rub" />
</div>
<div class="deck-info">
<div class="deck-name">Staroveký Egypt ⚔️</div>
<div class="deck-desc">Zlatý papyrus, Tutanchamón, Anubis a pyramídy</div>
</div>
</div>
<div class="deck-option-card @(Rules.SelectedDeck == "pets" ? "selected-deck" : "")"
@onclick='() => Rules.SelectedDeck = "pets"'>
<div class="deck-preview-img-box">
<img src="assets/cards/pets/Hearts_OverKnave.webp" class="deck-preview-thumb" alt="Kráľovské zvieratká" />
<img src="assets/cards/pets/Card_Back.webp" class="deck-preview-thumb-back" alt="Rub" />
</div>
<div class="deck-info">
<div class="deck-name">Kráľovské Zvieratká 🐱</div>
<div class="deck-desc">Mačičky a psíkovia v kráľovských korunkách</div>
</div>
</div>
<div class="deck-option-card @(Rules.SelectedDeck == "pixel" ? "selected-deck" : "")"
@onclick='() => Rules.SelectedDeck = "pixel"'>
<div class="deck-preview-img-box">
<img src="assets/cards/pixel/Leaves_Seven.webp" class="deck-preview-thumb" alt="Retro Pixel" />
<img src="assets/cards/pixel/Card_Back.webp" class="deck-preview-thumb-back" alt="Rub" />
</div>
<div class="deck-info">
<div class="deck-name">Retro 8-Bit Pixel 🕹️</div>
<div class="deck-desc">Nostalgický arkádový štýl klasických hier</div>
<div class="deck-name">@d.Name</div>
<div class="deck-desc">@d.Desc</div>
</div>
</div>
}
</div>
}
</div>
<!-- Štart tlačidlo -->
@@ -244,6 +226,29 @@
private int SelectedPlayerCount = 4;
private RuleOptions Rules = new();
private bool ShowRules = false;
private bool ShowDeckSelector = false;
private record DeckInfo(string Id, string Name, string Desc, string Thumb, string Back);
private readonly DeckInfo[] AvailableDecks = new[]
{
new DeckInfo("original", "Tradičný Mariáš 🎴", "Autentický dvojhlavý Tell vzor (Piatnik)", "assets/cards/original/Hearts_Seven.webp", "assets/cards/original/Card_Back.webp"),
new DeckInfo("janosik", "Zbojníci & Legendy 🪓", "Jánošík, Uhorčík, výšivky a tatranský folklór", "assets/cards/janosik/Hearts_King.webp", "assets/cards/janosik/Card_Back.webp"),
new DeckInfo("spongebob", "SpongeBob Cartoon 🧽", "Bikini Bottom edícia (postavičky a veľké symboly)", "assets/cards/spongebob/Hearts_OverKnave.webp", "assets/cards/spongebob/Card_Back.webp"),
new DeckInfo("cyberpunk", "Cyberpunk 2077 ⚡", "Neónový holografický štýl a kybernetickí hrdinovia", "assets/cards/cyberpunk/Leaves_King.webp", "assets/cards/cyberpunk/Card_Back.webp"),
new DeckInfo("fantasy", "Fantasy RPG 🐉", "Dračí lordi, mágovia, runy a rytieri", "assets/cards/fantasy/Acorns_OverKnave.webp", "assets/cards/fantasy/Card_Back.webp"),
new DeckInfo("egypt", "Staroveký Egypt ⚔️", "Zlatý papyrus, Tutanchamón, Anubis a pyramídy", "assets/cards/egypt/Bells_King.webp", "assets/cards/egypt/Card_Back.webp"),
new DeckInfo("pets", "Kráľovské Zvieratká 🐱", "Mačičky a psíkovia v kráľovských korunkách", "assets/cards/pets/Hearts_OverKnave.webp", "assets/cards/pets/Card_Back.webp"),
new DeckInfo("pixel", "Retro 8-Bit Pixel 🕹️", "Nostalgický arkádový štýl klasických hier", "assets/cards/pixel/Leaves_Seven.webp", "assets/cards/pixel/Card_Back.webp")
};
private DeckInfo CurrentDeck => AvailableDecks.FirstOrDefault(d => d.Id == Rules.SelectedDeck) ?? AvailableDecks[0];
private void SelectDeck(string deckId)
{
Rules.SelectedDeck = deckId;
}
private readonly string[] Avatars = ["👑", "🧙‍♂️", "🦊", "🦁", "🐯", "🤖", "🐉", "🥷", "👸", "🤠", "🦅", "🐺"];
+10
View File
@@ -75,6 +75,14 @@
</span>
</div>
}
@if (!string.IsNullOrEmpty(ThoughtBubble))
{
<div class="ai-thought-bubble @PositionClass @(IsThoughtFadingOut ? "fading-out" : "")">
<span class="thought-bubble-prefix">💭</span>
<span class="thought-bubble-text">@ThoughtBubble</span>
</div>
}
</div>
<!-- Mini karty pre vizuálny odpočet kariet v ruke -->
@@ -96,6 +104,8 @@
[Parameter] public bool CanBeRevived { get; set; } = false;
[Parameter] public int ReviveRoundsRemaining { get; set; } = 0;
[Parameter] public int ReviveTurnsRemaining { get; set; } = 0;
[Parameter] public string? ThoughtBubble { get; set; }
[Parameter] public bool IsThoughtFadingOut { get; set; } = false;
[Parameter] public bool IsOlympicMode { get; set; } = false;
[Parameter] public string PositionClass { get; set; } = "slot-top";
}
+310 -37
View File
@@ -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 &bull; 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;
}
+370 -32
View File
@@ -310,43 +310,45 @@ body, html {
display: flex;
flex-direction: column;
align-items: center;
background: rgba(10, 25, 18, 0.85);
border: 2px solid rgba(255, 255, 255, 0.15);
border-radius: 16px;
padding: 6px 12px;
backdrop-filter: blur(8px);
background: rgba(10, 25, 18, 0.88);
border: 2.5px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
padding: 10px 18px;
min-width: 110px;
backdrop-filter: blur(10px);
transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
}
.avatar-badge.active-turn {
border-color: var(--gold);
box-shadow: 0 0 25px rgba(245, 176, 65, 0.9), inset 0 0 12px rgba(245, 176, 65, 0.35);
transform: scale(1.09);
box-shadow: 0 0 30px rgba(245, 176, 65, 0.95), inset 0 0 16px rgba(245, 176, 65, 0.4);
transform: scale(1.12);
animation: turn-pulse 1.8s infinite;
}
@keyframes turn-pulse {
0%, 100% { box-shadow: 0 0 16px rgba(245, 176, 65, 0.7); }
50% { box-shadow: 0 0 30px rgba(245, 176, 65, 1), 0 0 45px rgba(245, 176, 65, 0.55); }
0%, 100% { box-shadow: 0 0 20px rgba(245, 176, 65, 0.75); }
50% { box-shadow: 0 0 38px rgba(245, 176, 65, 1), 0 0 55px rgba(245, 176, 65, 0.6); }
}
/* TURN INDICATOR BADGE (NA ŤAHU) */
.turn-indicator-badge {
position: absolute;
top: -14px;
top: -16px;
left: 50%;
transform: translateX(-50%);
display: inline-flex;
align-items: center;
gap: 5px;
gap: 6px;
background: linear-gradient(135deg, #ffd700, #f5b041);
color: #11281d;
font-size: 0.66rem;
font-size: 0.74rem;
font-weight: 900;
letter-spacing: 0.8px;
padding: 2px 9px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.6), 0 0 14px rgba(255, 215, 0, 0.8);
letter-spacing: 0.9px;
padding: 3px 12px;
border-radius: 14px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.6), 0 0 16px rgba(255, 215, 0, 0.85);
border: 1.5px solid #ffffff;
white-space: nowrap;
z-index: 20;
@@ -404,25 +406,28 @@ body, html {
}
.avatar-icon {
font-size: 1.8rem;
font-size: 2.5rem;
line-height: 1;
}
.avatar-name {
font-size: 0.85rem;
font-weight: 700;
margin-top: 2px;
font-size: 1.05rem;
font-weight: 800;
margin-top: 4px;
white-space: nowrap;
color: #ffffff;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.7);
}
.avatar-card-count {
font-size: 0.75rem;
background: rgba(245, 176, 65, 0.2);
font-size: 0.86rem;
background: rgba(245, 176, 65, 0.22);
color: var(--gold);
padding: 2px 8px;
border-radius: 10px;
margin-top: 2px;
font-weight: 700;
padding: 3px 12px;
border-radius: 12px;
margin-top: 4px;
font-weight: 800;
border: 1px solid rgba(245, 176, 65, 0.35);
}
/* FROZEN / ICE STATUS STYLES */
@@ -671,18 +676,18 @@ body, html {
/* OPPONENT MINI CARDS */
.opponent-mini-cards {
display: flex;
margin-top: 6px;
margin-top: 8px;
justify-content: center;
}
.mini-card-back {
width: 22px;
height: 32px;
width: 26px;
height: 38px;
background: linear-gradient(135deg, #8b1e1e, #4a0d0d);
border: 1px solid #ffd700;
border-radius: 3px;
margin-left: -12px;
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
border-radius: 4px;
margin-left: -14px;
box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.6);
transition: transform 0.2s ease;
}
@@ -1779,6 +1784,239 @@ body, html {
overflow: hidden;
}
/* LOBBY COLLAPSIBLE SECTIONS */
.lobby-accordion-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
background: rgba(0, 0, 0, 0.45);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 12px;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
user-select: none;
}
.lobby-accordion-header:hover {
background: rgba(245, 176, 65, 0.12);
border-color: rgba(245, 176, 65, 0.4);
}
.lobby-accordion-header.open {
border-color: rgba(245, 176, 65, 0.55);
background: rgba(245, 176, 65, 0.15);
}
.badge-rules-summary {
font-size: 0.72rem;
color: #cbd5e1;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
padding: 2px 8px;
border-radius: 10px;
font-weight: 600;
}
.accordion-toggle-btn {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.78rem;
font-weight: 800;
color: var(--gold);
background: rgba(245, 176, 65, 0.12);
padding: 4px 10px;
border-radius: 8px;
border: 1px solid rgba(245, 176, 65, 0.3);
transition: all 0.2s ease;
}
.lobby-accordion-header:hover .accordion-toggle-btn {
background: rgba(245, 176, 65, 0.25);
border-color: var(--gold);
}
.accordion-arrow {
font-size: 0.7rem;
}
/* SELECTED DECK PREVIEW CARD */
.selected-deck-summary-card {
display: flex;
align-items: center;
gap: 14px;
background: linear-gradient(135deg, rgba(245, 176, 65, 0.14) 0%, rgba(0, 0, 0, 0.6) 100%);
border: 2px solid rgba(245, 176, 65, 0.5);
border-radius: 14px;
padding: 10px 14px;
cursor: pointer;
transition: all 0.22s cubic-bezier(0.175, 0.885, 0.32, 1.275);
margin-top: 8px;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4);
}
.selected-deck-summary-card:hover {
transform: translateY(-2px);
border-color: var(--gold);
box-shadow: 0 6px 20px rgba(245, 176, 65, 0.3);
background: linear-gradient(135deg, rgba(245, 176, 65, 0.22) 0%, rgba(0, 0, 0, 0.7) 100%);
}
.badge-active-deck {
font-size: 0.66rem;
font-weight: 900;
background: var(--gold);
color: #000;
padding: 2px 6px;
border-radius: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-deck-change-action {
font-size: 0.8rem;
font-weight: 800;
color: #fff;
background: rgba(255, 255, 255, 0.1);
padding: 6px 12px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.25);
white-space: nowrap;
transition: all 0.2s ease;
}
.selected-deck-summary-card:hover .btn-deck-change-action {
background: rgba(245, 176, 65, 0.25);
border-color: var(--gold);
color: var(--gold);
}
/* AI THOUGHT BUBBLE */
.ai-thought-bubble {
position: absolute;
background: linear-gradient(135deg, rgba(15, 23, 42, 0.98) 0%, rgba(30, 41, 59, 0.98) 100%);
border: 2px solid #38bdf8;
color: #ffffff;
font-size: 0.98rem;
font-weight: 700;
line-height: 1.4;
padding: 10px 18px;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.85), 0 0 22px rgba(56, 189, 248, 0.5);
z-index: 100;
max-width: 320px;
min-width: 170px;
text-align: center;
pointer-events: none;
animation: thought-pop 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
backdrop-filter: blur(10px);
transition: opacity 0.55s ease, transform 0.55s cubic-bezier(0.4, 0, 0.2, 1), filter 0.55s ease;
opacity: 1;
filter: blur(0);
}
@keyframes thought-pop {
0% { transform: scale(0.7) translateY(8px); opacity: 0; }
100% { transform: scale(1) translateY(0); opacity: 1; }
}
.ai-thought-bubble.fading-out {
opacity: 0 !important;
filter: blur(3px) !important;
pointer-events: none !important;
}
.thought-bubble-prefix {
display: inline-block;
margin-right: 6px;
font-size: 1.15rem;
vertical-align: -1px;
}
.thought-bubble-text {
vertical-align: middle;
color: #ffffff;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
}
.thought-typing-cursor {
display: inline-block;
color: #38bdf8;
font-weight: 900;
margin-left: 2px;
animation: cursor-blink 0.6s infinite alternate;
}
@keyframes cursor-blink {
0% { opacity: 0.15; }
100% { opacity: 1; }
}
.ai-thought-bubble.slot-top {
top: 105%;
left: 50%;
transform: translateX(-50%);
margin-top: 10px;
}
.ai-thought-bubble.slot-top.fading-out {
transform: translateX(-50%) scale(0.9) translateY(-6px);
}
.ai-thought-bubble.slot-left {
top: 50%;
left: 110%;
transform: translateY(-50%);
margin-left: 10px;
}
.ai-thought-bubble.slot-left.fading-out {
transform: translateY(-50%) scale(0.9) translateX(-6px);
}
.ai-thought-bubble.slot-right {
top: 50%;
right: 110%;
transform: translateY(-50%);
margin-right: 10px;
}
.ai-thought-bubble.slot-right.fading-out {
transform: translateY(-50%) scale(0.9) translateX(6px);
}
.ai-thought-bubble.slot-top-left {
top: 105%;
left: 0%;
margin-top: 10px;
}
.ai-thought-bubble.slot-top-left.fading-out {
transform: scale(0.9) translateY(-6px);
}
.ai-thought-bubble.slot-top-right {
top: 105%;
right: 0%;
margin-top: 10px;
}
.ai-thought-bubble.slot-top-right.fading-out {
transform: scale(0.9) translateY(-6px);
}
.ai-thought-bubble.slot-bottom {
bottom: 110%;
left: 50%;
transform: translateX(-50%);
margin-bottom: 10px;
}
.ai-thought-bubble.slot-bottom.fading-out {
transform: translateX(-50%) scale(0.9) translateY(6px);
}
/* Canvas Particle Layer */
#particle-canvas {
position: absolute;
@@ -1906,6 +2144,106 @@ body, html {
100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
}
/* AI SPECIAL CARD JOKE BANNER */
.ai-joke-banner {
position: absolute;
top: 52px;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(135deg, rgba(35, 12, 50, 0.98) 0%, rgba(15, 23, 42, 0.98) 55%, rgba(45, 15, 25, 0.98) 100%);
border: 2px solid #ffd700;
border-radius: 20px;
box-shadow: 0 16px 45px rgba(0, 0, 0, 0.9), 0 0 35px rgba(255, 215, 0, 0.45), inset 0 0 20px rgba(255, 215, 0, 0.18);
padding: 14px 22px;
max-width: 580px;
width: 90%;
z-index: 300;
backdrop-filter: blur(12px);
animation: joke-entrance 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
transition: opacity 0.65s ease, transform 0.65s cubic-bezier(0.4, 0, 0.2, 1), filter 0.65s ease;
color: #ffffff;
opacity: 1;
filter: blur(0);
}
@keyframes joke-entrance {
0% { transform: translateX(-50%) translateY(-30px) scale(0.85); opacity: 0; }
100% { transform: translateX(-50%) translateY(0) scale(1); opacity: 1; }
}
.ai-joke-banner.fading-out {
opacity: 0 !important;
transform: translateX(-50%) translateY(-25px) scale(0.9) !important;
filter: blur(4px) !important;
pointer-events: none !important;
}
.joke-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
border-bottom: 1px solid rgba(255, 215, 0, 0.35);
padding-bottom: 6px;
}
.joke-header-badge {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.85rem;
font-weight: 900;
color: var(--gold);
letter-spacing: 0.8px;
text-transform: uppercase;
}
.joke-storyteller {
display: inline-flex;
align-items: center;
gap: 5px;
background: rgba(245, 176, 65, 0.22);
padding: 2px 10px;
border-radius: 12px;
border: 1px solid rgba(245, 176, 65, 0.45);
color: #fff;
font-size: 0.78rem;
font-weight: 700;
}
.joke-close-btn {
background: none;
border: none;
color: #94a3b8;
font-size: 1.1rem;
cursor: pointer;
line-height: 1;
padding: 2px 6px;
border-radius: 6px;
transition: all 0.2s ease;
}
.joke-close-btn:hover {
color: #ffd700;
background: rgba(255, 255, 255, 0.15);
}
.joke-setup {
font-size: 1.02rem;
font-weight: 600;
line-height: 1.35;
color: #f1f5f9;
margin-bottom: 6px;
}
.joke-punchline {
font-size: 1.1rem;
font-weight: 800;
line-height: 1.35;
color: #ffd700;
text-shadow: 0 0 14px rgba(255, 215, 0, 0.6);
}
/* HOLOGRAPHIC FOIL SHINE ON CARDS */
.game-card.playable::before {
content: '';
+11 -5
View File
@@ -11,11 +11,17 @@ window.FaraonFx = (function () {
function setSpeedLevel(level) {
switch (level) {
case 1: speedMultiplier = 0.7; break; // 0.5x
case 2: speedMultiplier = 0.85; break; // 0.75x
case 4: speedMultiplier = 1.5; break; // 1.5x
case 5: speedMultiplier = 2.4; break; // 2.5x Turbo
case 3:
case -5: speedMultiplier = 0.35; break; // 0.2x Zen
case -4: speedMultiplier = 0.45; break; // 0.35x Veľmi pomalá
case -3: speedMultiplier = 0.60; break; // 0.5x Pomalá
case -2: speedMultiplier = 0.75; break; // 0.65x Pokojná
case -1: speedMultiplier = 0.88; break; // 0.8x Zvoľnená
case 0: speedMultiplier = 1.0; break; // 1x Normálna
case 1: speedMultiplier = 1.25; break; // 1.25x Svižná
case 2: speedMultiplier = 1.55; break; // 1.5x Rýchla
case 3: speedMultiplier = 2.0; break; // 2x Veľmi rýchla
case 4: speedMultiplier = 2.75; break; // 2.75x Turbo
case 5: speedMultiplier = 3.8; break; // 4x Blesková
default: speedMultiplier = 1.0; break;
}
}
+284
View File
@@ -0,0 +1,284 @@
namespace Faraon.Core.AI;
using System;
using System.Collections.Generic;
using Faraon.Core.Models;
public static class AIDialogueService
{
private static readonly Random _rng = new();
// --- EASY (Začiatočník - prezrádza taktiku a neistotu) ---
private static readonly string[] EasyThinking =
[
"Mám ešte jedno eso, len neviem či ho dať teraz...",
"Keby mi radšej prišla zeleň, to by bolo super.",
"Dúfam, že po mne nikto nehodí sedmu!",
"Mám len samé vysoké karty, čo s tým...",
"Asi ti zmením farbu, táto sa mi veľmi nepáči.",
"Ktorú kartu dať skôr? Táto vyzerá celkom pekne.",
"Nemám veľa možností, toto snáď vyjde.",
"Mám tu jedného horníka, ale bojím sa ho použiť.",
"Ešteže mám túto záchrannú kartu...",
"Snáď nikto nezbadá, že mi ostala už len jedna karta!",
"Hm... pravidlá hovoria, že toto môžem zahrať, nie?",
"Tento ťah snáď nebude úplná katastrofa.",
"Čo ak zahrám toto a súper ma zničí?",
"Uff, dúfam, že nemáš sedmu na ruke."
];
// --- MEDIUM (Taktik - realistické premýšľanie) ---
private static readonly string[] MediumThinking =
[
"Skúsim ti podržať farbu, nech nemôžeš vyložiť.",
"Túto sedmu si radšej pošetrím na neskôr.",
"Pozrime sa, čo s týmto urobíš.",
"Ak teraz zahrám túto kartu, ďalší ťah mám voľný.",
"Idem po istote, netreba zbytočne riskovať.",
"Vidím, že zbieraš túto farbu, tak ti ju zmením.",
"Rýchle combo by ťa teraz mohlo prekvapiť.",
"Mením taktiku na rýchle vyprázdnenie ruky.",
"Kľúčové bude neprepustiť ťah bez tlaku.",
"Opatrný ťah, ešte nie je čas na otvorené finále.",
"Počítam karty, toto by malo bezpečne prejsť.",
"Skúsim ťa prinútiť potiahnuť z balíčka."
];
// --- HARD (Veľmajster - psychologické blufovanie a prevaha) ---
private static readonly string[] HardThinking =
[
"Presne do tejto pasce som ťa potreboval dostať.",
"Mám v ruke presne to, čo ťa bude bolieť.",
"Matematicky ti zostávajú maximálne dva ťahy.",
"Risk je zisk. Priprav sa na prekvapenie.",
"Túto hru mám už prečítanú od prvého kola.",
"Si si naozaj istý, že toto bol tvoj najlepší ťah?",
"Môj ďalší krok rozhodne o celom zápase.",
"Viem presne, aké karty ti ostali v ruke.",
"Pripravujem šach-mat, sleduj pozorne.",
"Všetko ide presne podľa môjho majstrovského plánu.",
"Tvoj osud je spečatený, žiadna záchrana nepríde.",
"Geniálny ťah si žiada trochu trpezlivosti."
];
// --- POSLEDNÁ KARTA (1 karta) ---
private static readonly string[] LastCardThoughts =
[
"Už mi chýba len jedna jediná karta!",
"Cítim vôňu víťazstva... už len krok!",
"Dúfam, že ma nikto nezablokuje v poslednej chvíli...",
"Kto ma teraz nezastaví, ten prehral zápas!",
"Už len jeden ťah a titul je môj!"
];
// --- ČELENIE SEDME (Facing Seven) ---
private static readonly string[] FacingSevenThoughts =
[
"Sedma?! Pozrime sa, či mám odpoveď...",
"Dúfam, že mám na ruke ďalšiu sedmu!",
"Toto bude bolieť, ak nemám obranu...",
"Ak mám Zeleného dolníka, tak sa vám vysmejem!"
];
// --- ČELENIE ESU (Facing Ace) ---
private static readonly string[] FacingAceThoughts =
[
"Eso? Skúsim to prebiť vlastným esom!",
"Zamrznúť teraz by bola veľká škoda...",
"Hľadám v ruke eso na prebitie..."
];
// --- BEŽNÉ LÍZANIE (Musí ťahať z balíčka) ---
private static readonly string[] MustDrawThoughts =
[
"Nič mi nesedí, musím líznuť z balíčka...",
"Snáď mi príde niečo poriadne...",
"Balíček, nesklam ma v tejto chvíli!",
"Škoda, tento ťah musím potiahnuť kartu."
];
// --- REAKCIE NA UDALOSTI ---
private static readonly string[] FreezeReactions =
[
"Brrr, to je ale poriadna zima!",
"Zamrzol som na jedno celé kolo...",
"Kto tu pustil túto sibírsku fujavicu?!",
"Počkaj, hneď ako rozmrznem, ti to vrátim!",
"Stojím, ale môj čas ešte len príde.",
"Mráz mi nevadí, aspoň lepšie premyslím stratégiu."
];
private static readonly string[] AceCounterReactions =
[
"Žiadne státie! Moje eso okamžite rozpúšťa ľad!",
"Na mňa si s mrazom neprídeš!",
"Vraciam ti to aj s úrokmi, stojíš ty!"
];
private static readonly string[] SmallPenaltyDrawReactions =
[
"Ejha, +3 karty? To ešte hravo zvládnem.",
"No pekne, sedma ma neobišla...",
"Dobre, beriem 3, ale vrátim vám ich dvojnásobne!"
];
private static readonly string[] BigPenaltyDrawReactions =
[
"Ejha, toľko kariet mi do ruky ani nevojde!",
"To je nejaká organizovaná pomsta?!",
"Kde mám teraz zohnať miesto na ruke pre toľko kariet?!",
"No výborne, zachvíľu budem mať celý balíček...",
"Tento masívny trest vám v ďalšom kole nezabudnem!"
];
private static readonly string[] SevenCanceledReactions =
[
"Haha, Zelený dolník vás zachránil!",
"Štít aktivovaný, dnes žiadne ťahanie sediem!",
"Itel Reding hovorí jasné: Dnes nie!",
"Sedmy vymazané na nulu, ideme ďalej!"
];
private static readonly string[] RevivedTargetReactions =
[
"Znova žijem! Pozor na mňa!",
"Červená sedma robí zázraky! Som späť v hre!",
"Ešte sme neskončili, návrat šampióna!"
];
private static readonly string[] RevivedSourceReactions =
[
"Nikam neodchádzaš, ešte si s tebou zahrám!",
"Vraciam ťa späť do hry, zábava pokračuje!",
"Predčasná radosť, poď pekne naspäť k stolu!"
];
private static readonly string[] SuitChangeReactions =
[
"Prepínam hru na moju obľúbenú farbu!",
"Uvidíme, či máte na túto farbu odpoveď...",
"Trochu to tu zamiešame a zmeníme smer!"
];
private static readonly string[] BurnReactions =
[
"BUM! Všetko zhorelo na prach!",
"Spálené! A ťahám znova!",
"Dokonalé 4 karty, stôl je čistý!"
];
public static string GetThinkingDialogue(Player player, GameState state)
{
if (player.Hand.Count == 1)
{
return PickRandom(LastCardThoughts);
}
if (state.PendingDrawCount > 0)
{
return PickRandom(FacingSevenThoughts);
}
if (state.PendingSkip)
{
return PickRandom(FacingAceThoughts);
}
return player.Difficulty switch
{
AIDifficulty.Easy => PickRandom(EasyThinking),
AIDifficulty.Hard => PickRandom(HardThinking),
_ => PickRandom(MediumThinking)
};
}
public static string GetMustDrawDialogue(Player player)
{
return PickRandom(MustDrawThoughts);
}
public static string GetEventReaction(GameEvent e, Player player)
{
return e.Type switch
{
GameEventType.AceSkipped when e.SourcePlayer == player => PickRandom(FreezeReactions),
GameEventType.AceCountered when e.SourcePlayer == player => PickRandom(AceCounterReactions),
GameEventType.SevenStacked when e.TargetPlayer == player || (e.ExtraData > 0 && e.SourcePlayer != player) =>
e.ExtraData >= 6 ? PickRandom(BigPenaltyDrawReactions) : PickRandom(SmallPenaltyDrawReactions),
GameEventType.SevenCanceled when e.SourcePlayer == player => PickRandom(SevenCanceledReactions),
GameEventType.PlayerRevived when e.TargetPlayer == player => PickRandom(RevivedTargetReactions),
GameEventType.PlayerRevived when e.SourcePlayer == player => PickRandom(RevivedSourceReactions),
GameEventType.SuitChanged when e.SourcePlayer == player => PickRandom(SuitChangeReactions),
GameEventType.CardsBurned when e.SourcePlayer == player => PickRandom(BurnReactions),
_ => ""
};
}
public record CardJoke(string Icon, string Category, string Setup, string Punchline);
// --- VTIPY: MASÍVNY TREST (9 - 12+ kariet) ---
private static readonly CardJoke[] MultiDrawJokes =
[
new("🎒", "KAUZÁLNY BATOH", "Pýta sa Faraón hráča pri stole: 'Prečo si prišiel na zápas s turistickým ruksakom?'", "– 'Aby sa mi tam zmestili všetky tie sedmičky z balíčka!'"),
new("🏢", "VRCHOL OPTIMIZMU", "Viete, čo je vrchol hráčskeho optimizmu?", "– Keď po 12 potiahnutých kartách zahlásiš: 'Počkajte, ešte dve kolá a končím!'"),
new("🏗️", "STAVEBNÝ ZÁMER", "Rozprávajú sa dvaja architekti pri stole:", "– 'Z toľkých kariet v ruke sa už dá postaviť pekný trojizbový byt aj s terasou!'"),
new("🍕", "RODINNÁ PIZZA", "Aký je rozdiel medzi rodinnou pizzou a hráčom s 12 kartami na ruke?", "– Rodinná pizza aspoň niekoho pri stole naozaj poteší!"),
new("📜", "DAŇ Z NEHNUTEĽNOSTI", "Dôležitý oznam od správcu turnaja:", "– Hráč s viac ako 10 kartami začína platiť daň za zastavanú plochu stola!")
];
// --- VTIPY: SPÁLENIE (4 rovnaké karty) ---
private static readonly CardJoke[] BurnJokes =
[
new("🎩", "ILUZIONISTA", "Kúzelník na javisku: 'A teraz sledujte pozorne, nechám zmiznúť 4 karty!'", "– Hráč z rohu kričí: 'Nič nové, to robím v každom kole spálením!'"),
new("🚒", "POŽIARNY POPLACH", "Hasiči práve vyhlásili výjazd k nášmu hernému stolu!", "– Niekto tu práve odpálil 4 rovnaké karty a stôl ľahol popolom!"),
new("🧙", "LEGENDA V OHNI", "Stretnú sa 4 rovnaké karty v balíčku a jedna hovorí:", "– 'Dnes z nás bude buď slávna legenda, alebo čistý popol!'"),
new("🔥", "ČISTÁ POÉZIA", "Spáliť 4 karty naraz jedným ťahom nie je len obyčajné šťastie...", "– ...je to čisté vizuálne umenie a nočná mora pre súperov!")
];
// --- VTIPY: DLHÁ HRA (TurnCount >= 28) ---
private static readonly CardJoke[] LongGameJokes =
[
new("⏳", "RODINNÝ DÔCHODOK", "Manželka volá kartárovi: 'Kedy už prídeš domov z toho zápasu?'", "– 'Drahá, ešte jedno kolo a môžeme ísť rovno spolu do dôchodku!'"),
new("🏛️", "PAMIATKOVÝ ÚRAD", "Táto partia trvá už tak neskutočne dlho...", "– ...že si na ďalší ťah budeme musieť pýtať povolenie od pamiatkárov!"),
new("🧔", "ČAS NA HOLENIE", "Začiatok zápasu: hladko oholení mladíci plní síl.", "– Aktuálny stav: fúzatí veteráni čakajúci na záchrannú zeleň!"),
new("🕰️", "ŠPORTOVÝ VÝKON", "Lekár sa pýta pacienta: 'Koľko hodín denne venujete športu?'", "– 'Už šiestu hodinu bez prestávky sedím pri napínavom Faraónovi!'")
];
// --- VTIPY: OŽIVENIE (Červená sedma) ---
private static readonly CardJoke[] ReviveJokes =
[
new("🧟", "ZÁKAZ ODCHODU", "Lekár konštatuje: 'Hráč úspešne vyhral a odchádza domov.'", "– Červená sedma na to: 'Ešte nie, kamarát! Poď pekne naspäť k stolu!'"),
new("⚡", "VZKRIESENIE ŠAMPIÓNA", "Viete, čo je rýchlejšie ako ranný pondelkový budík?", "– Hráč vrátený do hry Červenou sedmou s chuťou po veľkej pomste!")
];
public static CardJoke? GetContextualJoke(GameEventType? type, int extraData, int turnCount)
{
if (type == GameEventType.SevenStacked && extraData >= 9)
{
return PickRandom(MultiDrawJokes);
}
if (type == GameEventType.CardsBurned)
{
return PickRandom(BurnJokes);
}
if (type == GameEventType.PlayerRevived)
{
return PickRandom(ReviveJokes);
}
if (turnCount >= 28 && turnCount % 16 == 0)
{
return PickRandom(LongGameJokes);
}
return null;
}
private static T PickRandom<T>(T[] options)
{
if (options == null || options.Length == 0) return default!;
return options[_rng.Next(options.Length)];
}
}
+20 -8
View File
@@ -70,22 +70,29 @@ public class RuleOptions
public bool PassAndPlayPrivacyScreen { get; set; } = false;
/// <summary>
/// Úroveň rýchlosti hry (1 = 0.5x, 2 = 0.75x, 3 = 1x Normálna, 4 = 1.5x Rýchla, 5 = 2.5x Turbo).
/// Úroveň rýchlosti hry od -5 (Extra pomalá 0.2x) cez 0 (1x Normálna) až po +5 (Blesková 4x).
/// </summary>
public int SpeedLevel { get; set; } = 3;
public int SpeedLevel { get; set; } = 0;
/// <summary>
/// Rýchlosť AI ťahov v milisekundách podľa zvolenej úrovne rýchlosti.
/// Rýchlosť AI ťahov v milisekundách podľa zvolenej úrovne rýchlosti (-5 až +5).
/// </summary>
public int AIDelayMs
{
get => SpeedLevel switch
{
1 => 1600,
2 => 1200,
4 => 500,
5 => 200,
_ => 900
-5 => 3200,
-4 => 2600,
-3 => 2100,
-2 => 1700,
-1 => 1400,
0 => 1100,
1 => 850,
2 => 650,
3 => 450,
4 => 300,
5 => 150,
_ => 1100
};
set { }
}
@@ -100,4 +107,9 @@ public class RuleOptions
/// Hrá sa až kým nezostane iba jeden celkový víťaz.
/// </summary>
public bool OlympicMode { get; set; } = false;
/// <summary>
/// AI Human Mode: AI hráči zobrazujú premýšľacie bubliny s taktickými a zábavnými hláškami a reagujú na herné udalosti.
/// </summary>
public bool AIHumanMode { get; set; } = false;
}
+45
View File
@@ -431,4 +431,49 @@ public class GameEngineTests
// When facing penalty and disabled, Green Under Knave cannot be played
Assert.False(ActionValidator.CanPlayCard(greenUnderKnave, topSeven, null, 3, false, rules));
}
[Fact]
public void AIDialogueService_ReturnsAppropriateDialogues()
{
var easyAI = new Player("AI Easy", PlayerType.AI, "🦊", AIDifficulty.Easy);
var hardAI = new Player("AI Hard", PlayerType.AI, "🤖", AIDifficulty.Hard);
var state = new GameState();
easyAI.Hand.Add(new Card(Suit.Hearts, Rank.Eight));
easyAI.Hand.Add(new Card(Suit.Hearts, Rank.Nine));
// Thinking dialogue for easy AI
var easyThought = AIDialogueService.GetThinkingDialogue(easyAI, state);
Assert.False(string.IsNullOrWhiteSpace(easyThought));
// Thinking dialogue for hard AI
var hardThought = AIDialogueService.GetThinkingDialogue(hardAI, state);
Assert.False(string.IsNullOrWhiteSpace(hardThought));
// Event reactions
var freezeEvent = new GameEvent(GameEventType.AceSkipped, easyAI);
var freezeReaction = AIDialogueService.GetEventReaction(freezeEvent, easyAI);
Assert.False(string.IsNullOrWhiteSpace(freezeReaction));
var penaltyEvent = new GameEvent(GameEventType.SevenStacked, null, TargetPlayer: easyAI, ExtraData: 6);
var penaltyReaction = AIDialogueService.GetEventReaction(penaltyEvent, easyAI);
Assert.False(string.IsNullOrWhiteSpace(penaltyReaction));
var cancelEvent = new GameEvent(GameEventType.SevenCanceled, easyAI);
var cancelReaction = AIDialogueService.GetEventReaction(cancelEvent, easyAI);
Assert.False(string.IsNullOrWhiteSpace(cancelReaction));
// Card jokes
var multiDrawJoke = AIDialogueService.GetContextualJoke(GameEventType.SevenStacked, 12, 10);
Assert.NotNull(multiDrawJoke);
Assert.False(string.IsNullOrWhiteSpace(multiDrawJoke.Punchline));
var burnJoke = AIDialogueService.GetContextualJoke(GameEventType.CardsBurned, 0, 10);
Assert.NotNull(burnJoke);
Assert.False(string.IsNullOrWhiteSpace(burnJoke.Setup));
var longGameJoke = AIDialogueService.GetContextualJoke(null, 0, 32);
Assert.NotNull(longGameJoke);
Assert.False(string.IsNullOrWhiteSpace(longGameJoke.Punchline));
}
}