guandan.dev

guandan.dev

https://git.tonybtw.com/guandan.dev.git git://git.tonybtw.com/guandan.dev.git
11,130 bytes raw
1
import { Card, Rank, is_wild } from './types'
2
3
export interface Card_Group {
4
  type: 'bomb' | 'straight_flush' | 'triple' | 'pair' | 'wild' | 'single'
5
  cards: Card[]
6
}
7
8
// Group cards for visual display - finds bombs, straights, pairs, etc.
9
export function group_cards_for_display(cards: Card[], level: Rank): Card_Group[] {
10
  const groups: Card_Group[] = []
11
  const used = new Set<number>()
12
13
  // First extract wild cards (heart level cards)
14
  const wilds = cards.filter(c => is_wild(c, level))
15
  wilds.forEach(c => {
16
    used.add(c.Id)
17
    groups.push({ type: 'wild', cards: [c] })
18
  })
19
20
  const remaining = cards.filter(c => !used.has(c.Id))
21
22
  // Group by rank
23
  const by_rank = new Map<number, Card[]>()
24
  remaining.forEach(c => {
25
    const arr = by_rank.get(c.Rank) || []
26
    arr.push(c)
27
    by_rank.set(c.Rank, arr)
28
  })
29
30
  // Find bombs (4+ of same rank, or joker bomb)
31
  const jokers = remaining.filter(c => c.Rank === 13 || c.Rank === 14)
32
  if (jokers.length === 4) {
33
    jokers.forEach(c => used.add(c.Id))
34
    groups.push({ type: 'bomb', cards: jokers })
35
  }
36
37
  by_rank.forEach((rank_cards) => {
38
    const unused = rank_cards.filter(c => !used.has(c.Id))
39
    if (unused.length >= 4) {
40
      unused.forEach(c => used.add(c.Id))
41
      groups.push({ type: 'bomb', cards: unused })
42
    }
43
  })
44
45
  // Find straight flushes (5+ consecutive same suit)
46
  const by_suit = new Map<number, Card[]>()
47
  remaining.filter(c => !used.has(c.Id) && c.Rank < 13).forEach(c => {
48
    const arr = by_suit.get(c.Suit) || []
49
    arr.push(c)
50
    by_suit.set(c.Suit, arr)
51
  })
52
53
  by_suit.forEach((suit_cards) => {
54
    const sorted = [...suit_cards].sort((a, b) => a.Rank - b.Rank)
55
    let run: Card[] = []
56
57
    for (const card of sorted) {
58
      if (used.has(card.Id)) continue
59
      if (run.length === 0 || card.Rank === run[run.length - 1].Rank + 1) {
60
        run.push(card)
61
      } else {
62
        if (run.length >= 5) {
63
          run.forEach(c => used.add(c.Id))
64
          groups.push({ type: 'straight_flush', cards: run })
65
        }
66
        run = [card]
67
      }
68
    }
69
    if (run.length >= 5) {
70
      run.forEach(c => used.add(c.Id))
71
      groups.push({ type: 'straight_flush', cards: run })
72
    }
73
  })
74
75
  // Find triples
76
  by_rank.forEach((rank_cards) => {
77
    const unused = rank_cards.filter(c => !used.has(c.Id))
78
    if (unused.length === 3) {
79
      unused.forEach(c => used.add(c.Id))
80
      groups.push({ type: 'triple', cards: unused })
81
    }
82
  })
83
84
  // Find pairs
85
  by_rank.forEach((rank_cards) => {
86
    const unused = rank_cards.filter(c => !used.has(c.Id))
87
    if (unused.length === 2) {
88
      unused.forEach(c => used.add(c.Id))
89
      groups.push({ type: 'pair', cards: unused })
90
    }
91
  })
92
93
  // Remaining singles
94
  remaining.filter(c => !used.has(c.Id)).forEach(c => {
95
    groups.push({ type: 'single', cards: [c] })
96
  })
97
98
  return groups
99
}
100
101
// Find all cards with same rank as the given card
102
export function find_same_rank(cards: Card[], rank: number): Card[] {
103
  return cards.filter(c => c.Rank === rank)
104
}
105
106
// Combo types for play validation
107
export type Combo_Type =
108
  | 'single'
109
  | 'pair'
110
  | 'triple'
111
  | 'full_house'
112
  | 'straight'
113
  | 'tube' // consecutive pairs
114
  | 'plate' // consecutive triples
115
  | 'bomb_4'
116
  | 'bomb_5'
117
  | 'bomb_6'
118
  | 'bomb_7'
119
  | 'bomb_8'
120
  | 'straight_flush'
121
  | 'joker_bomb'
