6 Commits

Author SHA1 Message Date
8afb65612f Better button functionality, timer on console 2025-08-12 17:20:55 -04:00
ad989978e3 added console prints 2025-08-12 17:13:07 -04:00
c0014aa7d9 functioning start stop button 2025-08-12 08:31:38 -04:00
79c92c23aa testing button functionality 2025-08-11 20:11:32 -04:00
66f64510f8 displays team scores on LCD 2025-08-09 16:57:56 -04:00
2bc9a28162 Working display on the LCD 2025-08-09 16:50:37 -04:00
3 changed files with 159 additions and 99 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
lcd-env/

View File

@@ -0,0 +1,2 @@
start the virtual env with
source lcd-env/bin/activate

View File

@@ -1,134 +1,191 @@
#!/usr/bin/env python3
"""
Catchphrase
Author: Jerick Oates
"""
import random
import RPi.GPIO as GPIO
import time
import random
import threading
import sys
from RPLCD.i2c import CharLCD
# ---------- CONFIG ----------
WORDS_FILE = "words.txt" # file with one word per line
TIMER_SECONDS = 30 # how long the timer runs
# --- CONFIG ---
WORDS_FILE = "words.txt"
TIMER_SECONDS = 30
I2C_ADDRESS = 0x27
I2C_CHIP = 'PCF8574'
# --- GPIO SETUP ---
GPIO.setmode(GPIO.BCM)
# ---------- HELPERS ----------
# Buttons mapped to pins
BUTTON_START_STOP = 23 # Green button
BUTTON_NEXT = 27
BUTTON_TEAM1 = 22
BUTTON_TEAM2 = 17
for btn_pin in (BUTTON_START_STOP, BUTTON_NEXT, BUTTON_TEAM1, BUTTON_TEAM2):
GPIO.setup(btn_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
# --- LCD SETUP ---
lcd = CharLCD(I2C_CHIP, I2C_ADDRESS)
lcd.clear()
# --- GLOBALS ---
words = []
timer_thread = None
timer_running = False
time_left = TIMER_SECONDS
score_t1 = 0
score_t2 = 0
current_word = ""
lock = threading.Lock()
# --- LOAD WORDS ---
def load_words(path=WORDS_FILE):
"""Return a list of non-empty stripped lines from the given file."""
try:
with open(path, "r", encoding="utf-8") as f:
words = [line.strip() for line in f if line.strip()]
if not words:
lines = [line.strip() for line in f if line.strip()]
if not lines:
raise ValueError("Word file is empty.")
return words
return lines
except Exception as e:
print(f"❌ Error loading words: {e}")
sys.exit(1)
exit(1)
# --- LCD HELPERS ---
def lcd_print_lines(line1="", line2=""):
lcd.clear()
lcd.cursor_pos = (0, 0)
lcd.write_string(line1.ljust(16)[:16])
lcd.cursor_pos = (1, 0)
lcd.write_string(line2.ljust(16)[:16])
print(line1)
print(line2)
def pick_random(words):
"""Return a random word from the list."""
return random.choice(words)
def lcd_show_word_and_timer(word, seconds):
lcd.clear()
lcd.cursor_pos = (0, 0)
lcd.write_string(f"Time:{seconds:02d} ")
lcd.cursor_pos = (1, 0)
lcd.write_string(word.center(16))
print(f"Time:{seconds:02d}")
print(word)
def lcd_show_scores():
msg = f"T1:{score_t1} T2:{score_t2}".center(16)
lcd.clear()
lcd.write_string(msg)
print(msg)
# ---------- TIMER THREAD ----------
def lcd_show_winner(winner_team):
msg = f"🏆 Team {winner_team} Wins!".center(16)
lcd.clear()
lcd.write_string(msg)
print(msg)
# --- TIMER THREAD ---
class CountdownTimer(threading.Thread):
"""
Background thread that counts down for TIMER_SECONDS.
When it finishes, it calls an optional callback.
"""
def __init__(self, seconds=TIMER_SECONDS, on_finish=None):
def __init__(self, seconds):
super().__init__()
self.seconds = seconds
self.on_finish = on_finish # function to call when timer ends
self._running = threading.Event()
self._running.set()
def run(self):
for _ in range(self.seconds):
if not self._running.is_set():
break # stopped early by user
global time_left, timer_running
time_left = self.seconds
while time_left > 0 and self._running.is_set():
with lock:
lcd_show_word_and_timer(current_word, time_left)
time.sleep(1)
# Timer finished (or was stopped)
self._running.clear()
if self.on_finish:
self.on_finish() # announce completion
time_left -= 1
with lock:
timer_running = False
lcd_show_scores()
def stop(self):
"""Stop the timer before it reaches zero."""
self._running.clear()
@property
def is_running(self):
return self._running.is_set()
# --- GAME LOGIC ---
def pick_random_word():
global current_word
current_word = random.choice(words)
# ---------- MAIN LOOP ----------
def main():
words = load_words()
timer_thread = None
print("=== Catchphrase, but Better ===")
print("Commands:")
print(" start - begin the 30-second timer and show a word immediately")
print(" next - show another random word while the timer is running")
print(" stop - cancel the timer early")
print(" exit / quit - leave the program")
# Helper that prints when the timer ends
def announce_finish():
print("\n⏰ Time is up! Returning to menu.")
print("=== Catchphrase ===")
print("Commands:")
print(" start - begin the 30-second timer and show a word immediately")
print(" next - show another random word while the timer is running")
print(" stop - cancel the timer early")
print(" exit / quit - leave the program")
while True:
try:
cmd = input("\n> ").strip().lower()
except EOFError:
break
if cmd in ("exit", "quit"):
if timer_thread and timer_thread.is_running:
def start_stop_button_callback(channel):
global timer_running, timer_thread
with lock:
if timer_running:
# Stop timer
if timer_thread:
timer_thread.stop()
timer_thread.join()
print("👋 Goodbye!")
break
elif cmd == "start":
if timer_thread and timer_thread.is_running:
print("[!] Timer is already running.")
timer_running = False
lcd_show_scores()
else:
# Start a new timer with the finish callback
timer_thread = CountdownTimer(on_finish=announce_finish)
timer_thread.daemon = True # exit when main thread exits
# Start timer & show first word
pick_random_word()
timer_thread = CountdownTimer(TIMER_SECONDS)
timer_thread.daemon = True
timer_running = True
timer_thread.start()
# Show the first word right away
print(pick_random(words))
print(f"[+] 30-second timer started. ({TIMER_SECONDS}s)")
def next_button_callback(channel):
global current_word
with lock:
if timer_running: # Only works while timer runs
pick_random_word()
elif cmd == "next":
if not (timer_thread and timer_thread.is_running):
print("[!] No active timer - use 'start' first.")
def team1_button_callback(channel):
global score_t1
with lock:
if not timer_running: # Only works when timer stopped
score_t1 += 1
if score_t1 >= 7:
lcd_show_winner(1)
reset_game_after_delay()
else:
print(pick_random(words))
lcd_show_scores()
elif cmd == "stop":
if timer_thread and timer_thread.is_running:
timer_thread.stop()
timer_thread.join()
print("[+] Timer stopped early.")
def team2_button_callback(channel):
global score_t2
with lock:
if not timer_running: # Only works when timer stopped
score_t2 += 1
if score_t2 >= 7:
lcd_show_winner(2)
reset_game_after_delay()
else:
print("[!] There is no running timer to stop.")
lcd_show_scores()
else:
print(f"[!] Unknown command: {cmd}")
def reset_game_after_delay(delay=5):
def reset():
global score_t1, score_t2, current_word
time.sleep(delay)
with lock:
score_t1 = 0
score_t2 = 0
current_word = ""
lcd_print_lines("Catchphrase started!", "Press Green to Start")
threading.Thread(target=reset, daemon=True).start()
# --- Setup GPIO event detection ---
GPIO.add_event_detect(BUTTON_START_STOP, GPIO.FALLING, callback=start_stop_button_callback, bouncetime=300)
GPIO.add_event_detect(BUTTON_NEXT, GPIO.FALLING, callback=next_button_callback, bouncetime=300)
GPIO.add_event_detect(BUTTON_TEAM1, GPIO.FALLING, callback=team1_button_callback, bouncetime=300)
GPIO.add_event_detect(BUTTON_TEAM2, GPIO.FALLING, callback=team2_button_callback, bouncetime=300)
# --- Main program ---
def main():
global words
words = load_words()
lcd_print_lines("Catchphrase started!", "Press Green to Start")
try:
while True:
time.sleep(0.1)
except KeyboardInterrupt:
pass
finally:
GPIO.cleanup()
lcd.clear()
if __name__ == "__main__":
main()