Typing "AH" into a two-character box is fine at a desk and miserable at a table: it needs a keyboard, it accepts nonsense like "ZZ", and it happily deals the same card to two players. The two card fields become face-up card slots. Tapping one opens a 52-card grid — four suit columns, thirteen rank rows, one tap per card — and picking the first card moves straight on to the second, because a dealer picking hole cards is always picking two. Cards already dealt elsewhere at the table are greyed out and say who has them, so a duplicate is visibly out of play rather than silently accepted. Cards are still stored as the same two plain characters everything has always read; the suit only becomes a symbol on its way to a human. That formatting is now shared, so the table pucks, the seat editor, the pot wizard, the hand history and the CSV all say A♥ K♦ rather than three of them saying AH KD. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
132 lines
4.8 KiB
JavaScript
132 lines
4.8 KiB
JavaScript
// Tap a card instead of typing it.
|
|
//
|
|
// Typing "AH" into a two-character box is fine at a desk and miserable at a table:
|
|
// it needs a keyboard, it accepts nonsense like "ZZ", and it happily lets the same
|
|
// card land in two players' hands. The picker is one tap per card, greys out cards
|
|
// already dealt somewhere else at the table, and stores exactly the same two-character
|
|
// string the rest of the tool has always read.
|
|
const CardPicker = {
|
|
RANKS: ["A", "K", "Q", "J", "T", "9", "8", "7", "6", "5", "4", "3", "2"],
|
|
SUITS: [
|
|
{ code: "S", symbol: "♠", red: false },
|
|
{ code: "H", symbol: "♥", red: true },
|
|
{ code: "D", symbol: "♦", red: true },
|
|
{ code: "C", symbol: "♣", red: false },
|
|
],
|
|
|
|
state: null,
|
|
hooks: null,
|
|
seatIndex: null,
|
|
cardIndex: 0,
|
|
|
|
open(state, hooks, seatIndex, cardIndex) {
|
|
this.state = state;
|
|
this.hooks = hooks;
|
|
this.seatIndex = seatIndex;
|
|
this.cardIndex = cardIndex;
|
|
this.render();
|
|
document.getElementById("cardPickerOverlay").hidden = false;
|
|
},
|
|
|
|
close() {
|
|
document.getElementById("cardPickerOverlay").hidden = true;
|
|
},
|
|
|
|
// Every card sitting in front of anyone else. The slot being edited is excluded,
|
|
// so re-picking the card that's already there isn't treated as a duplicate.
|
|
cardsInUse() {
|
|
const used = new Map();
|
|
this.state.seats.forEach((player, seat) => {
|
|
(player.cards || []).forEach((card, slot) => {
|
|
if (!card) return;
|
|
if (seat === this.seatIndex && slot === this.cardIndex) return;
|
|
used.set(card.toUpperCase(), HoldemState.displayName(player, seat));
|
|
});
|
|
});
|
|
return used;
|
|
},
|
|
|
|
assign(card) {
|
|
const player = this.state.seats[this.seatIndex];
|
|
player.cards[this.cardIndex] = card;
|
|
this.hooks.onPick(this.seatIndex, this.cardIndex, card);
|
|
|
|
// Straight on to the second card — a dealer picking hole cards is always
|
|
// picking two, and making them re-open the picker doubles the taps.
|
|
if (this.cardIndex === 0 && !player.cards[1]) {
|
|
this.cardIndex = 1;
|
|
this.render();
|
|
return;
|
|
}
|
|
this.close();
|
|
},
|
|
|
|
clearSlot() {
|
|
const player = this.state.seats[this.seatIndex];
|
|
player.cards[this.cardIndex] = "";
|
|
this.hooks.onPick(this.seatIndex, this.cardIndex, "");
|
|
this.render();
|
|
},
|
|
|
|
render() {
|
|
const player = this.state.seats[this.seatIndex];
|
|
const used = this.cardsInUse();
|
|
|
|
document.getElementById("cardPickerTitle").textContent =
|
|
`${HoldemState.displayName(player, this.seatIndex)} — card ${this.cardIndex + 1}`;
|
|
|
|
// Which slot the next tap fills, switchable without closing the sheet.
|
|
const slots = document.getElementById("cardPickerSlots");
|
|
slots.innerHTML = "";
|
|
[0, 1].forEach(slot => {
|
|
const btn = document.createElement("button");
|
|
btn.type = "button";
|
|
btn.className = "pickerSlot";
|
|
btn.classList.toggle("active", slot === this.cardIndex);
|
|
const value = player.cards[slot] || "";
|
|
btn.textContent = value || "—";
|
|
btn.classList.toggle("cardRed", /[HD]$/i.test(value));
|
|
btn.addEventListener("click", () => {
|
|
this.cardIndex = slot;
|
|
this.render();
|
|
});
|
|
slots.appendChild(btn);
|
|
});
|
|
|
|
const grid = document.getElementById("cardPickerGrid");
|
|
grid.innerHTML = "";
|
|
this.RANKS.forEach(rank => {
|
|
this.SUITS.forEach(suit => {
|
|
const card = `${rank}${suit.code}`;
|
|
const takenBy = used.get(card);
|
|
|
|
const btn = document.createElement("button");
|
|
btn.type = "button";
|
|
btn.className = "pickerCard";
|
|
btn.classList.toggle("cardRed", suit.red);
|
|
btn.disabled = !!takenBy;
|
|
if (takenBy) btn.title = `Already dealt to ${takenBy}`;
|
|
if ((player.cards[this.cardIndex] || "").toUpperCase() === card) {
|
|
btn.classList.add("selected");
|
|
}
|
|
|
|
const rankEl = document.createElement("span");
|
|
rankEl.className = "pickerRank";
|
|
rankEl.textContent = rank;
|
|
const suitEl = document.createElement("span");
|
|
suitEl.className = "pickerSuit";
|
|
suitEl.textContent = suit.symbol;
|
|
btn.appendChild(rankEl);
|
|
btn.appendChild(suitEl);
|
|
|
|
btn.addEventListener("click", () => this.assign(card));
|
|
grid.appendChild(btn);
|
|
});
|
|
});
|
|
},
|
|
};
|
|
|
|
if (typeof module !== "undefined" && module.exports) {
|
|
module.exports = CardPicker;
|
|
}
|