Skip to content

Commit 93f48cc

Browse files
wakesyncclaude
andcommitted
feat(agents): add Texas Hold'em LLM prompts
Game-specific prompts for LLM agents playing Texas Hold'em: - TEXAS_HOLDEM_SYSTEM_PROMPT: Expert poker player system prompt with hand rankings and position strategy - formatPokerObservation(): Formats game state into LLM prompt - parsePokerAction(): Extracts action from LLM response - estimateHandStrength(): Pre-flop hand strength evaluation - Position descriptions (BTN, SB, BB, UTG, etc.) Prompt includes: - Complete hand rankings (Royal Flush to High Card) - Position-based strategy notes - Betting strategy guidelines - Clear action format requirements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5f2aeb3 commit 93f48cc

2 files changed

Lines changed: 233 additions & 0 deletions

File tree

packages/agents/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ export {
2323
} from "./llm-openrouter.js";
2424
export type { OpenRouterConfig } from "./llm-openrouter.js";
2525

26+
// Game-specific LLM prompts
27+
export {
28+
TEXAS_HOLDEM_SYSTEM_PROMPT,
29+
formatPokerObservation,
30+
parsePokerAction,
31+
estimateHandStrength,
32+
} from "./prompts/texas-holdem.js";
33+
2634
// Agent registry
2735
import { randomAgent } from "./random.js";
2836
import { rpsCounterAgent } from "./rps-counter.js";
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
/**
2+
* Texas Hold'em LLM Prompts
3+
*
4+
* Game-specific prompts for LLM agents playing Texas Hold'em poker.
5+
*/
6+
7+
import type { HoldemObservation, Card } from "@cmax/games";
8+
9+
export const TEXAS_HOLDEM_SYSTEM_PROMPT = `You are an expert poker player competing in a Texas Hold'em tournament. Make optimal decisions based on:
10+
- Hand strength and potential
11+
- Pot odds and implied odds
12+
- Position relative to dealer
13+
- Stack sizes and bet sizing
14+
- Opponent tendencies and bet patterns
15+
16+
HAND RANKINGS (strongest to weakest):
17+
1. Royal Flush (A-K-Q-J-10 same suit)
18+
2. Straight Flush (five consecutive cards, same suit)
19+
3. Four of a Kind (four cards of same rank)
20+
4. Full House (three of a kind + pair)
21+
5. Flush (five cards same suit)
22+
6. Straight (five consecutive cards)
23+
7. Three of a Kind
24+
8. Two Pair
25+
9. One Pair
26+
10. High Card
27+
28+
POSITION NOTES:
29+
- Early position: play tight, premium hands only
30+
- Middle position: slightly wider range
31+
- Late position: can play more hands, steal blinds
32+
- Button: best position, act last post-flop
33+
34+
BETTING STRATEGY:
35+
- Value bet with strong hands
36+
- Bluff selectively with good blockers
37+
- Consider pot odds when calling
38+
- Fold weak hands to heavy aggression
39+
40+
Respond with EXACTLY one of these actions (nothing else):
41+
- FOLD
42+
- CHECK
43+
- CALL
44+
- RAISE <amount>
45+
- ALL_IN`;
46+
47+
const SUIT_SYMBOLS: Record<string, string> = {
48+
h: "\u2665",
49+
d: "\u2666",
50+
c: "\u2663",
51+
s: "\u2660",
52+
};
53+
54+
/**
55+
* Format a card for display
56+
*/
57+
function formatCard(card: Card): string {
58+
return `${card.rank}${SUIT_SYMBOLS[card.suit] || card.suit}`;
59+
}
60+
61+
/**
62+
* Format cards array for display
63+
*/
64+
function formatCards(cards: Card[]): string {
65+
if (cards.length === 0) return "None yet";
66+
return cards.map(formatCard).join(" ");
67+
}
68+
69+
/**
70+
* Format a Texas Hold'em observation into a prompt for the LLM
71+
*/
72+
export function formatPokerObservation(obs: HoldemObservation): string {
73+
const hand = obs.hand ? formatCards(obs.hand) : "Unknown";
74+
const community = formatCards(obs.communityCards);
75+
76+
const opponents = obs.opponents
77+
.map((o) => {
78+
let status = "";
79+
if (o.folded) status = " (folded)";
80+
else if (o.allIn) status = " (all-in)";
81+
return ` - Player ${o.playerId}: ${o.chips} chips, bet ${o.bet}${status}`;
82+
})
83+
.join("\n");
84+
85+
let actions = "FOLD";
86+
if (obs.toCall === 0) {
87+
actions += ", CHECK";
88+
} else {
89+
actions += `, CALL (${obs.toCall})`;
90+
}
91+
actions += `, RAISE (min: ${obs.minRaise})`;
92+
actions += ", ALL_IN";
93+
94+
return `CURRENT SITUATION:
95+
- Your hand: ${hand}
96+
- Community cards: ${community}
97+
- Pot: ${obs.pot} chips
98+
- Your chips: ${obs.myChips}
99+
- Your current bet: ${obs.myBet}
100+
- Amount to call: ${obs.toCall}
101+
- Minimum raise: ${obs.minRaise}
102+
- Betting round: ${obs.round.toUpperCase()}
103+
- Your position: ${getPositionDescription(obs)}
104+
105+
OPPONENTS:
106+
${opponents}
107+
108+
LEGAL ACTIONS: ${actions}
109+
110+
Choose your action:`;
111+
}
112+
113+
/**
114+
* Get position description based on dealer button
115+
*/
116+
function getPositionDescription(obs: HoldemObservation): string {
117+
const numPlayers = obs.opponents.length + 1;
118+
const relativePosition = (obs.playerId - obs.dealerIndex + numPlayers) % numPlayers;
119+
120+
if (numPlayers === 2) {
121+
return relativePosition === 0 ? "Button/Small Blind" : "Big Blind";
122+
}
123+
124+
switch (relativePosition) {
125+
case 0:
126+
return "Button (BTN)";
127+
case 1:
128+
return "Small Blind (SB)";
129+
case 2:
130+
return "Big Blind (BB)";
131+
case 3:
132+
return "Under the Gun (UTG)";
133+
default:
134+
if (relativePosition >= numPlayers - 2) {
135+
return "Late Position (CO/HJ)";
136+
}
137+
return "Middle Position (MP)";
138+
}
139+
}
140+
141+
/**
142+
* Parse LLM response to extract poker action
143+
*/
144+
export function parsePokerAction(
145+
response: string,
146+
obs: HoldemObservation
147+
): string {
148+
const normalized = response.trim().toUpperCase();
149+
150+
// Check for simple actions
151+
if (normalized === "FOLD") return "fold";
152+
if (normalized === "CHECK") return "check";
153+
if (normalized === "CALL") return "call";
154+
if (normalized === "ALL_IN" || normalized === "ALLIN" || normalized === "ALL-IN") {
155+
return "all_in";
156+
}
157+
158+
// Parse RAISE <amount>
159+
const raiseMatch = normalized.match(/RAISE\s*(\d+)/);
160+
if (raiseMatch) {
161+
const amount = parseInt(raiseMatch[1], 10);
162+
const clampedAmount = Math.max(amount, obs.minRaise);
163+
return `raise_${clampedAmount}`;
164+
}
165+
166+
// If just "RAISE" without amount, use minimum
167+
if (normalized.startsWith("RAISE")) {
168+
return `raise_${obs.minRaise}`;
169+
}
170+
171+
// Default to fold if unparseable
172+
console.warn(`Could not parse poker action: "${response}", defaulting to fold`);
173+
return "fold";
174+
}
175+
176+
/**
177+
* Estimate hand strength (simplified)
178+
*/
179+
export function estimateHandStrength(
180+
hand: [Card, Card],
181+
communityCards: Card[]
182+
): string {
183+
// Pre-flop evaluation
184+
if (communityCards.length === 0) {
185+
const [c1, c2] = hand;
186+
const isPair = c1.rank === c2.rank;
187+
const isSuited = c1.suit === c2.suit;
188+
189+
const rankValue1 = "23456789TJQKA".indexOf(c1.rank);
190+
const rankValue2 = "23456789TJQKA".indexOf(c2.rank);
191+
const highRank = Math.max(rankValue1, rankValue2);
192+
const lowRank = Math.min(rankValue1, rankValue2);
193+
const gap = highRank - lowRank;
194+
195+
// Premium pairs
196+
if (isPair && highRank >= 10) return "Premium (high pair)";
197+
if (isPair && highRank >= 7) return "Strong (medium pair)";
198+
if (isPair) return "Playable (low pair)";
199+
200+
// Big cards
201+
if (highRank >= 11 && lowRank >= 10) {
202+
return isSuited ? "Premium (big suited)" : "Strong (big cards)";
203+
}
204+
205+
// Suited connectors
206+
if (isSuited && gap <= 2 && lowRank >= 6) {
207+
return "Playable (suited connector)";
208+
}
209+
210+
// Suited aces
211+
if (isSuited && (c1.rank === "A" || c2.rank === "A")) {
212+
return "Playable (suited ace)";
213+
}
214+
215+
// High cards
216+
if (highRank >= 10 && lowRank >= 8) {
217+
return "Marginal (high cards)";
218+
}
219+
220+
return "Weak (fold candidate)";
221+
}
222+
223+
// Post-flop - simplified
224+
return "Evaluate based on board texture";
225+
}

0 commit comments

Comments
 (0)