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.
182 lines
4.8 KiB
Svelte
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>
|