122
123
interface Detected_Combo {
124
  type: Combo_Type
125
  cards: Card[]
126
  value: number // for comparison
127
}
128
129
// Detect what combo a set of cards forms
130
export function detect_combo(cards: Card[], level: Rank): Detected_Combo | null {
131
  if (cards.length === 0) return null
132
133
  const sorted = [...cards].sort((a, b) => a.Rank - b.Rank)
134
  const n = sorted.length
135
136
  // Check joker bomb (4 jokers)
137
  if (n === 4 && sorted.every(c => c.Rank === 13 || c.Rank === 14)) {
138
    return { type: 'joker_bomb', cards, value: 1000 }
139
  }
140
141
  // Group by rank
142
  const by_rank = new Map<number, Card[]>()
143
  sorted.forEach(c => {
144
    const arr = by_rank.get(c.Rank) || []
145
    arr.push(c)
146
    by_rank.set(c.Rank, arr)
147
  })
148
149
  // Single
150
  if (n === 1) {
151
    return { type: 'single', cards, value: get_card_value(sorted[0], level) }
152
  }
153
154
  // Pair
155
  if (n === 2 && by_rank.size === 1) {
156
    return { type: 'pair', cards, value: get_card_value(sorted[0], level) }
157
  }
158
159
  // Triple
160
  if (n === 3 && by_rank.size === 1) {
161
    return { type: 'triple', cards, value: get_card_value(sorted[0], level) }
162
  }
163
164
  // Bombs (4-8 of same rank)
165
  if (by_rank.size === 1 && n >= 4 && n <= 8) {
166
    const bomb_types: Record<number, Combo_Type> = {
167
      4: 'bomb_4', 5: 'bomb_5', 6: 'bomb_6', 7: 'bomb_7', 8: 'bomb_8'
168
    }
169
    return { type: bomb_types[n], cards, value: get_card_value(sorted[0], level) + n * 100 }
170
  }
171
172
  // Full house (3+2)
173
  if (n === 5) {
174
    const counts = Array.from(by_rank.values()).map(arr => arr.length).sort()
175
    if (counts.length === 2 && counts[0] === 2 && counts[1] === 3) {
176
      const triple_rank = Array.from(by_rank.entries()).find(([_, arr]) => arr.length === 3)![0]
177
      return { type: 'full_house', cards, value: get_rank_value(triple_rank, level) }
178
    }
179
  }
180
181
  // Straight (5+ consecutive)
182
  if (n >= 5 && by_rank.size === n) {
183
    const ranks = sorted.map(c => c.Rank)
184
    if (is_consecutive(ranks)) {
185
      // Check if straight flush
186
      if (sorted.every(c => c.Suit === sorted[0].Suit)) {
187
        return { type: 'straight_flush', cards, value: get_card_value(sorted[n-1], level) + 500 + n * 10 }
188
      }
189
      return { type: 'straight', cards, value: get_card_value(sorted[n-1], level) }
190
    }
191
  }
192
193
  // Tube (consecutive pairs)
194
  if (n >= 4 && n % 2 === 0) {
195
    const pairs = Array.from(by_rank.entries())
196
    if (pairs.every(([_, arr]) => arr.length === 2)) {
197
      const ranks = pairs.map(([r, _]) => r).sort((a, b) => a - b)
198
      if (is_consecutive(ranks)) {
199
        return { type: 'tube', cards, value: get_rank_value(ranks[ranks.length - 1], level) }
200
      }
201
    }
202
  }
203
204
  // Plate (consecutive triples)
205
  if (n >= 6 && n % 3 === 0) {
206
    const triples = Array.from(by_rank.entries())
207
    if (triples.every(([_, arr]) => arr.length === 3)) {
208
      const ranks = triples.map(([r, _]) => r).sort((a, b) => a - b)
209
      if (is_consecutive(ranks)) {
210
        return { type: 'plate', cards, value: get_rank_value(ranks[ranks.length - 1], level) }
211
      }
212
    }
213
  }
214
215
  return null
216
}
217
218
function is_consecutive(ranks: number[]): boolean {
219
  const sorted = [...ranks].sort((a, b) => a - b)
220
  for (let i = 1; i < sorted.length; i++) {
221
    if (sorted[i] !== sorted[i - 1] + 1) return false
222
  }
223
  // Can't include 2 (rank 0) in straights
224
  if (sorted.includes(0)) return false
225
  return true
226
}
227
228
function get_card_value(card: Card, level: Rank): number {
229
  return get_rank_value(card.Rank, level)
230
}
231
232
function get_rank_value(rank: number, level: Rank): number {
233
  if (rank === 14) return 100 // red joker
234
  if (rank === 13) return 99  // black joker
235
  if (rank === level) return 98 // level card
236
  // 2 is highest non-special
237
  if (rank === 0) return 15
238
  // A is second highest
239
  if (rank === 12) return 14
240
  // rest are 3-K (ranks 1-11)
241
  return rank + 2
242
}
243
244
// Find valid plays that can beat the current table
245
export function find_valid_plays(
246
  hand: Card[],
247
  table_combo: Detected_Combo | null,
248
  level: Rank
249
): Card[][] {
250
  const suggestions: Card[][] = []
251
252
  if (!table_combo) {
253
    // Can play anything - suggest singles, pairs, triples
254
    const by_rank = new Map<number, Card[]>()
255
    hand.forEach(c => {
256
      const arr = by_rank.get(c.Rank) || []
257
      arr.push(c)
258
      by_rank.set(c.Rank, arr)
259
    })
260
261
    // Suggest lowest single
262
    const sorted = [...hand].sort((a, b) => get_card_value(a, level) - get_card_value(b, level))
263
    if (sorted.length > 0) {
264
      suggestions.push([sorted[0]])
265
    }
266
267
    // Suggest lowest pair
268
    for (const [_, cards] of by_rank) {
269
      if (cards.length >= 2) {
270
        suggestions.push(cards.slice(0, 2))
271
        break
272
      }
273
    }
274
275
    return suggestions.slice(0, 3)
276
  }
277
278
  // Need to beat the table combo
279
  const by_rank = new Map<number, Card[]>()
280
  hand.forEach(c => {
281
    const arr = by_rank.get(c.Rank) || []
282
    arr.push(c)
283
    by_rank.set(c.Rank, arr)
284
  })
285
286
  switch (table_combo.type) {
287
    case 'single': {
288
      const candidates = hand.filter(c => get_card_value(c, level) > table_combo.value)
289
        .sort((a, b) => get_card_value(a, level) - get_card_value(b, level))
290
      if (candidates.length > 0) {
291
        suggestions.push([candidates[0]])
292
      }
293
      break
294
    }
295
    case 'pair': {
296
      for (const [rank, cards] of by_rank) {
297
        if (cards.length >= 2 && get_rank_value(rank, level) > table_combo.value) {
298
          suggestions.push(cards.slice(0, 2))
299
          if (suggestions.length >= 2) break
300
        }
301
      }
302
      break
303
    }
304
    case 'triple': {
305
      for (const [rank, cards] of by_rank) {
306
        if (cards.length >= 3 && get_rank_value(rank, level) > table_combo.value) {
307
          suggestions.push(cards.slice(0, 3))
308
          if (suggestions.length >= 2) break
309
        }
310
      }
311
      break
312
    }
313
    default:
314
      // For complex combos, just return empty for now
315
      break
316
  }
317
318
  // Always suggest bombs as alternatives
319
  for (const [_, cards] of by_rank) {
320
    if (cards.length >= 4) {
321
      suggestions.push(cards)
322
    }
323
  }
324
325
  // Joker bomb
326
  const jokers = hand.filter(c => c.Rank === 13 || c.Rank === 14)
327
  if (jokers.length === 4) {
328
    suggestions.push(jokers)
329
  }
330
331
  return suggestions.slice(0, 3)
332
}
333
334
// Quick select helpers
335
export function select_pair(hand: Card[], level: Rank): Card[] | null {
336
  const by_rank = new Map<number, Card[]>()
337
  hand.forEach(c => {
338
    const arr = by_rank.get(c.Rank) || []
339
    arr.push(c)
340
    by_rank.set(c.Rank, arr)
341
  })
342
343
  // Find lowest pair
344
  const sorted_ranks = Array.from(by_rank.keys())
345
    .sort((a, b) => get_rank_value(a, level) - get_rank_value(b, level))
346
347
  for (const rank of sorted_ranks) {
348
    const cards = by_rank.get(rank)!
349
    if (cards.length >= 2) {
350
      return cards.slice(0, 2)
351
    }
352
  }
353
  return null
354
}
355
356
export function select_triple(hand: Card[], level: Rank): Card[] | null {
357
  const by_rank = new Map<number, Card[]>()
358
  hand.forEach(c => {
359
    const arr = by_rank.get(c.Rank) || []
360
    arr.push(c)
361
    by_rank.set(c.Rank, arr)
362
  })
363
364
  const sorted_ranks = Array.from(by_rank.keys())
365
    .sort((a, b) => get_rank_value(a, level) - get_rank_value(b, level))
366
367
  for (const rank of sorted_ranks) {
368
    const cards = by_rank.get(rank)!
369
    if (cards.length >= 3) {
370
      return cards.slice(0, 3)
371
    }
372
  }
373
  return null
374
}
375
376
export function select_bomb(hand: Card[], level: Rank): Card[] | null {
377
  const by_rank = new Map<number, Card[]>()
378
  hand.forEach(c => {
379
    const arr = by_rank.get(c.Rank) || []
380
    arr.push(c)
381
    by_rank.set(c.Rank, arr)
382
  })
383
384
  // Joker bomb first
385
  const jokers = hand.filter(c => c.Rank === 13 || c.Rank === 14)
386
  if (jokers.length === 4) {
387
    return jokers
388
  }
389
390
  const sorted_ranks = Array.from(by_rank.keys())
391
    .sort((a, b) => get_rank_value(a, level) - get_rank_value(b, level))
392
393
  for (const rank of sorted_ranks) {
394
    const cards = by_rank.get(rank)!
395
    if (cards.length >= 4) {
396
      return cards
397
    }
398
  }
399
  return null
400
}