Download src/streamlit_app.py from valent-p/x: direct link, hf CLI and curl.
- Browser
- Download file 48.4 kB
-
https://huggingface.co/spaces/valent-p/x/resolve/main/src/streamlit_app.py
- Command line
-
hf download hf://spaces/valent-p/x/src/streamlit_app.py
-
curl -L -o streamlit_app.py https://huggingface.co/spaces/valent-p/x/resolve/main/src/streamlit_app.py
48.4 kB
| import streamlit as st | |
| import random | |
| import time | |
| from datetime import datetime | |
| import pandas as pd | |
| # import numpy as np # Not explicitly used, can be removed if not needed for other things | |
| import altair as alt | |
| # Set page configuration (should be the first Streamlit command) | |
| st.set_page_config( | |
| page_title="Number Master Challenge", | |
| page_icon="π―", | |
| layout="centered", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # Apply custom CSS for better styling | |
| st.markdown(""" | |
| <style> | |
| /* --- CSS Variables --- */ | |
| :root { | |
| --primary-color: #00aaff; | |
| --primary-color-dark: #0088cc; | |
| --primary-color-darker: #006699; | |
| --primary-color-light: #cceeff; | |
| --primary-color-lighter: #e6f7ff; | |
| --primary-color-lightest: #f0faff; | |
| --text-color: #212529; | |
| --text-color-light: #495057; | |
| --text-color-lighter: #6c757d; | |
| --text-on-primary: #ffffff; | |
| --bg-color: #f8fcff; | |
| --bg-color-alt: #ffffff; | |
| --border-color: #dee2e6; | |
| --border-color-light: #e9ecef; | |
| --border-focus-color: var(--primary-color); | |
| --success-color: #28a745; | |
| --warning-color: #ffc107; | |
| --error-color: #dc3545; | |
| --info-color: #17a2b8; | |
| --border-radius: 8px; | |
| --border-radius-sm: 4px; | |
| --border-radius-lg: 12px; | |
| --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.05); | |
| --shadow: 0 4px 8px rgba(0, 0, 0, 0.07); | |
| --shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.07); | |
| --shadow-primary: 0 4px 12px rgba(0, 170, 255, 0.2); | |
| --shadow-primary-inset: inset 0 0 0 2px var(--primary-color); /* For focus */ | |
| --font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; | |
| } | |
| /* --- Global Styles --- */ | |
| body { | |
| font-family: var(--font-family-sans-serif); | |
| background-color: var(--bg-color); | |
| color: var(--text-color); | |
| line-height: 1.6; | |
| font-size: 16px; /* Base font size */ | |
| } | |
| /* Main app container (Streamlit >= 1.13.0) */ | |
| .main .block-container { | |
| padding-top: 2rem; | |
| padding-bottom: 2rem; | |
| padding-left: 2rem; | |
| padding-right: 2rem; | |
| } | |
| /* Older Streamlit might use this */ | |
| .main > div:first-child { | |
| padding: 2rem; | |
| } | |
| /* Remove Streamlit's default top padding if header is hidden */ | |
| .main[data-testid="stAppViewContainer"] > section:first-child { | |
| /* padding-top: 0 !important; if you hide the header via config */ | |
| } | |
| /* --- Typography --- */ | |
| h1, .stHeading h1, .game-title { /* For st.title, and your custom class */ | |
| font-size: 2.8rem; | |
| font-weight: 700; | |
| color: var(--primary-color-darker); | |
| margin-bottom: 1rem; | |
| line-height: 1.2; | |
| } | |
| h2, .stHeading h2 { /* For st.header */ | |
| font-size: 2.2rem; | |
| font-weight: 600; | |
| color: var(--primary-color-dark); | |
| margin-top: 2rem; | |
| margin-bottom: 0.75rem; | |
| } | |
| h3, .stHeading h3 { /* For st.subheader */ | |
| font-size: 1.75rem; | |
| font-weight: 600; | |
| color: var(--text-color); | |
| margin-top: 1.5rem; | |
| margin-bottom: 0.5rem; | |
| } | |
| p, .stMarkdown p, .stText { /* For st.markdown, st.write */ | |
| margin-bottom: 1rem; | |
| color: var(--text-color-light); | |
| } | |
| a { | |
| color: var(--primary-color); | |
| text-decoration: none; | |
| transition: color 0.2s ease; | |
| } | |
| a:hover { | |
| color: var(--primary-color-dark); | |
| text-decoration: underline; | |
| } | |
| .stCodeBlock, code { | |
| background-color: var(--primary-color-lighter) !important; | |
| border-radius: var(--border-radius-sm); | |
| padding: 0.2em 0.4em; | |
| font-size: 0.9em; | |
| border: 1px solid var(--primary-color-light); | |
| } | |
| pre code { | |
| padding: 1em; | |
| display: block; | |
| overflow-x: auto; | |
| } | |
| .stCaption { | |
| color: var(--text-color-lighter); | |
| font-size: 0.85em; | |
| } | |
| /* --- Buttons (st.button, st.download_button) --- */ | |
| .stButton > button, .stDownloadButton > button { | |
| background-color: var(--primary-color); | |
| color: var(--text-on-primary); | |
| border: none; | |
| padding: 0.65rem 1.25rem; | |
| border-radius: var(--border-radius); | |
| font-weight: 600; | |
| font-size: 0.95rem; | |
| box-shadow: var(--shadow-sm); | |
| transition: background-color 0.2s ease, transform 0.1s ease, box-shadow 0.2s ease; | |
| cursor: pointer; | |
| } | |
| .stButton > button:hover, .stDownloadButton > button:hover { | |
| background-color: var(--primary-color-dark); | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow-primary); | |
| } | |
| .stButton > button:active, .stDownloadButton > button:active { | |
| background-color: var(--primary-color-darker); | |
| transform: translateY(0); | |
| box-shadow: var(--shadow-sm); | |
| } | |
| .stButton > button:disabled, .stDownloadButton > button:disabled { | |
| background-color: var(--border-color-light); | |
| color: var(--text-color-lighter); | |
| cursor: not-allowed; | |
| box-shadow: none; | |
| transform: none; | |
| } | |
| .stButton > button:focus, .stDownloadButton > button:focus { | |
| outline: none; | |
| box-shadow: var(--shadow-primary-inset), var(--shadow-primary); | |
| } | |
| /* --- Input Widgets (Text, Number, Text Area) --- */ | |
| .stTextInput > div > div > input, | |
| .stNumberInput > div > div > input, | |
| .stTextArea > div > textarea { | |
| background-color: var(--bg-color-alt); | |
| border: 1px solid var(--border-color); | |
| border-radius: var(--border-radius); | |
| padding: 0.6rem 0.8rem; | |
| color: var(--text-color); | |
| font-size: 0.95rem; | |
| transition: border-color 0.2s ease, box-shadow 0.2s ease; | |
| } | |
| .stTextInput > div > div > input:focus, | |
| .stNumberInput > div > div > input:focus, | |
| .stTextArea > div > textarea:focus { | |
| border-color: var(--border-focus-color); | |
| box-shadow: var(--shadow-primary-inset); | |
| outline: none; | |
| } | |
| .stTextInput > div > div > input::placeholder, | |
| .stNumberInput > div > div > input::placeholder, | |
| .stTextArea > div > textarea::placeholder { | |
| color: var(--text-color-lighter); | |
| opacity: 0.7; | |
| } | |
| .stTextArea > div > textarea { | |
| min-height: 80px; | |
| } | |
| /* Number input buttons */ | |
| .stNumberInput button { | |
| border: 1px solid var(--border-color); | |
| background-color: var(--bg-color-alt); | |
| color: var(--text-color-light); | |
| transition: background-color 0.2s ease, color 0.2s ease; | |
| } | |
| .stNumberInput button:hover { | |
| background-color: var(--primary-color-lighter); | |
| color: var(--primary-color-dark); | |
| } | |
| .stNumberInput button:active { | |
| background-color: var(--primary-color-light); | |
| } | |
| /* --- Slider (st.slider, st.select_slider) --- */ | |
| .stSlider [data-baseweb="slider"] { | |
| padding-top: 10px; /* Space for thumb */ | |
| padding-bottom: 10px; | |
| } | |
| .stSlider [data-baseweb="slider"] > div:nth-child(2) { /* Track */ | |
| background-color: var(--primary-color-light); | |
| height: 6px !important; | |
| } | |
| .stSlider [data-baseweb="slider"] > div:nth-child(3) { /* Filled part of track */ | |
| background-color: var(--primary-color); | |
| height: 6px !important; | |
| } | |
| .stSlider [data-baseweb="slider"] > div[role="slider"] { /* Thumb */ | |
| background-color: var(--primary-color); | |
| border: 2px solid var(--bg-color-alt); | |
| box-shadow: var(--shadow-sm); | |
| width: 20px !important; | |
| height: 20px !important; | |
| margin-top: -7px; /* Adjust to center on track */ | |
| transition: transform 0.2s ease, box-shadow 0.2s ease; | |
| } | |
| .stSlider [data-baseweb="slider"] > div[role="slider"]:hover { | |
| transform: scale(1.1); | |
| box-shadow: var(--shadow-primary); | |
| } | |
| .stSlider [data-baseweb="slider"] > div[role="slider"]:focus { | |
| box-shadow: var(--shadow-primary-inset), var(--shadow-primary); | |
| } | |
| /* Select Slider specific - may need more specific selectors for values */ | |
| .stSelectSlider .stTickBar > div { /* Tick labels */ | |
| color: var(--text-color-lighter); | |
| } | |
| /* --- Selectbox (st.selectbox) & Multiselect (st.multiselect) --- */ | |
| .stSelectbox > div > div[data-baseweb="select"], | |
| .stMultiSelect > div > div[data-baseweb="multi-select"] { | |
| border-radius: var(--border-radius); | |
| border: 1px solid var(--border-color); | |
| background-color: var(--bg-color-alt); | |
| font-size: 0.95rem; | |
| color: var(--text-color); | |
| transition: border-color 0.2s ease, box-shadow 0.2s ease; | |
| } | |
| .stSelectbox > div > div[data-baseweb="select"]:focus-within, | |
| .stMultiSelect > div > div[data-baseweb="multi-select"]:focus-within { | |
| border-color: var(--border-focus-color); | |
| box-shadow: var(--shadow-primary-inset); | |
| } | |
| /* Dropdown arrow */ | |
| .stSelectbox [data-baseweb="select"] [data-testid="stIcon"], | |
| .stMultiSelect [data-baseweb="multi-select"] [data-testid="stIcon"] { | |
| color: var(--text-color-light); | |
| } | |
| /* Selected value in multiselect */ | |
| .stMultiSelect span[data-baseweb="tag"] { | |
| background-color: var(--primary-color-light) !important; | |
| color: var(--primary-color-darker) !important; | |
| border-radius: var(--border-radius-sm) !important; | |
| } | |
| .stMultiSelect span[data-baseweb="tag"] > span[role="button"] { /* The 'x' to remove */ | |
| color: var(--primary-color-darker) !important; | |
| } | |
| /* Dropdown menu list styling */ | |
| [data-baseweb="popover"] [data-baseweb="menu"] { | |
| border-radius: var(--border-radius); | |
| box-shadow: var(--shadow-lg); | |
| border: 1px solid var(--border-color-light); | |
| background-color: var(--bg-color-alt); | |
| } | |
| [data-baseweb="popover"] [data-baseweb="menu"] li { | |
| color: var(--text-color); | |
| padding-top: 0.5rem; | |
| padding-bottom: 0.5rem; | |
| transition: background-color 0.15s ease; | |
| } | |
| [data-baseweb="popover"] [data-baseweb="menu"] li:hover { | |
| background-color: var(--primary-color-lighter); | |
| } | |
| [data-baseweb="popover"] [data-baseweb="menu"] li[aria-selected="true"] { | |
| background-color: var(--primary-color-light); | |
| color: var(--primary-color-darker); | |
| font-weight: 600; | |
| } | |
| /* --- Radio (st.radio) & Checkbox (st.checkbox) & Toggle (st.toggle) --- */ | |
| .stRadio [data-baseweb="radio"] label, | |
| .stCheckbox [data-baseweb="checkbox"] label, | |
| .stToggle [data-baseweb="checkbox"] label /* Toggle uses checkbox base */ | |
| { | |
| color: var(--text-color-light); | |
| align-items: center; /* Vertically align text with custom control */ | |
| } | |
| /* Customizing the radio circle / checkbox square / toggle switch */ | |
| .stRadio [data-baseweb="radio"] > div:first-child > div, /* Radio circle */ | |
| .stCheckbox [data-baseweb="checkbox"] > div:first-child > div /* Checkbox square */ | |
| { | |
| border-color: var(--border-color) !important; | |
| background-color: var(--bg-color-alt) !important; | |
| transition: border-color 0.2s ease, background-color 0.2s ease; | |
| } | |
| .stRadio [data-baseweb="radio"] > div:first-child > div:hover, | |
| .stCheckbox [data-baseweb="checkbox"] > div:first-child > div:hover { | |
| border-color: var(--primary-color-dark) !important; | |
| } | |
| /* Checked state */ | |
| .stRadio input:checked + div > div, | |
| .stCheckbox input:checked + div > div { | |
| background-color: var(--primary-color) !important; | |
| border-color: var(--primary-color) !important; | |
| } | |
| /* Radio inner dot / Checkbox checkmark */ | |
| .stRadio input:checked + div > div > div { /* Radio inner dot */ | |
| background-color: var(--text-on-primary) !important; | |
| } | |
| .stCheckbox input:checked + div > div > svg { /* Checkbox checkmark */ | |
| fill: var(--text-on-primary) !important; | |
| } | |
| /* Toggle Switch */ | |
| .stToggle [data-baseweb="checkbox"] > div:first-child { /* The toggle track */ | |
| background-color: var(--border-color-light) !important; | |
| border-radius: 12px !important; /* Pill shape */ | |
| width: 44px !important; | |
| height: 24px !important; | |
| } | |
| .stToggle input:checked + div > div:first-child { /* Track when checked */ | |
| background-color: var(--primary-color) !important; | |
| } | |
| .stToggle [data-baseweb="checkbox"] > div:first-child > div { /* The toggle knob */ | |
| background-color: var(--bg-color-alt) !important; | |
| box-shadow: var(--shadow-sm); | |
| border: none !important; | |
| width: 20px !important; | |
| height: 20px !important; | |
| top: 2px !important; /* Position knob within track */ | |
| left: 2px !important; | |
| transition: transform 0.2s ease-in-out; | |
| } | |
| .stToggle input:checked + div > div:first-child > div { /* Knob when checked */ | |
| transform: translateX(20px) !important; | |
| background-color: var(--text-on-primary) !important; | |
| } | |
| /* --- Date & Time Inputs (st.date_input, st.time_input) --- */ | |
| .stDateInput input, .stTimeInput input { | |
| background-color: var(--bg-color-alt); | |
| border: 1px solid var(--border-color); | |
| border-radius: var(--border-radius); | |
| padding: 0.6rem 0.8rem; | |
| color: var(--text-color); | |
| font-size: 0.95rem; | |
| } | |
| .stDateInput input:focus, .stTimeInput input:focus { | |
| border-color: var(--border-focus-color); | |
| box-shadow: var(--shadow-primary-inset); | |
| outline: none; | |
| } | |
| /* Calendar/Time popover - usually styled by baseweb, some overrides */ | |
| [data-baseweb="popover"] .DayPicker_MonthHeader, | |
| [data-baseweb="popover"] .CalendarMonth_caption { | |
| background-color: var(--primary-color-lighter); | |
| color: var(--primary-color-darker); | |
| padding: 0.5rem; | |
| } | |
| [data-baseweb="popover"] .DayPicker_Day--selected { | |
| background-color: var(--primary-color) !important; | |
| color: var(--text-on-primary) !important; | |
| } | |
| [data-baseweb="popover"] .DayPicker_Day:hover:not(.DayPicker_Day--selected) { | |
| background-color: var(--primary-color-lighter) !important; | |
| } | |
| /* --- File Uploader (st.file_uploader) --- */ | |
| .stFileUploader { | |
| border: 2px dashed var(--primary-color-light); | |
| background-color: var(--primary-color-lightest); | |
| border-radius: var(--border-radius-lg); | |
| padding: 2rem; | |
| text-align: center; | |
| transition: background-color 0.2s ease, border-color 0.2s ease; | |
| } | |
| .stFileUploader:hover { | |
| border-color: var(--primary-color); | |
| background-color: var(--primary-color-lighter); | |
| } | |
| .stFileUploader button { /* The "Browse files" button inside */ | |
| background-color: var(--primary-color); | |
| color: var(--text-on-primary); | |
| border: none; | |
| padding: 0.5rem 1rem; | |
| border-radius: var(--border-radius); | |
| font-weight: 500; | |
| } | |
| .stFileUploader button:hover { | |
| background-color: var(--primary-color-dark); | |
| } | |
| .stFileUploader [data-testid="stFileUploadDropzoneInstructions"] { | |
| color: var(--primary-color-dark); | |
| } | |
| /* Uploaded file item */ | |
| .stFileUploader [data-testid="stFileUploaderFile"] { | |
| background-color: var(--bg-color-alt); | |
| border: 1px solid var(--border-color-light); | |
| border-radius: var(--border-radius-sm); | |
| padding: 0.5rem; | |
| } | |
| /* --- Color Picker (st.color_picker) --- */ | |
| .stColorPicker button { | |
| border-radius: var(--border-radius); | |
| border: 1px solid var(--border-color); | |
| box-shadow: var(--shadow-sm); | |
| } | |
| .stColorPicker button:hover { | |
| border-color: var(--primary-color-dark); | |
| } | |
| .stColorPicker button:focus-within { /* When picker is open or button focused */ | |
| border-color: var(--border-focus-color); | |
| box-shadow: var(--shadow-primary-inset); | |
| } | |
| /* The popover part of the color picker is harder to style without very specific selectors or JS */ | |
| /* --- Progress Bar (st.progress) --- */ | |
| .stProgress > div > div > div > div { | |
| background-color: var(--primary-color); | |
| border-radius: var(--border-radius-sm); | |
| } | |
| .stProgress > div > div { /* The track */ | |
| background-color: var(--primary-color-light); | |
| border-radius: var(--border-radius-sm); | |
| } | |
| /* --- Status Messages (st.error, st.warning, st.info, st.success, st.exception) --- */ | |
| .stAlert { | |
| border-radius: var(--border-radius); | |
| border-width: 0px; /* Remove default baseweb border */ | |
| border-left-width: 5px !important; /* Prominent left border */ | |
| padding: 1rem 1.25rem; | |
| box-shadow: var(--shadow); | |
| font-size: 0.95rem; | |
| } | |
| .stAlert strong { /* Make title bolder */ | |
| font-weight: 600; | |
| } | |
| .stAlert [data-testid="stNotificationContentSuccess"] { | |
| border-left-color: var(--success-color) !important; | |
| background-color: #e9f7ef; /* Light success green */ | |
| color: #196835; | |
| } | |
| .stAlert [data-testid="stNotificationContentInfo"] { | |
| border-left-color: var(--info-color) !important; | |
| background-color: #e8f6f8; /* Light info blue */ | |
| color: #0c5460; | |
| } | |
| .stAlert [data-testid="stNotificationContentWarning"] { | |
| border-left-color: var(--warning-color) !important; | |
| background-color: #fff8e6; /* Light warning orange */ | |
| color: #856404; | |
| } | |
| .stAlert [data-testid="stNotificationContentError"] { | |
| border-left-color: var(--error-color) !important; | |
| background-color: #fbe9eb; /* Light error red */ | |
| color: #721c24; | |
| } | |
| /* Icon colors */ | |
| .stAlert [data-testid="stNotificationContentSuccess"] svg { fill: var(--success-color); } | |
| .stAlert [data-testid="stNotificationContentInfo"] svg { fill: var(--info-color); } | |
| .stAlert [data-testid="stNotificationContentWarning"] svg { fill: var(--warning-color); } | |
| .stAlert [data-testid="stNotificationContentError"] svg { fill: var(--error-color); } | |
| /* --- Expander (st.expander) --- */ | |
| .stExpander { | |
| border: 1px solid var(--border-color-light); | |
| border-radius: var(--border-radius-lg); /* Larger radius for container feel */ | |
| box-shadow: var(--shadow-sm); | |
| background-color: var(--bg-color-alt); /* Give it a distinct background */ | |
| overflow: hidden; /* Ensure border-radius clips content */ | |
| } | |
| .stExpander header { | |
| background-color: transparent; /* Default bg is white, so no change needed or var(--bg-color-alt) */ | |
| color: var(--primary-color-dark); | |
| padding: 0.75rem 1.25rem; | |
| border-bottom: 1px solid var(--border-color-light); /* Separator when open */ | |
| transition: background-color 0.2s ease; | |
| } | |
| .stExpander header:hover { | |
| background-color: var(--primary-color-lightest); | |
| } | |
| .stExpander > div:nth-child(2) { /* Content area */ | |
| padding: 1.25rem; | |
| background-color: var(--bg-color-alt); /* Consistent background */ | |
| } | |
| .stExpander summary svg { /* The expand/collapse icon */ | |
| fill: var(--primary-color-dark); | |
| } | |
| /* --- Tabs (st.tabs) --- */ | |
| div[data-baseweb="tabs"] button[role="tab"] { | |
| background-color: transparent !important; | |
| color: var(--text-color-light) !important; | |
| padding: 0.75rem 1.25rem !important; | |
| border-bottom: 2px solid transparent !important; | |
| transition: color 0.2s ease, border-color 0.2s ease; | |
| font-weight: 500; | |
| } | |
| div[data-baseweb="tabs"] button[role="tab"]:hover { | |
| color: var(--primary-color) !important; | |
| } | |
| div[data-baseweb="tabs"] button[role="tab"][aria-selected="true"] { | |
| color: var(--primary-color-dark) !important; | |
| border-bottom-color: var(--primary-color) !important; | |
| font-weight: 600; | |
| } | |
| div[data-baseweb="tabs"] > div:last-child { /* Tab content panel */ | |
| padding-top: 1.5rem; | |
| background-color: var(--bg-color-alt); /* Optional: if you want tab content on a card */ | |
| border-radius: 0 0 var(--border-radius) var(--border-radius); /* If above bg is used */ | |
| /* box-shadow: var(--shadow-sm); */ /* If above bg is used */ | |
| } | |
| /* --- Sidebar --- */ | |
| section[data-testid="stSidebar"] { | |
| background-color: var(--primary-color-lightest); /* Slightly different bg */ | |
| border-right: 1px solid var(--border-color-light); | |
| box-shadow: var(--shadow); | |
| } | |
| section[data-testid="stSidebar"] .stButton > button { | |
| /* Example: Make sidebar buttons full width and slightly different style */ | |
| width: 100%; | |
| background-color: var(--primary-color-light); | |
| color: var(--primary-color-darker); | |
| font-weight: 600; | |
| } | |
| section[data-testid="stSidebar"] .stButton > button:hover { | |
| background-color: var(--primary-color); | |
| color: var(--text-on-primary); | |
| } | |
| /* Styles for widgets *inside* the sidebar can be targeted like: */ | |
| /* section[data-testid="stSidebar"] .stSelectbox { ... } */ | |
| /* --- Dataframes & Tables (st.dataframe, st.table) --- */ | |
| .stDataFrame, .stTable { | |
| border-radius: var(--border-radius); | |
| overflow: hidden; /* To make border-radius work on table */ | |
| box-shadow: var(--shadow-sm); | |
| border: 1px solid var(--border-color-light); | |
| } | |
| .stDataFrame table, .stTable table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| } | |
| .stDataFrame th, .stTable th { | |
| background-color: var(--primary-color-lighter); | |
| color: var(--primary-color-darker); | |
| font-weight: 600; | |
| padding: 0.75rem 1rem; | |
| text-align: left; | |
| border-bottom: 2px solid var(--primary-color-light); | |
| } | |
| .stDataFrame td, .stTable td { | |
| padding: 0.6rem 1rem; | |
| border-bottom: 1px solid var(--border-color-light); | |
| color: var(--text-color-light); | |
| background-color: var(--bg-color-alt); | |
| } | |
| .stDataFrame tr:last-child td, .stTable tr:last-child td { | |
| border-bottom: none; | |
| } | |
| .stDataFrame tr:hover td, .stTable tr:hover td { | |
| background-color: var(--primary-color-lightest); /* Subtle hover */ | |
| } | |
| /* --- Metric (st.metric) --- */ | |
| .stMetric { | |
| background-color: var(--bg-color-alt); | |
| padding: 1.5rem; | |
| border-radius: var(--border-radius-lg); | |
| box-shadow: var(--shadow); | |
| text-align: center; /* Center align metric content */ | |
| } | |
| .stMetric > label { /* Metric Label */ | |
| color: var(--text-color-lighter); | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| } | |
| .stMetric > div:nth-of-type(1) { /* Metric Value */ | |
| font-size: 2.5rem; | |
| font-weight: 700; | |
| color: var(--primary-color-dark); | |
| line-height: 1; | |
| margin: 0.25rem 0; | |
| } | |
| .stMetric > div:nth-of-type(2) { /* Metric Delta */ | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| } | |
| /* Delta colors - Streamlit uses data-testid for up/down arrow */ | |
| .stMetric [data-testid="stMetricDeltaIcon-Up"] + div { color: var(--success-color); } | |
| .stMetric [data-testid="stMetricDeltaIcon-Down"] + div { color: var(--error-color); } | |
| .stMetric [data-testid="stMetricDeltaIcon-Up"] svg { fill: var(--success-color); } | |
| .stMetric [data-testid="stMetricDeltaIcon-Down"] svg { fill: var(--error-color); } | |
| /* --- Chat Elements (st.chat_message, st.chat_input) --- */ | |
| .stChatMessage { | |
| border-radius: var(--border-radius-lg); | |
| padding: 0.75rem 1.25rem; | |
| margin-bottom: 0.75rem; | |
| box-shadow: var(--shadow-sm); | |
| max-width: 80%; /* Don't let messages span full width */ | |
| } | |
| .stChatMessage[data-testid="stChatMessageContentUser"] { /* User message */ | |
| background-color: var(--primary-color); | |
| color: var(--text-on-primary); | |
| margin-left: auto; /* Align to right */ | |
| border-bottom-right-radius: var(--border-radius-sm); /* Speech bubble effect */ | |
| } | |
| .stChatMessage[data-testid="stChatMessageContentUser"] a { color: var(--primary-color-lighter); } | |
| .stChatMessage[data-testid="stChatMessageContentAssistant"] { /* Assistant message */ | |
| background-color: var(--bg-color-alt); | |
| color: var(--text-color); | |
| margin-right: auto; /* Align to left */ | |
| border: 1px solid var(--border-color-light); | |
| border-bottom-left-radius: var(--border-radius-sm); /* Speech bubble effect */ | |
| } | |
| .stChatMessage .stMarkdown p:last-child { | |
| margin-bottom: 0; /* Remove extra margin from last paragraph in chat */ | |
| } | |
| .stChatInput { | |
| background-color: var(--bg-color-alt); | |
| border-top: 1px solid var(--border-color-light); | |
| padding: 0.75rem 1rem; | |
| box-shadow: 0 -2px 5px rgba(0,0,0,0.03); | |
| } | |
| .stChatInput textarea { | |
| background-color: var(--primary-color-lightest); | |
| border: 1px solid var(--primary-color-light); | |
| border-radius: var(--border-radius); | |
| padding: 0.5rem 0.75rem; | |
| color: var(--text-color); | |
| } | |
| .stChatInput textarea:focus { | |
| border-color: var(--primary-color); | |
| box-shadow: var(--shadow-primary-inset); | |
| } | |
| .stChatInput button[kind="secondary"] { /* The send button */ | |
| background-color: var(--primary-color); | |
| border-radius: 50%; /* Make it circular */ | |
| padding: 0.5rem; | |
| width: 40px; | |
| height: 40px; | |
| transition: background-color 0.2s ease; | |
| } | |
| .stChatInput button[kind="secondary"]:hover { | |
| background-color: var(--primary-color-dark); | |
| } | |
| .stChatInput button[kind="secondary"] svg { | |
| fill: var(--text-on-primary); | |
| } | |
| /* --- Custom Game Specific Styles from previous example --- */ | |
| .game-card { | |
| background-color: var(--bg-color-alt); | |
| padding: 2rem; | |
| border-radius: var(--border-radius-lg); | |
| box-shadow: 0 8px 16px rgba(0, 170, 255, 0.1), 0 4px 8px rgba(0,0,0,0.05); | |
| margin-bottom: 2rem; | |
| } | |
| .stat-card { | |
| background-color: var(--primary-color-lighter); | |
| padding: 1.25rem; | |
| border-radius: var(--border-radius); | |
| margin-bottom: 1rem; | |
| border: 1px solid var(--primary-color-light); | |
| } | |
| .feedback-text { | |
| font-size: 1.1rem; | |
| text-align: center; | |
| margin: 1.5rem 0; | |
| font-weight: 500; | |
| color: var(--primary-color-dark); | |
| } | |
| .celebration { | |
| text-align: center; | |
| font-size: 2.2rem; | |
| color: var(--warning-color); /* Using warning color for gold */ | |
| margin: 1.5rem 0; | |
| font-weight: bold; | |
| text-shadow: 0 0 10px rgba(255, 193, 7, 0.5); | |
| } | |
| .hint-text { | |
| font-style: italic; | |
| color: var(--text-color-lighter); | |
| text-align: center; | |
| margin-top: 0.75rem; | |
| font-size: 0.9rem; | |
| } | |
| /* --- Spinner --- */ | |
| [data-testid="stSpinner"] > div { | |
| border-top-color: var(--primary-color) !important; /* Color of the spinner arc */ | |
| border-right-color: var(--primary-color-light) !important; | |
| border-bottom-color: var(--primary-color-light) !important; | |
| border-left-color: var(--primary-color-light) !important; | |
| } | |
| /* --- Scrollbars (Webkit only for simplicity, add Firefox if needed) --- */ | |
| ::-webkit-scrollbar { | |
| width: 10px; | |
| height: 10px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: var(--primary-color-lightest); | |
| border-radius: var(--border-radius); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--primary-color-light); | |
| border-radius: var(--border-radius); | |
| border: 2px solid var(--primary-color-lightest); /* Creates padding around thumb */ | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--primary-color); | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # --- Session State Initialization --- | |
| # Using a helper to make it slightly cleaner | |
| def init_session_state_key(key, value): | |
| if key not in st.session_state: | |
| st.session_state[key] = value | |
| init_session_state_key('number_to_guess', random.randint(1, 100)) # Default to Medium on first ever load | |
| init_session_state_key('attempts', 0) | |
| init_session_state_key('total_games', 0) | |
| init_session_state_key('total_guesses', 0) | |
| init_session_state_key('wins', 0) | |
| init_session_state_key('best_score', float('inf')) | |
| init_session_state_key('game_active', True) | |
| init_session_state_key('difficulty', 'Medium') # Default difficulty | |
| init_session_state_key('max_attempts', 10) # Default for Medium | |
| init_session_state_key('range_min', 1) # Default for Medium | |
| init_session_state_key('range_max', 100) # Default for Medium | |
| init_session_state_key('history', []) | |
| init_session_state_key('game_start_time', time.time()) | |
| init_session_state_key('feedback', "Select your difficulty and make your first guess!") | |
| init_session_state_key('hint_used', False) | |
| init_session_state_key('guess_history', []) | |
| # --- Game Logic Functions --- | |
| DIFFICULTY_SETTINGS = { | |
| 'Easy': {'min': 1, 'max': 50, 'attempts': 15}, | |
| 'Medium': {'min': 1, 'max': 100, 'attempts': 10}, | |
| 'Hard': {'min': 1, 'max': 200, 'attempts': 8}, | |
| 'Expert': {'min': 1, 'max': 500, 'attempts': 6} | |
| } | |
| def reset_game(difficulty_changed=False): | |
| settings = DIFFICULTY_SETTINGS[st.session_state.difficulty] | |
| st.session_state.range_min = settings['min'] | |
| st.session_state.range_max = settings['max'] | |
| st.session_state.max_attempts = settings['attempts'] | |
| st.session_state.number_to_guess = random.randint(st.session_state.range_min, st.session_state.range_max) | |
| st.session_state.attempts = 0 | |
| st.session_state.game_active = True | |
| if difficulty_changed: | |
| st.session_state.feedback = f"Difficulty set to {st.session_state.difficulty}. New game started!" | |
| else: | |
| st.session_state.feedback = "New game started! Make your guess." | |
| st.session_state.hint_used = False | |
| st.session_state.guess_history = [] | |
| st.session_state.game_start_time = time.time() | |
| # No need to manage input widget keys explicitly if using st.form and relying on Streamlit's default keying or explicit keys for inputs. | |
| def update_stats(won): | |
| st.session_state.total_games += 1 | |
| st.session_state.total_guesses += st.session_state.attempts | |
| if won: | |
| st.session_state.wins += 1 | |
| if st.session_state.attempts < st.session_state.best_score: | |
| st.session_state.best_score = st.session_state.attempts | |
| game_duration = round(time.time() - st.session_state.game_start_time, 1) | |
| st.session_state.history.append({ | |
| 'date': datetime.now().strftime("%Y-%m-%d %H:%M"), | |
| 'difficulty': st.session_state.difficulty, | |
| 'attempts': st.session_state.attempts, | |
| 'max_attempts': st.session_state.max_attempts, | |
| 'won': won, | |
| 'duration': game_duration, | |
| 'number': st.session_state.number_to_guess # Store the number for history | |
| }) | |
| def evaluate_guess(guess_value): # Renamed param to avoid conflict | |
| # Ensure guess is an integer | |
| try: | |
| guess_value = int(guess_value) | |
| except (ValueError, TypeError): | |
| st.session_state.feedback = "Invalid guess. Please enter a whole number." | |
| return # Don't proceed with evaluation | |
| st.session_state.attempts += 1 | |
| st.session_state.guess_history.append(guess_value) | |
| if guess_value == st.session_state.number_to_guess: | |
| st.session_state.feedback = f"π CORRECT! You found the number {st.session_state.number_to_guess} in {st.session_state.attempts} attempts!" | |
| st.session_state.game_active = False | |
| update_stats(True) | |
| st.balloons() # Add some celebration | |
| elif st.session_state.attempts >= st.session_state.max_attempts: | |
| st.session_state.feedback = f"π Game over! You've used all {st.session_state.max_attempts} attempts. The number was {st.session_state.number_to_guess}." | |
| st.session_state.game_active = False | |
| update_stats(False) | |
| elif guess_value < st.session_state.number_to_guess: | |
| st.session_state.feedback = "π Too low! Aim higher." | |
| else: # guess_value > st.session_state.number_to_guess | |
| st.session_state.feedback = "π Too high! Aim lower." | |
| def give_hint(): | |
| if not st.session_state.game_active: | |
| return "Game is over. Start a new game to get hints!" | |
| if st.session_state.hint_used: | |
| return "You've already used your hint for this game!" | |
| target = st.session_state.number_to_guess | |
| hints = [] | |
| if target % 2 == 0: | |
| hints.append("The number is even.") | |
| else: | |
| hints.append("The number is odd.") | |
| if st.session_state.difficulty in ['Easy', 'Medium'] and len(str(target)) > 1 : # More complex hints for easier levels | |
| digit_sum = sum(int(digit) for digit in str(target)) | |
| hints.append(f"The sum of its digits is {digit_sum}.") | |
| if st.session_state.difficulty == 'Easy': | |
| if target > st.session_state.range_min + (st.session_state.range_max - st.session_state.range_min) / 2: | |
| hints.append("It's in the upper half of the range.") | |
| else: | |
| hints.append("It's in the lower half of the range.") | |
| st.session_state.hint_used = True # Mark hint as used | |
| # st.session_state.attempts += 1 # Optional: Penalize hint usage by an attempt | |
| # if st.session_state.attempts >= st.session_state.max_attempts: | |
| # st.session_state.feedback = f"π Game over! You've used all {st.session_state.max_attempts} attempts. The number was {st.session_state.number_to_guess}." | |
| # st.session_state.game_active = False | |
| # update_stats(False) | |
| print(hints) # debug | |
| return " ".join(hints) if hints else "Hmm, I'm feeling shy with hints right now." | |
| # Cache the fun fact for the session | |
| def get_number_fun_fact_cached(): | |
| fun_facts = [ | |
| "The fear of numbers is called 'arithmophobia'.", | |
| "Zero (0) was not recognized as a number in Europe until the 13th century.", | |
| "7 is often considered the world's most common lucky number.", | |
| "The Mayans and Babylonians independently developed the concept of zero.", | |
| "142857 is a cyclic number: 142857 Γ 2 = 285714, 142857 Γ 3 = 428571, etc.", | |
| "There are 293 ways to make change for a US dollar using standard coins.", | |
| "A googol is 1 followed by 100 zeros. A googolplex is 10 to the power of a googol.", | |
| "In many East Asian cultures, the number 4 is unlucky as it sounds like 'death'.", | |
| "Ancient Greeks considered 10 to be a perfect number (tetractys).", | |
| "A 'perfect number' is a positive integer that is equal to the sum of its proper positive divisors. E.g., 6 (1+2+3).", | |
| "No number from 1 to 999 contains the letter 'a' when spelled out in English.", | |
| "The number 8 is considered very lucky in Chinese culture, sounding like 'wealth' or 'prosper'.", | |
| "The sum of the first 10 Lucas numbers (starting L0=2, L1=1) is 123 + 1 = 124. L10 is 123.", # Corrected fact | |
| "Euler's number (e) is an irrational number approximately equal to 2.71828." | |
| ] | |
| return random.choice(fun_facts) | |
| # --- Sidebar --- | |
| with st.sidebar: | |
| st.markdown("## Game Settings") | |
| difficulty_options = list(DIFFICULTY_SETTINGS.keys()) | |
| current_difficulty_index = difficulty_options.index(st.session_state.difficulty) | |
| new_difficulty = st.selectbox("Select Difficulty", difficulty_options, | |
| index=current_difficulty_index, key="difficulty_selector") | |
| if new_difficulty != st.session_state.difficulty: | |
| st.session_state.difficulty = new_difficulty | |
| reset_game(difficulty_changed=True) | |
| st.rerun() # Explicitly rerun to update UI based on new difficulty | |
| st.markdown("---") | |
| st.markdown("## Game Statistics") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.metric("Games Played", st.session_state.total_games) | |
| st.metric("Wins", st.session_state.wins) | |
| with col2: | |
| st.metric("Best Score (Attempts)", st.session_state.best_score if st.session_state.best_score != float('inf') else "N/A") | |
| win_rate_val = (st.session_state.wins / st.session_state.total_games * 100) if st.session_state.total_games > 0 else 0 | |
| st.metric("Win Rate", f"{win_rate_val:.1f}%") | |
| avg_attempts_val = (st.session_state.total_guesses / st.session_state.total_games) if st.session_state.total_games > 0 else 0 | |
| st.metric("Avg. Attempts per Game", f"{avg_attempts_val:.1f}" if st.session_state.total_games > 0 else "N/A", | |
| help="Average attempts across all completed games.") | |
| st.markdown("---") | |
| st.markdown("## Did You Know?") | |
| st.markdown(f"*{get_number_fun_fact_cached()}*") # Use cached version | |
| st.markdown("---") | |
| if st.button("Reset All Stats", type="secondary"): | |
| # Clear specific stats, keep difficulty | |
| init_session_state_key('total_games', 0) # Re-init to 0 | |
| st.session_state.total_games = 0 | |
| st.session_state.total_guesses = 0 | |
| st.session_state.wins = 0 | |
| st.session_state.best_score = float('inf') | |
| st.session_state.history = [] | |
| # reset_game() # Don't reset the current game, just the stats | |
| st.success("All game statistics have been reset!") | |
| time.sleep(1.5) # Give user time to see message | |
| st.rerun() | |
| # --- Main Game Area --- | |
| st.markdown('<h1 class="game-title">Number Master Challenge</h1>', unsafe_allow_html=True) | |
| st.markdown("<p style='font-size:80%; text-align: center; color: #777;'>Number Master Challenge by Valentino Phiri</p>", unsafe_allow_html=True) | |
| st.markdown(f'<p class="game-subtitle">Guess the number between {st.session_state.range_min} and {st.session_state.range_max}!</p>', unsafe_allow_html=True) | |
| # Game info card | |
| with st.container(border=False): # Use border=True for themed container | |
| st.markdown('<div class="game-card">', unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown(f"**π― Difficulty:** {st.session_state.difficulty}") | |
| st.markdown(f"**π’ Range:** {st.session_state.range_min} β {st.session_state.range_max}") | |
| with col2: | |
| st.markdown(f"**β³ Attempts Left:** {st.session_state.max_attempts - st.session_state.attempts}") | |
| # Progress bar for attempts | |
| progress_value = (st.session_state.attempts / st.session_state.max_attempts) if st.session_state.max_attempts > 0 else 0 | |
| st.progress(progress_value, text=f"{st.session_state.attempts}/{st.session_state.max_attempts} Guesses Made") | |
| # Feedback message | |
| feedback_type = "info" | |
| if "CORRECT" in st.session_state.feedback: feedback_type = "success" | |
| elif "Game over" in st.session_state.feedback or "Too low" in st.session_state.feedback or "Too high" in st.session_state.feedback: feedback_type = "warning" | |
| if "Invalid" in st.session_state.feedback: feedback_type = "error" | |
| if feedback_type == "success": st.success(st.session_state.feedback) | |
| elif feedback_type == "warning": st.warning(st.session_state.feedback) | |
| elif feedback_type == "error": st.error(st.session_state.feedback) | |
| else: st.info(st.session_state.feedback) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # Guessing input area | |
| if st.session_state.game_active: | |
| st.markdown('<div class="game-card">', unsafe_allow_html=True) | |
| tab1, tab2 = st.tabs(["β¨οΈ Type Guess", "ποΈ Slider Guess"]) | |
| with tab1: # Type Guess is often preferred, so make it first | |
| with st.form(key="typed_form"): | |
| # Default value for number_input could be last guess or middle of remaining range | |
| # For simplicity, let's use a placeholder or the min of the range | |
| default_typed_guess = st.session_state.range_min | |
| if st.session_state.guess_history: # Try to default to something near last guess | |
| last_guess = st.session_state.guess_history[-1] | |
| if st.session_state.feedback.startswith("π Too low"): | |
| default_typed_guess = min(st.session_state.range_max, last_guess + 1) | |
| elif st.session_state.feedback.startswith("π Too high"): | |
| default_typed_guess = max(st.session_state.range_min, last_guess - 1) | |
| typed_guess_value = st.number_input("Enter your guess:", | |
| min_value=st.session_state.range_min, | |
| max_value=st.session_state.range_max, | |
| value=default_typed_guess, # Smarter default | |
| step=1, | |
| key="direct_guess_input") # Unique key for the input widget | |
| typed_submit_button = st.form_submit_button("Guess with Number") | |
| if typed_submit_button: | |
| # st.write(f"DEBUG: Typed form submitted with {typed_guess_value}") # For debugging | |
| evaluate_guess(typed_guess_value) | |
| st.rerun() # Rerun to update UI (feedback, attempts, etc.) | |
| with tab2: | |
| with st.form(key="slider_form"): | |
| # Default slider value to middle of the current range | |
| slider_default_val = (st.session_state.range_min + st.session_state.range_max) // 2 | |
| if st.session_state.guess_history: # Try to default to something near last guess | |
| last_guess = st.session_state.guess_history[-1] | |
| if st.session_state.range_min <= last_guess <= st.session_state.range_max: | |
| slider_default_val = last_guess | |
| slider_guess_value = st.slider("Slide to your guess:", | |
| min_value=st.session_state.range_min, | |
| max_value=st.session_state.range_max, | |
| value=slider_default_val, # Smarter default | |
| key="slider_guess_input") # Unique key | |
| slider_submit_button = st.form_submit_button("Guess with Slider") | |
| if slider_submit_button: | |
| # st.write(f"DEBUG: Slider form submitted with {slider_guess_value}") # For debugging | |
| evaluate_guess(slider_guess_value) | |
| st.rerun() # Rerun to update UI | |
| # Hint button (outside forms but within game_active and game-card) | |
| if not st.session_state.hint_used: # Only show hint button if not used | |
| if st.button("π‘ Give Me a Hint", key="hint_button", help="Get a clue about the number. May cost an attempt in future versions!"): | |
| hint_text = give_hint() | |
| st.markdown(f'<div class="hint-text">{hint_text}</div>', unsafe_allow_html=True) | |
| # If hint costs an attempt and evaluate_guess is not called inside give_hint, rerun might be needed | |
| # For now, give_hint updates session_state.hint_used, rerun helps if UI depends on it. | |
| st.rerun() # Rerun to hide hint button if give_hint sets hint_used=True | |
| else: | |
| st.markdown('<div class="hint-text">Hint has been used for this game.</div>', unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # Game over screen / Next game | |
| if not st.session_state.game_active: | |
| st.markdown('<div class="game-card">', unsafe_allow_html=True) | |
| if "CORRECT" in st.session_state.feedback: | |
| st.markdown('<div class="celebration">π CONGRATULATIONS! π</div>', unsafe_allow_html=True) | |
| # Calculate score (optional, based on difficulty and attempts) | |
| # score = int(1000 * (list(DIFFICULTY_SETTINGS.keys()).index(st.session_state.difficulty) + 1) / st.session_state.attempts) | |
| # st.markdown(f"<h3 style='text-align: center;'>Your score: {score} points</h3>", unsafe_allow_html=True) | |
| else: # Game lost | |
| st.markdown('<div class="celebration" style="color: #FF6B6B;">π GAME OVER π</div>', unsafe_allow_html=True) | |
| encouragements = [ | |
| "That was a tough one! Try again?", "So close! Next time for sure.", "Don't give up, a new challenge awaits!", | |
| "Practice makes perfect. Ready for another round?", "Every master was once a beginner. New game?" | |
| ] | |
| st.markdown(f"<p style='text-align: center; font-style: italic;'>{random.choice(encouragements)}</p>", unsafe_allow_html=True) | |
| if st.button("Start New Game", key="new_game_button", type="primary"): | |
| reset_game() | |
| st.rerun() | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # Guess history chart (shown after at least one guess) | |
| if st.session_state.guess_history: | |
| st.markdown('<div class="game-card">', unsafe_allow_html=True) | |
| st.markdown("#### Your Guess Journey") | |
| guess_df = pd.DataFrame({ | |
| 'Attempt': range(1, len(st.session_state.guess_history) + 1), | |
| 'Guess': st.session_state.guess_history | |
| }) | |
| # Determine y-axis scale dynamically but capped by game range | |
| min_guess_val = min(st.session_state.guess_history) if st.session_state.guess_history else st.session_state.range_min | |
| max_guess_val = max(st.session_state.guess_history) if st.session_state.guess_history else st.session_state.range_max | |
| # Add a bit of padding to the y-axis, but don't exceed game range | |
| y_domain_min = max(st.session_state.range_min, min_guess_val - 10) | |
| y_domain_max = min(st.session_state.range_max, max_guess_val + 10) | |
| # Ensure min is less than max if only one guess or guesses are close | |
| if y_domain_min >= y_domain_max: | |
| y_domain_min = st.session_state.range_min | |
| y_domain_max = st.session_state.range_max | |
| base_chart = alt.Chart(guess_df).mark_line(point=True, color='cornflowerblue').encode( | |
| x=alt.X('Attempt:Q', axis=alt.Axis(tickMinStep=1, title='Attempt Number')), # Ensure integer ticks | |
| y=alt.Y('Guess:Q', scale=alt.Scale(domain=[y_domain_min, y_domain_max]), title='Guessed Value'), | |
| tooltip=['Attempt', 'Guess'] | |
| ).properties( | |
| title='Guesses vs. Attempts' | |
| ) | |
| if not st.session_state.game_active: # Show target line only when game is over | |
| target_df = pd.DataFrame({'Target': [st.session_state.number_to_guess]}) | |
| target_line = alt.Chart(target_df).mark_rule(color='red', strokeDash=[5,5]).encode( | |
| y='Target:Q', | |
| size=alt.value(2) # Make line thicker | |
| ) | |
| label = target_line.mark_text( | |
| align='left', | |
| baseline='middle', | |
| dx=7, | |
| text=f"Target: {st.session_state.number_to_guess}" | |
| ).encode(text=alt.value(f"Target: {st.session_state.number_to_guess}")) | |
| st.altair_chart(base_chart + target_line + label, use_container_width=True) | |
| else: | |
| st.altair_chart(base_chart, use_container_width=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # Game history table | |
| if st.session_state.history: | |
| if st.checkbox("Show Full Game History", value=False, key="show_history_checkbox"): | |
| st.markdown("## Your Past Games") | |
| history_df = pd.DataFrame(st.session_state.history).sort_values(by="date", ascending=False) | |
| if not history_df.empty: | |
| history_df['Result'] = history_df.apply( | |
| lambda x: f"Won ({x['attempts']}/{x['max_attempts']} attempts)" if x['won'] else f"Lost ({x['attempts']}/{x['max_attempts']} attempts)", axis=1 | |
| ) | |
| history_df['Target Number'] = history_df['number'] # Add target number column | |
| st.dataframe( | |
| history_df[['date', 'difficulty', 'Result', 'Target Number', 'duration']], | |
| column_config={ | |
| 'date': st.column_config.DatetimeColumn("Date & Time", format="YYYY-MM-DD HH:mm"), | |
| 'difficulty': 'Difficulty', | |
| 'Result': 'Outcome', | |
| 'Target Number': 'Secret Number', | |
| 'duration': st.column_config.NumberColumn('Duration (s)', format="%.1fs") | |
| }, | |
| use_container_width=True, | |
| hide_index=True | |
| ) | |
| st.markdown("---") | |
| st.markdown("<p style='text-align: center; color: #777;'>Number Master Challenge by Valentino Phiri</p>", unsafe_allow_html=True) |