PokeR/src/lib/components/TableSetup.svelte
Veit F. 422fa5b3ab feat: implement bot intelligence system with 8 archetypes and coaching
Add personality-driven bots with 8 archetypes (Nit, TAG, LAG, Maniac, Calling Station, Loose Fish, Old Man, Monster TAG) across 5 skill levels.

Includes:

- Three-layer decision pipeline (base strategy → personality filter → skill noise)

- Decision timer system with archetype-specific timeout defaults

- Observation tracking engine (VPIP, PFR, Fold-to-CBet, WTSD, bet sizing, timing tells)

- Player classification engine with weighted scoring and confidence scaling

- Table setup UI with visual seat editor and quick presets

- Info display system with 4 visibility levels

- Teaching coach with post-hand analysis and real-time suggestions

Archives bot-intelligence change and syncs all 8 delta specs to main specs.
2026-05-17 22:41:09 +02:00

182 lines
4.8 KiB
Svelte

<script lang="ts">
import type { SeatConfig } from '$lib/game/table-presets';
import { ARCHETYPE_LABELS, ALL_ARCHETYPES } from '$lib/types/bot-archetype';
import { SKILL_LABELS, ALL_SKILL_LEVELS } from '$lib/types/skill-level';
let {
seatConfigs,
humanSeatIndex = 0,
onSeatChange
} = $props<{
seatConfigs: SeatConfig[];
humanSeatIndex?: number;
onSeatChange?: (index: number, config: SeatConfig) => void;
}>();
let selectedSeatIndex = $state<number | null>(null);
let archetypeValue = $state('');
let skillValue = $state('');
function getSeatPosition(index: number, total: number): string {
const angle = (index / total) * 2 * Math.PI - Math.PI / 2;
const radiusX = 180;
const radiusY = 130;
const x = 200 + Math.cos(angle) * radiusX - 40;
const y = 150 + Math.sin(angle) * radiusY - 30;
return `left: ${x}px; top: ${y}px;`;
}
$effect(() => {
if (selectedSeatIndex !== null && selectedSeatIndex !== humanSeatIndex) {
const config = seatConfigs[selectedSeatIndex];
archetypeValue = config.archetype;
skillValue = config.skillLevel;
}
});
function saveChanges() {
if (selectedSeatIndex === null || selectedSeatIndex === humanSeatIndex) return;
const newConfig: SeatConfig = {
...seatConfigs[selectedSeatIndex],
archetype: archetypeValue as SeatConfig['archetype'],
skillLevel: skillValue as SeatConfig['skillLevel']
};
if (onSeatChange) {
onSeatChange(selectedSeatIndex, newConfig);
}
}
</script>
<div class="table-setup">
<div class="poker-table">
{#each seatConfigs as config, i}
<button
class="seat"
class:human={i === humanSeatIndex}
class:selected={i === selectedSeatIndex}
onclick={() => selectedSeatIndex = i}
disabled={i === humanSeatIndex}
style={getSeatPosition(i, seatConfigs.length)}
>
<div class="seat-label">Seat {i + 1}</div>
{#if i === humanSeatIndex}
<div class="human-badge">YOU</div>
{:else}
<div class="archetype">{ARCHETYPE_LABELS[config.archetype as keyof typeof ARCHETYPE_LABELS]}</div>
<div class="skill">{SKILL_LABELS[config.skillLevel as keyof typeof SKILL_LABELS]}</div>
{/if}
</button>
{/each}
</div>
{#if selectedSeatIndex !== null && selectedSeatIndex !== humanSeatIndex}
<div class="seat-editor">
<h3>Edit Seat {selectedSeatIndex + 1}</h3>
<div class="field">
<label for="archetype-select">Archetype</label>
<select id="archetype-select" bind:value={archetypeValue} onchange={saveChanges}>
{#each ALL_ARCHETYPES as archetype}
<option value={archetype}>{ARCHETYPE_LABELS[archetype]}</option>
{/each}
</select>
</div>
<div class="field">
<label for="skill-select">Skill Level</label>
<select id="skill-select" bind:value={skillValue} onchange={saveChanges}>
{#each ALL_SKILL_LEVELS as level}
<option value={level}>{SKILL_LABELS[level]}</option>
{/each}
</select>
</div>
</div>
{/if}
</div>
<style>
.table-setup {
display: flex;
gap: 2rem;
align-items: flex-start;
}
.poker-table {
position: relative;
width: 400px;
height: 300px;
border-radius: 50%;
border: 3px solid #2d5a27;
background: radial-gradient(ellipse at center, #1a4a1a 0%, #0d2a0d 100%);
}
.seat {
position: absolute;
width: 80px;
height: 60px;
border-radius: 8px;
border: 2px solid #444;
background: rgba(0, 0, 0, 0.7);
color: #ccc;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 0.7rem;
transition: border-color 0.2s, transform 0.1s;
}
.seat:hover {
border-color: #888;
transform: scale(1.05);
}
.seat.selected {
border-color: #4a9eff;
box-shadow: 0 0 10px rgba(74, 158, 255, 0.5);
}
.seat.human {
border-color: #ff9800;
cursor: default;
}
.seat-label {
font-size: 0.6rem;
color: #888;
}
.human-badge {
font-weight: bold;
color: #ff9800;
}
.archetype {
font-size: 0.55rem;
text-align: center;
line-height: 1.1;
}
.skill {
font-size: 0.5rem;
color: #888;
}
.seat-editor {
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
padding: 1rem;
min-width: 200px;
}
.seat-editor h3 {
margin: 0 0 1rem;
font-size: 0.9rem;
}
.field {
margin-bottom: 0.75rem;
}
.field label {
display: block;
font-size: 0.75rem;
color: #888;
margin-bottom: 0.25rem;
}
.field select {
width: 100%;
padding: 0.4rem;
border-radius: 4px;
border: 1px solid #444;
background: #1a1a1a;
color: #ccc;
}
</style>