Last active
June 24, 2025 19:21
-
-
Save st1vms/547c57b23e4d9baed118de10ebbf5a51 to your computer and use it in GitHub Desktop.
Card Counter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Requirements: pip install keyboard matplotlib | |
import keyboard | |
import matplotlib.pyplot as plt | |
from matplotlib.animation import FuncAnimation | |
from threading import Thread | |
# Number of decks in use | |
NUM_DECKS = 8 | |
# Mapping of keyboard keys to card values | |
KEY_MAP = { | |
"1": "A", # Subjective mapping | |
"2": "2", | |
"3": "3", | |
"4": "4", | |
"5": "5", | |
"6": "6", | |
"7": "7", | |
"8": "8", | |
"9": "9", | |
"0": "10", # Subjective mapping | |
"/": "J", # Subjective mapping | |
"*": "Q", # Subjective mapping | |
"-": "K", # Subjective mapping | |
} | |
# Initial count of each card in an 8-deck shoe | |
INITIAL_CARDS = { | |
"A": 4 * NUM_DECKS, | |
"2": 4 * NUM_DECKS, | |
"3": 4 * NUM_DECKS, | |
"4": 4 * NUM_DECKS, | |
"5": 4 * NUM_DECKS, | |
"6": 4 * NUM_DECKS, | |
"7": 4 * NUM_DECKS, | |
"8": 4 * NUM_DECKS, | |
"9": 4 * NUM_DECKS, | |
"10": 4 * NUM_DECKS, | |
"J": 4 * NUM_DECKS, | |
"Q": 4 * NUM_DECKS, | |
"K": 4 * NUM_DECKS, | |
} | |
# Runtime state | |
REMAINING_CARDS = INITIAL_CARDS.copy() # Mutable copy for tracking usage | |
CARD_LOG = [] # Ordered log of input cards | |
LABELS = list(INITIAL_CARDS.keys()) # For plotting | |
LAST_CARD_TEXT = None # Used to store the matplotlib text object | |
def calculate_hi_lo(): | |
""" | |
Calculate the Hi-Lo running and true count based on the card log. | |
""" | |
running = 0 | |
# Hi-Lo count system | |
for card in CARD_LOG: | |
if card in ["A", "2", "3", "4", "5", "6"]: | |
running -= 1 | |
elif card in ["10", "J", "Q", "K"]: | |
running += 1 | |
total_cards_left = sum(REMAINING_CARDS.values()) | |
decks_remaining = max(total_cards_left / 52, 0.01) # Avoid division by zero | |
true_count = running / decks_remaining | |
return running, round(true_count, 2) | |
def update(_frame): | |
""" | |
Update the bar plot and overlayed text each frame. | |
""" | |
global LAST_CARD_TEXT | |
plt.cla() # Clear current axes | |
# Bar chart values | |
values = [REMAINING_CARDS[l] for l in LABELS] | |
bars = plt.bar(LABELS, values, color="dodgerblue", edgecolor="black") | |
# Title with current count values | |
running, true = calculate_hi_lo() | |
plt.title(f"RC: {running} | TC: {true}", pad=30, fontsize=20) | |
plt.xticks(fontsize=14) | |
max_y = max(INITIAL_CARDS.values()) | |
plt.yticks(range(0, max_y + 1, 4), fontsize=14) | |
plt.ylim(0, max_y) | |
# Display card probability over each bar | |
total_cards_left = sum(REMAINING_CARDS.values()) | |
for bar, label in zip(bars, LABELS): | |
prob = ( | |
(REMAINING_CARDS[label] / total_cards_left) * 100 | |
if total_cards_left > 0 | |
else 0 | |
) | |
plt.text( | |
bar.get_x() + bar.get_width() / 2, | |
bar.get_height(), | |
f"{prob:.1f}%", | |
ha="center", | |
va="bottom", | |
fontsize=12, | |
) | |
# Show the last 10 cards played | |
last_cards = CARD_LOG[-20:] | |
text_str = "Last 20 Cards: " + " ".join(last_cards) | |
if LAST_CARD_TEXT is not None: | |
LAST_CARD_TEXT.set_text(text_str) | |
else: | |
LAST_CARD_TEXT = plt.gcf().text(0.5, 0.02, text_str, ha="center", fontsize=18) | |
def listen_keys(): | |
""" | |
Listen for keypresses and update game state accordingly. | |
""" | |
while True: | |
event = keyboard.read_event() | |
if event.event_type == keyboard.KEY_DOWN: | |
key = event.name.lower() | |
if key in KEY_MAP: | |
label = KEY_MAP[key] | |
if REMAINING_CARDS[label] > 0: | |
CARD_LOG.append(label) | |
REMAINING_CARDS[label] -= 1 | |
# Allow undoing the last card entry | |
elif key == "backspace" and CARD_LOG: | |
last = CARD_LOG.pop() | |
REMAINING_CARDS[last] += 1 | |
def _main() -> None: | |
""" | |
Initialize plot and start key listener. | |
""" | |
fig = plt.figure(figsize=(10, 8)) | |
fig.canvas.manager.set_window_title("Card Counter") | |
# Disable matplotlib's default key handler | |
fig.canvas.mpl_disconnect(fig.canvas.manager.key_press_handler_id) | |
# Start animation for live updates | |
_ = FuncAnimation(fig, update, interval=200, cache_frame_data=False) | |
# Start key listening in a background thread | |
Thread(target=listen_keys, daemon=True).start() | |
plt.tight_layout() | |
plt.show() | |
if __name__ == "__main__": | |
_main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Requirements: pip install keyboard matplotlib | |
import keyboard | |
import matplotlib.pyplot as plt | |
from matplotlib.animation import FuncAnimation | |
from threading import Thread | |
# Number of decks in use | |
NUM_DECKS = 8 | |
# Mapping of keyboard keys to card values | |
KEY_MAP = { | |
"1": "A", # Subjective mapping | |
"2": "2", | |
"3": "3", | |
"4": "4", | |
"5": "5", | |
"6": "6", | |
"7": "7", | |
"8": "8", | |
"9": "9", | |
"0": "10", # Subjective mapping | |
"/": "J", # Subjective mapping | |
"*": "Q", # Subjective mapping | |
"-": "K", # Subjective mapping | |
} | |
# Initial count of each card in an 8-deck shoe | |
INITIAL_CARDS = { | |
"A": 4 * NUM_DECKS, | |
"2": 4 * NUM_DECKS, | |
"3": 4 * NUM_DECKS, | |
"4": 4 * NUM_DECKS, | |
"5": 4 * NUM_DECKS, | |
"6": 4 * NUM_DECKS, | |
"7": 4 * NUM_DECKS, | |
"8": 4 * NUM_DECKS, | |
"9": 4 * NUM_DECKS, | |
"10": 4 * NUM_DECKS, | |
"J": 4 * NUM_DECKS, | |
"Q": 4 * NUM_DECKS, | |
"K": 4 * NUM_DECKS, | |
} | |
# Runtime state | |
REMAINING_CARDS = INITIAL_CARDS.copy() # Mutable copy for tracking usage | |
CARD_LOG = [] # Ordered log of input cards | |
LABELS = list(INITIAL_CARDS.keys()) # For plotting | |
LAST_CARD_TEXT = None # Used to store the matplotlib text object | |
def calculate_hi_lo(): | |
""" | |
Calculate the Hi-Lo running and true count based on the card log. | |
""" | |
running = 0 | |
# Hi-Lo count system | |
for card in CARD_LOG: | |
if card in ["2", "3", "4", "5", "6"]: | |
running += 1 | |
elif card in ["10", "J", "Q", "K", "A"]: | |
running -= 1 | |
total_cards_left = sum(REMAINING_CARDS.values()) | |
decks_remaining = max(total_cards_left / 52, 0.01) # Avoid division by zero | |
true_count = running / decks_remaining | |
return running, round(true_count, 2) | |
def update(_frame): | |
""" | |
Update the bar plot and overlayed text each frame. | |
""" | |
global LAST_CARD_TEXT | |
plt.cla() # Clear current axes | |
# Bar chart values | |
values = [REMAINING_CARDS[l] for l in LABELS] | |
bars = plt.bar(LABELS, values, color="dodgerblue", edgecolor="black") | |
# Title with current count values | |
running, true = calculate_hi_lo() | |
plt.title(f"RC: {running} | TC: {true}", pad=30, fontsize=20) | |
plt.xticks(fontsize=14) | |
max_y = max(INITIAL_CARDS.values()) | |
plt.yticks(range(0, max_y + 1, 4), fontsize=14) | |
plt.ylim(0, max_y) | |
# Display card probability over each bar | |
total_cards_left = sum(REMAINING_CARDS.values()) | |
for bar, label in zip(bars, LABELS): | |
prob = ( | |
(REMAINING_CARDS[label] / total_cards_left) * 100 | |
if total_cards_left > 0 | |
else 0 | |
) | |
plt.text( | |
bar.get_x() + bar.get_width() / 2, | |
bar.get_height(), | |
f"{prob:.1f}%", | |
ha="center", | |
va="bottom", | |
fontsize=12, | |
) | |
# Show the last 10 cards played | |
last_cards = CARD_LOG[-20:] | |
text_str = "Last 20 Cards: " + " ".join(last_cards) | |
if LAST_CARD_TEXT is not None: | |
LAST_CARD_TEXT.set_text(text_str) | |
else: | |
LAST_CARD_TEXT = plt.gcf().text(0.5, 0.02, text_str, ha="center", fontsize=18) | |
def listen_keys(): | |
""" | |
Listen for keypresses and update game state accordingly. | |
""" | |
while True: | |
event = keyboard.read_event() | |
if event.event_type == keyboard.KEY_DOWN: | |
key = event.name.lower() | |
if key in KEY_MAP: | |
label = KEY_MAP[key] | |
if REMAINING_CARDS[label] > 0: | |
CARD_LOG.append(label) | |
REMAINING_CARDS[label] -= 1 | |
# Allow undoing the last card entry | |
elif key == "backspace" and CARD_LOG: | |
last = CARD_LOG.pop() | |
REMAINING_CARDS[last] += 1 | |
def _main() -> None: | |
""" | |
Initialize plot and start key listener. | |
""" | |
fig = plt.figure(figsize=(10, 8)) | |
fig.canvas.manager.set_window_title("Card Counter") | |
# Disable matplotlib's default key handler | |
fig.canvas.mpl_disconnect(fig.canvas.manager.key_press_handler_id) | |
# Start animation for live updates | |
_ = FuncAnimation(fig, update, interval=200, cache_frame_data=False) | |
# Start key listening in a background thread | |
Thread(target=listen_keys, daemon=True).start() | |
plt.tight_layout() | |
plt.show() | |
if __name__ == "__main__": | |
_main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment