Spaces:
Sleeping
Sleeping
Download app.py from bsant576/crack_the_code: direct link, hf CLI and curl.
- Browser
- Download file 14 kB
-
https://huggingface.co/spaces/bsant576/crack_the_code/resolve/main/app.py
- Command line
-
hf download hf://spaces/bsant576/crack_the_code/app.py
-
curl -L -o app.py https://huggingface.co/spaces/bsant576/crack_the_code/resolve/main/app.py
14 kB
| import random | |
| import threading | |
| import time | |
| import gradio as gr | |
| # ZeroGPU Spaces require at least one @spaces.GPU function at startup. This game needs no | |
| # GPU, so this is a do-nothing placeholder. It is skipped when `spaces` isn't installed | |
| # (local runs and regular CPU hardware). | |
| try: | |
| import spaces | |
| def _gpu_placeholder(): | |
| return None | |
| except ImportError: | |
| pass | |
| MAX_MOVES = 10 | |
| ROOM_TTL_SECONDS = 24 * 60 * 60 | |
| # No I, O, 0 or 1 so room codes are easy to read aloud and type on a phone. | |
| ROOM_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" | |
| DIGITS = list("123456789") | |
| # ---------- shared room store (one per running server, shared by every visitor) ---------- | |
| _LOCK = threading.Lock() | |
| _ROOMS = {} # code -> {"secret": str, "created": float} | |
| def create_room(secret: str) -> str: | |
| with _LOCK: | |
| now = time.time() | |
| for code in [c for c, r in _ROOMS.items() if now - r["created"] > ROOM_TTL_SECONDS]: | |
| del _ROOMS[code] | |
| while True: | |
| code = "".join(random.choices(ROOM_ALPHABET, k=4)) | |
| if code not in _ROOMS: | |
| break | |
| _ROOMS[code] = {"secret": secret, "created": now} | |
| return code | |
| def lookup_room(code: str): | |
| with _LOCK: | |
| room = _ROOMS.get(code.strip().upper()) | |
| if room and time.time() - room["created"] <= ROOM_TTL_SECONDS: | |
| return room["secret"] | |
| return None | |
| # ---------- game logic ---------- | |
| def new_code() -> str: | |
| """4 unique digits, 1-9 (no zeroes, no repeats).""" | |
| return "".join(random.sample("123456789", 4)) | |
| def score(secret: str, guess: str): | |
| """Return (right_place, right_number). Digits are unique, so a set overlap works.""" | |
| right_place = sum(s == g for s, g in zip(secret, guess)) | |
| right_number = len(set(secret) & set(guess)) | |
| return right_place, right_number | |
| def validate(guess: str, previous=()): | |
| if len(guess) != 4: | |
| return "You do not have the correct character amount (need exactly 4)." | |
| if not guess.isdigit() or "0" in guess: | |
| return "There are no letters or zeroes in the code." | |
| if len(set(guess)) != 4: | |
| return "There are no repeats in the code." | |
| if guess in previous: | |
| return "You already guessed that." | |
| return None | |
| def win_message(move: int) -> str: | |
| if move == 1: | |
| return "Wow, first try, that was really lucky." | |
| if move < 5: | |
| return "Wow, you actually guessed it. Good job!!!" | |
| if move < 8: | |
| return "Cutting it a little close, but good job." | |
| return "Wow, nice guess but almost too late. Good job!" | |
| def hint_message(right_number: int) -> str: | |
| if right_number == 0: | |
| return "Man, that wasn't even close." | |
| if right_number <= 2: | |
| return "You are doing well, nice guess. JK loser. HAHAHAHAHA" | |
| return "You are almost there. Don't let me win. HAHAHAHAHAHA" | |
| # ---------- per-visitor state and rendering ---------- | |
| def fresh_state(): | |
| return {"mode": "menu", "secret": "", "room_code": "", "history": [], "status": "playing"} | |
| def game_state(mode, secret, room_code=""): | |
| return {"mode": mode, "secret": secret, "room_code": room_code, "history": [], "status": "playing"} | |
| def history_table(history) -> str: | |
| """Guess history as a Markdown table (fixed layout, so it can't stretch on phones).""" | |
| if not history: | |
| return "" | |
| lines = [ | |
| "**Previous guesses**", | |
| "", | |
| "| Move | Guess | Right place | Right number |", | |
| "|:---:|:---:|:---:|:---:|", | |
| ] | |
| for i, (g, rp, rn) in enumerate(history): | |
| lines.append(f"| {i + 1} | `{g}` | {rp} | {rn} |") | |
| return "\n".join(lines) | |
| def view(s, msg="", reset_marks=False): | |
| """Updates for VIEW_OUTPUTS, in order.""" | |
| in_game = s["mode"] != "menu" | |
| over = s["status"] != "playing" | |
| used = len(s["history"]) | |
| if s["mode"] == "room": | |
| badge = f"Room **{s['room_code']}** Β· a friend set this code" | |
| else: | |
| badge = "Solo game Β· random code" | |
| if over: | |
| header = "### Game over" | |
| else: | |
| header = f"### Move {used + 1} of {MAX_MOVES}\n{MAX_MOVES - used} left" | |
| marks = gr.update(visible=in_game and not over) | |
| if reset_marks: | |
| marks = gr.update(visible=in_game and not over, value=DIGITS) | |
| history_md = history_table(s["history"]) | |
| again_label = "π Try this code again" if s["mode"] == "room" else "π New game" | |
| return [ | |
| s, | |
| gr.update(visible=not in_game), # menu | |
| gr.update(visible=in_game), # game | |
| badge, | |
| header, | |
| marks, | |
| gr.update(visible=in_game and not over), # guess row | |
| msg, | |
| gr.update(value=history_md, visible=used > 0), | |
| gr.update(visible=over or used > 0, value=again_label), | |
| "", # clear the guess box | |
| ] | |
| # ---------- event handlers ---------- | |
| def on_start_solo(): | |
| return view(game_state("solo", new_code()), reset_marks=True) | |
| def on_create_room(secret_in): | |
| secret_in = (secret_in or "").strip() | |
| error = validate(secret_in) | |
| if error: | |
| return f"**{error}**", "" | |
| code = create_room(secret_in) | |
| return ( | |
| f"Room created. Send this room code to your friend:\n\n## `{code}`\n\n" | |
| "They open this app, choose **Join a game**, and enter it. " | |
| "Rooms are cleared after 24 hours or when the app restarts." | |
| ), "" | |
| def on_join(room_in): | |
| code = (room_in or "").strip().upper() | |
| if len(code) != 4: | |
| return view(fresh_state()) + ["**Room codes have 4 characters.**", ""] | |
| secret = lookup_room(code) | |
| if secret is None: | |
| return view(fresh_state()) + [ | |
| "**Room not found.** Check the code, or ask your friend to set a new one " | |
| "(rooms are cleared when the app restarts).", | |
| "", | |
| ] | |
| return view(game_state("room", secret, code), reset_marks=True) + ["", ""] | |
| def on_guess(s, guess_in): | |
| if s["mode"] == "menu" or s["status"] != "playing": | |
| return view(s) | |
| guess = (guess_in or "").strip() | |
| error = validate(guess, [h[0] for h in s["history"]]) | |
| if error: | |
| return view(s, f"**{error}**") | |
| right_place, right_number = score(s["secret"], guess) | |
| s = {**s, "history": s["history"] + [(guess, right_place, right_number)]} | |
| used = len(s["history"]) | |
| if guess == s["secret"]: | |
| s["status"] = "won" | |
| msg = ( | |
| f"π **{win_message(used)}**\n\nThe code was **{s['secret']}**. " | |
| f"You cracked it in {used} move(s)." | |
| ) | |
| elif used >= MAX_MOVES: | |
| s["status"] = "lost" | |
| msg = f"**Out of moves!** The code was **{s['secret']}**. Better luck next time." | |
| else: | |
| msg = hint_message(right_number) | |
| return view(s, msg) | |
| def on_again(s): | |
| if s["mode"] == "room": | |
| return view(game_state("room", s["secret"], s["room_code"]), reset_marks=True) | |
| return view(game_state("solo", new_code()), reset_marks=True) | |
| def on_menu(s): | |
| return view(fresh_state()) | |
| # ---------- UI ---------- | |
| # Mobile first: everything is a single narrow column. On wider screens the page grows and the | |
| # game splits into two panes (controls on the left, feedback and history on the right). | |
| CSS = """ | |
| .gradio-container { | |
| max-width: 32rem !important; | |
| margin: 0 auto !important; | |
| padding-left: 0.75rem !important; | |
| padding-right: 0.75rem !important; | |
| } | |
| #menu { max-width: 32rem; margin: 0 auto; } | |
| #history table { width: 100%; table-layout: fixed; } | |
| #history th, #history td { text-align: center; word-break: break-word; } | |
| #marks label { min-width: 2.6rem; justify-content: center; } | |
| /* Inputs at 16px+ so iOS Safari does not zoom the page when a field is focused. */ | |
| #guess_in input, #guess_in textarea, | |
| #room_in input, #room_in textarea, | |
| #secret_in input, #secret_in textarea { | |
| font-size: 1.25rem !important; | |
| letter-spacing: 0.25em; | |
| text-align: center; | |
| } | |
| #room_in input, #room_in textarea { text-transform: uppercase; } | |
| /* Touch screens: bigger tap targets. */ | |
| @media (pointer: coarse) { | |
| button { min-height: 3rem; font-size: 1.05rem !important; } | |
| #marks label { min-width: 3rem; min-height: 3rem; font-size: 1.1rem; } | |
| #marks .wrap { gap: 0.4rem; } | |
| } | |
| /* Small phones: tighter spacing, smaller table text. */ | |
| @media (max-width: 480px) { | |
| .gradio-container { padding-left: 0.5rem !important; padding-right: 0.5rem !important; } | |
| h1 { font-size: 1.6rem !important; } | |
| #history table { font-size: 0.8rem; } | |
| #history th, #history td { padding: 0.3rem 0.2rem; } | |
| } | |
| /* Laptops and desktops: use the extra width. */ | |
| @media (min-width: 900px) { | |
| .gradio-container { max-width: 60rem !important; } | |
| #game_panes { gap: 1.5rem; } | |
| #marks label { min-width: 2.4rem; } | |
| } | |
| """ | |
| # Show the numeric keypad on phones for the digit and room-code fields, and turn off | |
| # autocorrect/autocomplete so the browser does not interfere with guesses. | |
| HEAD = """ | |
| <script> | |
| (function () { | |
| const FIELDS = { | |
| guess_in: { inputmode: "numeric", pattern: "[1-9]*" }, | |
| secret_in: { inputmode: "numeric", pattern: "[1-9]*" }, | |
| room_in: { inputmode: "text", autocapitalize: "characters" }, | |
| }; | |
| function apply() { | |
| for (const [id, attrs] of Object.entries(FIELDS)) { | |
| const el = document.querySelector("#" + id + " input, #" + id + " textarea"); | |
| if (!el) continue; | |
| el.setAttribute("autocomplete", "off"); | |
| el.setAttribute("autocorrect", "off"); | |
| el.setAttribute("spellcheck", "false"); | |
| for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v); | |
| } | |
| } | |
| new MutationObserver(apply).observe(document.documentElement, { childList: true, subtree: true }); | |
| window.addEventListener("load", apply); | |
| })(); | |
| </script> | |
| """ | |
| with gr.Blocks(title="Crack the Code") as demo: | |
| state = gr.State(fresh_state()) | |
| gr.Markdown("# π Crack the Code") | |
| # ----- menu ----- | |
| with gr.Column(visible=True, elem_id="menu") as menu_col: | |
| gr.Markdown( | |
| "Guess a secret 4-digit code in **10 moves**. No repeats and no zeroes. " | |
| "Play alone, or set a code for a friend." | |
| ) | |
| with gr.Tabs(): | |
| with gr.Tab("Play solo"): | |
| gr.Markdown("I'll make up a random code.") | |
| solo_btn = gr.Button("Start", variant="primary") | |
| with gr.Tab("Set a code"): | |
| gr.Markdown( | |
| "Pick a secret code for a friend to guess. You'll get a room code to send them. " | |
| "They can join from any device." | |
| ) | |
| secret_in = gr.Textbox( | |
| label="Your secret code", | |
| type="password", | |
| max_length=4, | |
| placeholder="4 digits, 1-9, no repeats", | |
| elem_id="secret_in", | |
| ) | |
| create_btn = gr.Button("Create room", variant="primary") | |
| set_out = gr.Markdown() | |
| with gr.Tab("Join a game"): | |
| gr.Markdown("Enter the room code your friend sent you.") | |
| room_in = gr.Textbox( | |
| label="Room code", max_length=4, placeholder="e.g. KQ7M", elem_id="room_in" | |
| ) | |
| join_btn = gr.Button("Join", variant="primary") | |
| join_out = gr.Markdown() | |
| # ----- game ----- | |
| with gr.Column(visible=False) as game_col: | |
| badge = gr.Markdown() | |
| gr.Markdown( | |
| "The code has 4 digits: **no repeats and no zeroes**. " | |
| f"Guess it in **{MAX_MOVES} moves** or you lose. " | |
| "Good luck, HAHAHAHAHA!!!" | |
| ) | |
| header = gr.Markdown() | |
| # Two panes side by side on wide screens; they stack automatically on narrow ones. | |
| with gr.Row(equal_height=False, elem_id="game_panes"): | |
| with gr.Column(scale=1, min_width=280): | |
| marks = gr.CheckboxGroup( | |
| choices=DIGITS, | |
| value=DIGITS, | |
| label="Still possible", | |
| info="Untick a digit to cross it off. This is only a note for you.", | |
| elem_id="marks", | |
| interactive=True, # outputs-only components default to read-only; this one is a personal notepad | |
| ) | |
| with gr.Column(visible=True) as guess_row: | |
| guess_in = gr.Textbox( | |
| label="Your guess", | |
| max_length=4, | |
| placeholder="e.g. 1234", | |
| elem_id="guess_in", | |
| ) | |
| guess_btn = gr.Button("Guess", variant="primary") | |
| with gr.Column(scale=1, min_width=280): | |
| msg = gr.Markdown() | |
| history = gr.Markdown(visible=False, elem_id="history") | |
| again_btn = gr.Button("π New game", visible=False) | |
| menu_btn = gr.Button("Back to menu") | |
| with gr.Accordion("How hints work", open=False): | |
| gr.Markdown( | |
| "- **Right place**: digits that are correct *and* in the correct position.\n" | |
| "- **Right number**: digits that are in the code at all (in any position).\n" | |
| "- Right place is always β€ right number." | |
| ) | |
| VIEW_OUTPUTS = [ | |
| state, menu_col, game_col, badge, header, marks, guess_row, msg, history, again_btn, guess_in, | |
| ] | |
| solo_btn.click(on_start_solo, None, VIEW_OUTPUTS) | |
| create_btn.click(on_create_room, secret_in, [set_out, secret_in]) | |
| secret_in.submit(on_create_room, secret_in, [set_out, secret_in]) | |
| join_btn.click(on_join, room_in, VIEW_OUTPUTS + [join_out, room_in]) | |
| room_in.submit(on_join, room_in, VIEW_OUTPUTS + [join_out, room_in]) | |
| guess_btn.click(on_guess, [state, guess_in], VIEW_OUTPUTS) | |
| guess_in.submit(on_guess, [state, guess_in], VIEW_OUTPUTS) | |
| again_btn.click(on_again, state, VIEW_OUTPUTS) | |
| menu_btn.click(on_menu, state, VIEW_OUTPUTS) | |
| if __name__ == "__main__": | |
| demo.launch(css=CSS, head=HEAD) |