Claude offers a hand of blackjack while it works on a long task

When Claude starts a task it expects to take a while, it offers you a hand of blackjack in a side pane. You play while it works.
🃏 Big refactor – this will take a while. Fancy a hand of blackjack? [ Play ] [ No thanks ] [ Don't ask again ]

mcp__blackjack__offer_blackjack. Claude itself decides when to call it: at the start of each long task. There is no timer or heuristic.h Hit, s Stand, d Double, n new deal; arrows/Tab move between buttons, Enter presses; Esc closes the table./blackjack opens the table at any time, even while a turn is running.Inside Claude Code:
/plugin marketplace add gszecsenyi/blackjack-claude-mod
/plugin install blackjack@blackjack-claude-mod
/reload-plugins
/blackjack
Or from a local clone: claude --plugin-dir /path/to/blackjack-claude-mod.
claude plugin validate .
claude plugin test .
MIT – see LICENSE.
hooks/register.tsx 204 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Card, Game } from '../types'
5import {
6 START_CHIPS,
7 canDouble,
8 cryptoRandom,
9 deal,
10 double,
11 hit,
12 label,
13 newGame,
14 stand,
15 value,
16} from './game'
17
18const PANE = 'blackjack'
19const TOOL = 'offer_blackjack'
20
21const game = atom({ plugin: 'blackjack', key: 'game' } as const, null)
22const isOfferShown = atom({ plugin: 'blackjack', key: 'isOfferShown' } as const, false)
23const wasOffered = atom({ plugin: 'blackjack', key: 'wasOffered' } as const, false)
24const offerReason = atom({ plugin: 'blackjack', key: 'offerReason' } as const, '')
25
26const RULES =
27 'Get closer to 21 than the dealer without going over. Face cards are 10, aces 1 or 11; the dealer draws to 17, blackjack pays 3:2.'
28
29const DESCRIPTION = [
30 'Offers the user a hand of blackjack in a side pane while you keep working.',
31 'Call it once per task, right as you start a task you expect to take several minutes',
32 '(many files to change, a long build or test run, a broad refactor or investigation).',
33 'Do not call it for short tasks or quick questions. It returns immediately; carry on with the task.',
34].join(' ')
35
36async function loadChips($: EngineInterface): Promise<number> {
37 const stored = await $.store.get('chips')
38
39 return typeof stored === 'number' && stored > 0 ? stored : START_CHIPS
40}
41
42async function play($: EngineInterface, move: (g: Game) => Game) {
43 const chips = await loadChips($)
44 const after = await update($, game, g => move(g ?? newGame(chips)))
45 await $.store.set('chips', after.chips)
46}
47
48const openTable = ($: EngineInterface) =>
49 $.ui.open({ id: PANE, title: 'Blackjack', focus: true, closeOnEscape: true, holdToasts: true, rows: 12 })
50
51const hand = (cards: readonly Card[], isHidden = false) =>
52 cards.map((card, i) => (isHidden && i === 1 ? '🂠' : label(card))).join(' ')
53
54export const register: Register = on => {
55 on('session.start', async ($, e, next) => {
56 await $.tool.register({
57 name: TOOL,
58 description: DESCRIPTION,
59 inputSchema: {
60 type: 'object',
61 properties: {
62 reason: { type: 'string', description: 'A few words on why this will take a while' },
63 },
64 },
65 })
66 await $.command.register({
67 name: 'blackjack',
68 description: 'Open the blackjack table',
69 immediate: true,
70 })
71
72 return next(e)
73 })
74
75 on('tool.call', { tool: 'mcp__blackjack__offer_blackjack' }, async ($, e) => {
76 if ((await $.store.get('optOut')) === true) {
77 return { result: 'The user turned blackjack offers off. Carry on with the task.' }
78 }
79 if (await read($, wasOffered)) {
80 return { result: 'Already offered for this task. Carry on with the task.' }
81 }
82 if ((await $.ui.panes()).some(pane => pane.id === PANE)) {
83 return { result: 'The user is already playing. Carry on with the task.' }
84 }
85 const reason = (e as { reason?: unknown }).reason
86 await update($, offerReason, () => (typeof reason === 'string' ? reason.slice(0, 80) : ''))
87 await update($, wasOffered, () => true)
88 await update($, isOfferShown, () => true)
89
90 return { result: 'Offered. Carry on with the task; do not mention the game.' }
91 })
92
93 on('command.run', { command: 'blackjack' }, async $ => {
94 await update($, isOfferShown, () => false)
95 await openTable($)
96
97 return { text: 'Blackjack table open (Esc returns to the prompt).' }
98 })
99
100 on('turn.complete', async ($, e, next) => {
101 await update($, isOfferShown, () => false)
102 await update($, wasOffered, () => false)
103 const isPlaying = (await $.ui.panes()).some(pane => pane.id === PANE)
104 if (isPlaying) {
105 $.ui.status('🃏 Claude is done – your game will wait.')
106 }
107
108 return next(e)
109 })
110
111 on('ui.close', ($, e, next) => {
112 if (e.id === PANE) {
113 $.ui.status(undefined)
114 }
115
116 return next(e)
117 })
118
119 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
120 if (e.props.hasSurvey || !(await read($, isOfferShown))) {
121 return next(e)
122 }
123 const { Box, Button, Text } = $.ui.resolve(e)
124 const reason = await read($, offerReason)
125 const hide = () => update($, isOfferShown, () => false)
126
127 return (
128 <Box gap={1}>
129 <Text>
130 🃏 {reason ? `${reason} – this` : 'This'} will take a while. Fancy a hand of blackjack?
131 </Text>
132 <Button
133 key="play"
134 label="Play"
135 variant="primary"
136 onPress={async () => {
137 await hide()
138 await openTable($)
139 }}
140 />
141 <Button key="no" label="No thanks" onPress={hide} />
142 <Button
143 key="never"
144 label="Don't ask again"
145 dimColor
146 onPress={async () => {
147 await $.store.set('optOut', true)
148 await hide()
149 }}
150 />
151 </Box>
152 )
153 })
154
155 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
156 const { Box, Button, Text } = $.ui.resolve(e)
157 const g = (await read($, game)) ?? newGame(await loadChips($))
158 const isLive = g.phase === 'player'
159 const random = cryptoRandom
160 const keys = isLive ? `h Hit · s Stand${canDouble(g) ? ' · d Double' : ''}` : 'n Deal'
161 const help = `${keys} · ←→/Tab move · Enter press · Esc close`
162
163 return (
164 <Box flexDirection="column">
165 <Text dimColor>{RULES}</Text>
166 {g.phase === 'idle' ? (
167 <Text dimColor>Press Deal (n) to start. Bet: 50.</Text>
168 ) : (
169 <Box flexDirection="column">
170 <Text>
171 Dealer: {hand(g.dealer, isLive)}
172 <Text dimColor> {isLive ? '' : `(${value(g.dealer)})`}</Text>
173 </Text>
174 <Text>
175 You: {hand(g.player)}
176 <Text dimColor> ({value(g.player)})</Text>
177 </Text>
178 </Box>
179 )}
180 <Text bold={g.phase === 'done'}>{g.message || ' '}</Text>
181 <Text dimColor>
182 Chips: {g.chips}
183 {isLive ? ` Bet: ${g.bet}` : ''}
184 </Text>
185 <Box gap={1}>
186 {isLive ? (
187 <Box gap={1}>
188 <Button key="hit" label="Hit" hotkey="h" variant="primary" autoFocus onPress={() => play($, s => hit(s, random))} />
189 <Button key="stand" label="Stand" hotkey="s" onPress={() => play($, s => stand(s, random))} />
190 {canDouble(g) && (
191 <Button key="double" label="Double" hotkey="d" onPress={() => play($, s => double(s, random))} />
192 )}
193 </Box>
194 ) : (
195 <Button key="deal" label="Deal" hotkey="n" variant="primary" autoFocus onPress={() => play($, s => deal(s, random))} />
196 )}
197 <Button key="close" label="Close" role="dismiss" dimColor onPress={() => $.ui.close({ id: PANE })} />
198 </Box>
199 <Text dimColor>{help}</Text>
200 </Box>
201 )
202 })
203}
204hooks/game.ts 165 lines1import type { Card, Game, Suit } from '../types'
2
3export const START_CHIPS = 1000
4export const BET = 50
5
6const RANKS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
7const SUITS: Suit[] = ['♠', '♥', '♦', '♣']
8const DECKS = 4
9const RESHUFFLE_BELOW = 20
10
11export type Random = () => number
12
13export const cryptoRandom: Random = () => {
14 const one = new Uint32Array(1)
15 crypto.getRandomValues(one)
16
17 return (one[0] ?? 0) / 2 ** 32
18}
19
20export function shoe(random: Random): Card[] {
21 const cards: Card[] = []
22 for (let d = 0; d < DECKS; d++) {
23 for (const suit of SUITS) {
24 for (const rank of RANKS) {
25 cards.push({ rank, suit })
26 }
27 }
28 }
29 for (let i = cards.length - 1; i > 0; i--) {
30 const j = Math.floor(random() * (i + 1))
31 const a = cards[i] as Card
32 cards[i] = cards[j] as Card
33 cards[j] = a
34 }
35
36 return cards
37}
38
39export function value(hand: readonly Card[]): number {
40 let total = 0
41 let aces = 0
42 for (const card of hand) {
43 if (card.rank === 'A') {
44 aces += 1
45 total += 11
46 } else if (card.rank === 'J' || card.rank === 'Q' || card.rank === 'K') {
47 total += 10
48 } else {
49 total += Number(card.rank)
50 }
51 }
52 while (total > 21 && aces > 0) {
53 total -= 10
54 aces -= 1
55 }
56
57 return total
58}
59
60export const isBlackjack = (hand: readonly Card[]) => hand.length === 2 && value(hand) === 21
61
62export const label = (card: Card) => `${card.rank}${card.suit}`
63
64export function newGame(chips: number): Game {
65 return { deck: [], player: [], dealer: [], phase: 'idle', bet: 0, chips, message: '' }
66}
67
68function draw(deck: Card[], random: Random): { card: Card; deck: Card[] } {
69 const from = deck.length > 0 ? deck : shoe(random)
70 const [card, ...rest] = from
71
72 return { card: card as Card, deck: rest }
73}
74
75export function deal(game: Game, random: Random): Game {
76 const chips = game.chips < BET ? START_CHIPS : game.chips
77 let deck = game.deck.length < RESHUFFLE_BELOW ? shoe(random) : game.deck
78 const take = () => {
79 const drawn = draw(deck, random)
80 deck = drawn.deck
81
82 return drawn.card
83 }
84 const player = [take()]
85 const dealer = [take()]
86 player.push(take())
87 dealer.push(take())
88
89 const next: Game = {
90 deck,
91 player,
92 dealer,
93 phase: 'player',
94 bet: BET,
95 chips: chips - BET,
96 message: game.chips < BET ? 'Out of chips – here are 1000 more.' : '',
97 }
98
99 return isBlackjack(player) || isBlackjack(dealer) ? settle(next) : next
100}
101
102export function hit(game: Game, random: Random): Game {
103 if (game.phase !== 'player') {
104 return game
105 }
106 const { card, deck } = draw(game.deck, random)
107 const next = { ...game, deck, player: [...game.player, card] }
108
109 return value(next.player) >= 21 ? stand(next, random) : next
110}
111
112export const canDouble = (game: Game) =>
113 game.phase === 'player' && game.player.length === 2 && game.chips >= game.bet
114
115export function double(game: Game, random: Random): Game {
116 if (!canDouble(game)) {
117 return game
118 }
119 const { card, deck } = draw(game.deck, random)
120
121 return stand(
122 { ...game, deck, player: [...game.player, card], chips: game.chips - game.bet, bet: game.bet * 2 },
123 random,
124 )
125}
126
127export function stand(game: Game, random: Random): Game {
128 if (game.phase !== 'player') {
129 return game
130 }
131 if (value(game.player) > 21) {
132 return settle(game)
133 }
134 let deck = game.deck
135 const dealer = [...game.dealer]
136 while (value(dealer) < 17) {
137 const drawn = draw(deck, random)
138 deck = drawn.deck
139 dealer.push(drawn.card)
140 }
141
142 return settle({ ...game, deck, dealer })
143}
144
145export function settle(game: Game): Game {
146 const you = value(game.player)
147 const house = value(game.dealer)
148 const done = (won: number, message: string): Game => ({
149 ...game,
150 phase: 'done',
151 chips: game.chips + won,
152 message,
153 })
154
155 if (isBlackjack(game.player) && isBlackjack(game.dealer)) return done(game.bet, 'Both have blackjack – push.')
156 if (isBlackjack(game.player)) return done(game.bet + Math.floor(game.bet * 1.5), `Blackjack! +${Math.floor(game.bet * 1.5)}`)
157 if (isBlackjack(game.dealer)) return done(0, `Dealer has blackjack. −${game.bet}`)
158 if (you > 21) return done(0, `Bust (${you}). −${game.bet}`)
159 if (house > 21) return done(game.bet * 2, `Dealer busts (${house}). +${game.bet}`)
160 if (you > house) return done(game.bet * 2, `You win, ${you} vs ${house}. +${game.bet}`)
161 if (you < house) return done(0, `Dealer wins, ${house} vs ${you}. −${game.bet}`)
162
163 return done(game.bet, `Push, ${you}.`)
164}
165types/index.d.ts 25 lines1export type Suit = '♠' | '♥' | '♦' | '♣'
2export type Card = { rank: string; suit: Suit }
3export type Phase = 'idle' | 'player' | 'done'
4
5export type Game = {
6 deck: Card[]
7 player: Card[]
8 dealer: Card[]
9 phase: Phase
10 bet: number
11 chips: number
12 message: string
13}
14
15declare module 'claude-code' {
16 interface PluginState {
17 blackjack: {
18 game: Game | null
19 isOfferShown: boolean
20 wasOffered: boolean
21 offerReason: string
22 }
23 }
24}
25