Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eccee4d3f1 | ||
|
|
45de1fd982 | ||
|
|
7a95e50fe7 | ||
|
|
3d78ff7863 | ||
|
|
4c6c53ff13 | ||
|
|
8e62094a6f | ||
|
|
0f8640e9e8 | ||
|
|
6b07093d68 | ||
|
|
a3f9ae6a17 | ||
|
|
d34d98c15d | ||
|
|
8dfebaa9f8 | ||
|
|
f29d81e932 | ||
|
|
73f403693a | ||
|
|
1f6a5b4a44 | ||
|
|
437d46e975 | ||
|
|
7621938f04 | ||
|
|
1a9a440509 | ||
|
|
d9a7c94125 |
No files matched your search
@@ -26,5 +26,10 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
scp -i ~/.ssh/id_rsa wordle ${{ vars.REMOTE_USER }}@${{ vars.REMOTE_SERVER }}:${{ vars.REMOTE_DEST }}
|
scp -i ~/.ssh/id_rsa wordle ${{ vars.REMOTE_USER }}@${{ vars.REMOTE_SERVER }}:${{ vars.REMOTE_DEST }}
|
||||||
|
|
||||||
|
- name: Transfer web pages via SCP
|
||||||
|
run: |
|
||||||
|
scp -i ~/.ssh/id_rsa www/html/wordle/* ${{ vars.REMOTE_USER }}@${{ vars.REMOTE_SERVER }}:/var/www/html/wordle/
|
||||||
|
|
||||||
- name: Clean up SSH key
|
- name: Clean up SSH key
|
||||||
|
if: always() # remove the key even if a transfer failed
|
||||||
run: rm -rf ~/.ssh
|
run: rm -rf ~/.ssh
|
||||||
@@ -33,6 +33,7 @@ function main() {
|
|||||||
declare -g ANSWER
|
declare -g ANSWER
|
||||||
declare -g POOL='QWERTYUIOPASDFGHJKLZXCVBNM'
|
declare -g POOL='QWERTYUIOPASDFGHJKLZXCVBNM'
|
||||||
declare -Ag YELLOWS
|
declare -Ag YELLOWS
|
||||||
|
declare -Ag KEYS
|
||||||
ANSWER=$(select_random_word)
|
ANSWER=$(select_random_word)
|
||||||
|
|
||||||
set_options "$@"
|
set_options "$@"
|
||||||
@@ -50,24 +51,31 @@ function main() {
|
|||||||
init_board
|
init_board
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ "${ONLINE}" == 1 ]] && save_game "NO_EXIT"
|
[[ "${ONLINE}" == 1 ]] && save_game "${ROW}"
|
||||||
|
|
||||||
render_board
|
render_board
|
||||||
|
|
||||||
for (( ROW; ROW<HEIGHT; ROW++ )); do
|
for (( ROW; ROW<HEIGHT; ROW++ )); do
|
||||||
declare GUESS
|
declare GUESS
|
||||||
[[ "${KEYBOARD}" == 1 ]] && render_keyboard
|
[[ "${KEYBOARD}" == 1 ]] && render_keyboard
|
||||||
GUESS=$(retrieve_guess)
|
GUESS=$(retrieve_guess) || { echo -en "${NEWLINE}"; exit 0; }
|
||||||
check_guess "${GUESS}" "${ROW}" ; WIN=$?
|
check_guess "${GUESS}" "${ROW}" ; WIN=$?
|
||||||
|
|
||||||
|
# Save before the reveal so quitting or disconnecting can't undo a guess
|
||||||
|
if [[ $WIN == 5 ]] || (( ROW+1 == HEIGHT )); then
|
||||||
|
save_result
|
||||||
|
elif [[ "${ONLINE}" == 1 ]]; then
|
||||||
|
save_game "$(( ROW+1 ))"
|
||||||
|
fi
|
||||||
|
|
||||||
reveal_row "${ROW}"
|
reveal_row "${ROW}"
|
||||||
|
|
||||||
if [[ $WIN == 5 ]]; then
|
if [[ $WIN == 5 ]]; then
|
||||||
save_result
|
|
||||||
echo -e "${CLEAR_DOWN}YOU WIN"
|
echo -e "${CLEAR_DOWN}YOU WIN"
|
||||||
if [[ "${ONLINE}" == 1 && "${DAILY_DONE}" != 1 ]]; then
|
if [[ "${ONLINE}" == 1 && "${DAILY_DONE}" != 1 ]]; then
|
||||||
echo "Share/view your game with -> curl \"${URL_BASE}/${RESULT_FILE}\""
|
echo "Share/view your game with -> curl \"${URL_BASE}/${RESULT_FILE}\""
|
||||||
echo "Share/view your stats with -> curl \"${URL_BASE}/stats/${USER}\""
|
echo "Share/view your stats with -> curl \"${URL_BASE}/stats/${USER}\""
|
||||||
|
echo "Check out the new homepage in your browser -> ${URL_BASE}"
|
||||||
while read -r board_line; do echo "${board_line//[A-Z]/ }"; done < "${RESULT_FILE}"
|
while read -r board_line; do echo "${board_line//[A-Z]/ }"; done < "${RESULT_FILE}"
|
||||||
fi
|
fi
|
||||||
echo "Guess - $(( ROW+1 ))/6"
|
echo "Guess - $(( ROW+1 ))/6"
|
||||||
@@ -75,17 +83,12 @@ function main() {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
[[ "${BORDER}" == 1 ]] && echo -en "${CLEAR_DOWN}${INDENT}${INDENT}"
|
render_answer
|
||||||
echo -en "${CLEAR_LINE}${BOLD}"
|
|
||||||
for (( i=0; i<WIDTH; i++ )); do
|
|
||||||
echo -en " ${ANSWER:$i:1} "
|
|
||||||
done
|
|
||||||
echo -en "${NEWLINE}${RESET}"
|
|
||||||
|
|
||||||
save_result
|
|
||||||
if [[ "${ONLINE}" == 1 && "${DAILY_DONE}" != 1 ]]; then
|
if [[ "${ONLINE}" == 1 && "${DAILY_DONE}" != 1 ]]; then
|
||||||
echo "Share/view your game with -> curl \"${URL_BASE}/${RESULT_FILE}\""
|
echo "Share/view your game with -> curl \"${URL_BASE}/${RESULT_FILE}\""
|
||||||
echo "Share/view your stats with -> curl \"${URL_BASE}/stats/${USER}\""
|
echo "Share/view your stats with -> curl \"${URL_BASE}/stats/${USER}\""
|
||||||
|
echo "Check out the new homepage in your browser -> ${URL_BASE}"
|
||||||
while read -r board_line; do echo "${board_line//[A-Z]/ }"; done < "${RESULT_FILE}"
|
while read -r board_line; do echo "${board_line//[A-Z]/ }"; done < "${RESULT_FILE}"
|
||||||
fi
|
fi
|
||||||
exit 0
|
exit 0
|
||||||
@@ -137,9 +140,15 @@ function set_options() {
|
|||||||
echo -en "${CLEAR_SCREEN}"
|
echo -en "${CLEAR_SCREEN}"
|
||||||
|
|
||||||
declare -g USER
|
declare -g USER
|
||||||
read -r -p "Username: " USER
|
read -r -p "Username: " USER || exit 1
|
||||||
USER="${USER,,}"
|
USER="${USER,,}"
|
||||||
|
|
||||||
|
# Username is used in file names and URLs so only allow safe characters
|
||||||
|
while [[ -z "${USER}" || "${USER}" == *[!a-z0-9_]* || ${#USER} -gt 32 ]]; do
|
||||||
|
read -r -p "${CLEAR_UP1}Username (letters, numbers and _ only): " USER || exit 1
|
||||||
|
USER="${USER,,}"
|
||||||
|
done
|
||||||
|
|
||||||
declare -g DAY
|
declare -g DAY
|
||||||
DAY=$(( EPOCHSECONDS / 86400 ))
|
DAY=$(( EPOCHSECONDS / 86400 ))
|
||||||
|
|
||||||
@@ -173,14 +182,15 @@ function set_options() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
trap save_game SIGINT
|
# Progress is saved after every guess so Ctrl-C only needs to tidy the terminal
|
||||||
|
trap 'echo -en "${SHOW_CURSOR}${RESET}"; exit 0' SIGINT
|
||||||
;;
|
;;
|
||||||
d)
|
d)
|
||||||
DELAY=0
|
DELAY=0
|
||||||
;;
|
;;
|
||||||
h)
|
h)
|
||||||
echo -e \
|
echo -e \
|
||||||
"Usage: <program_name> [OPTIONS]
|
"Usage: wordle [OPTIONS]
|
||||||
Play a game for Wordle
|
Play a game for Wordle
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
@@ -228,29 +238,54 @@ function render_board() {
|
|||||||
[[ "${BORDER}" == 1 ]] && echo -e "└─────────────────┘"
|
[[ "${BORDER}" == 1 ]] && echo -e "└─────────────────┘"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function render_answer() {
|
||||||
|
[[ "${BORDER}" == 1 ]] && echo -en "${CLEAR_DOWN}${INDENT}${INDENT}"
|
||||||
|
echo -en "${CLEAR_LINE}${BOLD}"
|
||||||
|
for (( i=0; i<WIDTH; i++ )); do
|
||||||
|
echo -en " ${ANSWER:$i:1} "
|
||||||
|
done
|
||||||
|
echo -en "${NEWLINE}${RESET}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Exits non-zero when input has ended so the caller can quit
|
||||||
function retrieve_guess() {
|
function retrieve_guess() {
|
||||||
if [[ -n $1 ]]; then
|
if [[ -n $1 ]]; then
|
||||||
read -r -p "${CLEAR_UP1}$1: " GUESS
|
read -r -p "${CLEAR_UP1}$1: " GUESS || end_of_input
|
||||||
else
|
else
|
||||||
read -r -p "${CLEAR_2END}Enter Guess: " GUESS
|
read -r -p "${CLEAR_2END}Enter Guess: " GUESS || end_of_input
|
||||||
fi
|
fi
|
||||||
|
|
||||||
while [[ ${#GUESS} -ne 5 ]] || [[ ! "${GUESS,,}" =~ ^[a-z]{5}$ ]]; do
|
while [[ ${#GUESS} -ne 5 ]] || [[ ! "${GUESS,,}" =~ ^[a-z]{5}$ ]]; do
|
||||||
read -r -p "${CLEAR_UP1}Must be 5 letters: " GUESS
|
read -r -p "${CLEAR_UP1}Must be 5 letters: " GUESS || end_of_input
|
||||||
done
|
done
|
||||||
declare valid_guess
|
declare valid_guess
|
||||||
validate_guess "${GUESS}" ; valid_guess=$?
|
validate_guess "${GUESS}" ; valid_guess=$?
|
||||||
|
|
||||||
[[ ${valid_guess} == 1 ]] && GUESS=$(retrieve_guess "Not A Valid Word")
|
declare message
|
||||||
[[ ${valid_guess} == 2 ]] && GUESS=$(retrieve_guess "Not A Valid Guess")
|
declare -r pos="${HARD_MODE_POS}"
|
||||||
[[ ${valid_guess} == 3 ]] && GUESS=$(retrieve_guess "GREEN Issue")
|
declare -r letter="${GUESS:pos:1}"
|
||||||
[[ ${valid_guess} == 4 ]] && GUESS=$(retrieve_guess "YELLOW Issue")
|
declare -ra ordinals=(1st 2nd 3rd 4th 5th)
|
||||||
[[ ${valid_guess} == 5 ]] && GUESS=$(retrieve_guess "POOL Issue")
|
[[ ${valid_guess} == 1 ]] && message="Not A Valid Word"
|
||||||
[[ ${valid_guess} == 6 ]] && GUESS=$(retrieve_guess "NEW Issue")
|
[[ ${valid_guess} == 3 ]] && message="${ordinals[pos]} letter must be ${greens[pos]}"
|
||||||
|
[[ ${valid_guess} == 4 ]] && message="Must contain ${yellows[pos]}"
|
||||||
|
[[ ${valid_guess} == 5 ]] && message="${letter^^} is not in the word"
|
||||||
|
[[ ${valid_guess} == 6 ]] && message="${ordinals[pos]} letter can't be ${letter^^}"
|
||||||
|
|
||||||
|
if [[ -n "${message}" ]]; then
|
||||||
|
GUESS=$(retrieve_guess "${message}") || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo -n "${GUESS^^}"
|
echo -n "${GUESS^^}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Called when read gets Ctrl-D. On a terminal treat it like Enter, but if
|
||||||
|
# input has really ended (closed pipe) exit, or the retry loop spins forever
|
||||||
|
function end_of_input() {
|
||||||
|
[[ -t 0 ]] || exit 1
|
||||||
|
# Ctrl-D doesn't move to a new line like Enter, which the prompts expect
|
||||||
|
echo -en "${NEWLINE}" >&2
|
||||||
|
}
|
||||||
|
|
||||||
function render_keyboard() {
|
function render_keyboard() {
|
||||||
declare pos=$(( BORDER*2+7 ))
|
declare pos=$(( BORDER*2+7 ))
|
||||||
echo -en "${RESET}\e[$pos;0H"
|
echo -en "${RESET}\e[$pos;0H"
|
||||||
@@ -260,7 +295,12 @@ function render_keyboard() {
|
|||||||
elif [[ $i == 19 ]]; then
|
elif [[ $i == 19 ]]; then
|
||||||
echo -en "${NEWLINE} "
|
echo -en "${NEWLINE} "
|
||||||
fi
|
fi
|
||||||
echo -n "${POOL:$i:1} "
|
declare key="${POOL:$i:1}"
|
||||||
|
if [[ -n "${KEYS[$key]}" ]]; then
|
||||||
|
echo -n "${KEYS[$key]}${BOLD}${LETTER}${key}${RESET} "
|
||||||
|
else
|
||||||
|
echo -n "${key} "
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
echo -en "${NEWLINE}"
|
echo -en "${NEWLINE}"
|
||||||
}
|
}
|
||||||
@@ -286,6 +326,8 @@ function hard_mode_check() {
|
|||||||
declare GUESS=$1
|
declare GUESS=$1
|
||||||
|
|
||||||
for (( i=0; i<WIDTH; i++ )); do
|
for (( i=0; i<WIDTH; i++ )); do
|
||||||
|
HARD_MODE_POS=$i # letter position that failed, for the error message
|
||||||
|
|
||||||
if [[ "${STRICT_HARD_MODE}" == 1 ]]; then
|
if [[ "${STRICT_HARD_MODE}" == 1 ]]; then
|
||||||
[[ "${POOL}" == *${GUESS:i:1}* ]] || return 5
|
[[ "${POOL}" == *${GUESS:i:1}* ]] || return 5
|
||||||
fi
|
fi
|
||||||
@@ -301,7 +343,7 @@ function hard_mode_check() {
|
|||||||
for (( r=ROW-1; r>=0; r-- )); do
|
for (( r=ROW-1; r>=0; r-- )); do
|
||||||
CHAR="${GUESS:i:1}"
|
CHAR="${GUESS:i:1}"
|
||||||
PREV_CHAR="${YELLOWS[$r,$i]}"
|
PREV_CHAR="${YELLOWS[$r,$i]}"
|
||||||
[[ "${CHAR}" == "${PREV_CHAR}" ]] && return 4
|
[[ "${CHAR}" == "${PREV_CHAR}" ]] && return 6
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -331,6 +373,7 @@ function check_guess() {
|
|||||||
|
|
||||||
if [[ "${_char}" == "${_answer:$c:1}" ]]; then
|
if [[ "${_char}" == "${_answer:$c:1}" ]]; then
|
||||||
greens[c]="${_char}"
|
greens[c]="${_char}"
|
||||||
|
KEYS[$_char]="${GREEN}"
|
||||||
BOARD[$_row,$c]="${GREEN}${BOLD}${LETTER} ${_char} ${UNBOLD}"
|
BOARD[$_row,$c]="${GREEN}${BOLD}${LETTER} ${_char} ${UNBOLD}"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
@@ -339,6 +382,7 @@ function check_guess() {
|
|||||||
chars="${chars/${_char}/}"
|
chars="${chars/${_char}/}"
|
||||||
yellows[c]="${_char}" # volatile - stores previous row yellows
|
yellows[c]="${_char}" # volatile - stores previous row yellows
|
||||||
YELLOWS[$_row,$c]="${_char}" # stores all yellow from all rounds
|
YELLOWS[$_row,$c]="${_char}" # stores all yellow from all rounds
|
||||||
|
[[ "${KEYS[$_char]}" != "${GREEN}" ]] && KEYS[$_char]="${YELLOW}"
|
||||||
BOARD[$_row,$c]="${YELLOW}${BOLD}${LETTER} ${_char} ${UNBOLD}"
|
BOARD[$_row,$c]="${YELLOW}${BOLD}${LETTER} ${_char} ${UNBOLD}"
|
||||||
fi
|
fi
|
||||||
[[ "${ANSWER}" =~ ${_char} ]] && continue
|
[[ "${ANSWER}" =~ ${_char} ]] && continue
|
||||||
@@ -370,8 +414,9 @@ function load_game() {
|
|||||||
source "${PROGRESS_FILE}"
|
source "${PROGRESS_FILE}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# shellcheck disable=SC2317 # Don't warn about unreachable commands in this function
|
# Takes the row the game should resume from
|
||||||
function save_game() {
|
function save_game() {
|
||||||
|
declare -r row=$1
|
||||||
: > "${PROGRESS_FILE}"
|
: > "${PROGRESS_FILE}"
|
||||||
|
|
||||||
for key in "${!BOARD[@]}"; do
|
for key in "${!BOARD[@]}"; do
|
||||||
@@ -382,8 +427,21 @@ function save_game() {
|
|||||||
echo "YELLOWS[$key]='${YELLOWS[$key]}'" >> "${PROGRESS_FILE}"
|
echo "YELLOWS[$key]='${YELLOWS[$key]}'" >> "${PROGRESS_FILE}"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
for key in "${!KEYS[@]}"; do
|
||||||
|
echo "KEYS[$key]='${KEYS[$key]}'" >> "${PROGRESS_FILE}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Previous row's hints, needed by hard_mode_check after a resume
|
||||||
|
for key in "${!greens[@]}"; do
|
||||||
|
echo "greens[$key]='${greens[$key]}'" >> "${PROGRESS_FILE}"
|
||||||
|
done
|
||||||
|
|
||||||
|
for key in "${!yellows[@]}"; do
|
||||||
|
echo "yellows[$key]='${yellows[$key]}'" >> "${PROGRESS_FILE}"
|
||||||
|
done
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "ROW=${ROW}"
|
echo "ROW=${row}"
|
||||||
echo "POOL=${POOL}"
|
echo "POOL=${POOL}"
|
||||||
echo "HARD_MODE=${HARD_MODE}"
|
echo "HARD_MODE=${HARD_MODE}"
|
||||||
echo "STRICT_HARD_MODE=${STRICT_HARD_MODE}"
|
echo "STRICT_HARD_MODE=${STRICT_HARD_MODE}"
|
||||||
@@ -392,26 +450,17 @@ function save_game() {
|
|||||||
echo "IN_PROGRESS=1"
|
echo "IN_PROGRESS=1"
|
||||||
[[ "${PLAYED}" == 1 ]] && echo 'PLAYED=1'
|
[[ "${PLAYED}" == 1 ]] && echo 'PLAYED=1'
|
||||||
} >> "${PROGRESS_FILE}"
|
} >> "${PROGRESS_FILE}"
|
||||||
|
|
||||||
echo -en "${RESET}"
|
|
||||||
[[ -z $1 ]] && exit 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function save_result() {
|
function save_result() {
|
||||||
[[ "${ONLINE}" == 0 ]] && return 0
|
[[ "${ONLINE}" == 0 ]] && return 0
|
||||||
[[ "${ONLINE}" == 1 ]] && update_stats
|
|
||||||
if [[ "${ONLINE}" == 1 ]] && [[ "${DAILY_DONE}" != 1 ]]; then
|
if [[ "${ONLINE}" == 1 ]] && [[ "${DAILY_DONE}" != 1 ]]; then
|
||||||
|
update_stats
|
||||||
render_board > "${RESULT_FILE}"
|
render_board > "${RESULT_FILE}"
|
||||||
render_board > "${CUR_RESULT_FILE}"
|
render_board > "${CUR_RESULT_FILE}"
|
||||||
if [[ $WIN != 5 ]]; then
|
if [[ $WIN != 5 ]]; then
|
||||||
[[ "${BORDER}" == 1 ]] && echo -en "${CLEAR_DOWN}${INDENT}${INDENT}" >> "${RESULT_FILE}"
|
render_answer >> "${RESULT_FILE}"
|
||||||
echo -en "${CLEAR_LINE}${BOLD}" >> "${RESULT_FILE}"
|
render_answer >> "${CUR_RESULT_FILE}"
|
||||||
for (( i=0; i<WIDTH; i++ )); do
|
|
||||||
echo -en " ${ANSWER:$i:1} " >> "${RESULT_FILE}"
|
|
||||||
echo -en " ${ANSWER:$i:1} " >> "${CUR_RESULT_FILE}"
|
|
||||||
done
|
|
||||||
echo -en "${NEWLINE}${RESET}" >> "${RESULT_FILE}"
|
|
||||||
echo -en "${NEWLINE}${RESET}" >> "${CUR_RESULT_FILE}"
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
echo 'PLAYED=1' > "${PROGRESS_FILE}"
|
echo 'PLAYED=1' > "${PROGRESS_FILE}"
|
||||||
@@ -451,6 +500,7 @@ function update_stats() {
|
|||||||
integer_part=$(( result / 100 ))
|
integer_part=$(( result / 100 ))
|
||||||
decimal_part=$(( result % 100 ))
|
decimal_part=$(( result % 100 ))
|
||||||
|
|
||||||
|
[[ ${decimal_part} -lt 10 ]] && decimal_part="0${decimal_part}"
|
||||||
AVERAGE="${integer_part}.${decimal_part}"
|
AVERAGE="${integer_part}.${decimal_part}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,407 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Wordle Board Viewer</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: #111;
|
||||||
|
color: #eee;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 26px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-label {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a {
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0.35rem 0.9rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #2f5740;
|
||||||
|
background: #162218;
|
||||||
|
color: #9af7b8;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
transition: background 0.2s, transform 0.1s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.nav a:hover {
|
||||||
|
background: #1c3421;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 0 6px rgba(81,255,122,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- grid --- */
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 50px);
|
||||||
|
grid-gap: 6px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 0 4px rgba(0,0,0,0.5);
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
background: #333 !important;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reveal button */
|
||||||
|
#reveal {
|
||||||
|
padding: 10px 18px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
transition: transform 0.1s ease, box-shadow 0.1s ease;
|
||||||
|
}
|
||||||
|
#reveal:hover {
|
||||||
|
background: #45A047;
|
||||||
|
}
|
||||||
|
#reveal.pressed {
|
||||||
|
transform: translateY(2px);
|
||||||
|
box-shadow: inset 0 0 10px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Raw ANSI box (hidden) */
|
||||||
|
pre {
|
||||||
|
background: #222;
|
||||||
|
color: #ccc;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600px;
|
||||||
|
overflow-x: auto;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Error box (nice green glow) --- */
|
||||||
|
.error-box {
|
||||||
|
background: #141414;
|
||||||
|
border: 1px solid #2f5740;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-top: 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 0 20px #000 inset, 0 0 15px rgba(81,255,122,0.12);
|
||||||
|
max-width: 600px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<link rel="icon" type="image/png" href="https://www.adbraun.lol/wordle/favicon.ico"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h2 id="title">Wordle Board</h2>
|
||||||
|
<div id="date" class="date-label"></div>
|
||||||
|
|
||||||
|
<div class="nav">
|
||||||
|
<a href="/wordle" id="search-link">🔍 Search users</a>
|
||||||
|
<a href="#" id="stats-link">📊 Back to stats</a>
|
||||||
|
<a href="#" id="games-link">📅 Recent games</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="grid-container"></div>
|
||||||
|
|
||||||
|
<button id="reveal">Reveal Letters</button>
|
||||||
|
|
||||||
|
<div id="error" class="error-box" style="display:none;"></div>
|
||||||
|
|
||||||
|
<pre id="raw-output"></pre>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* Convert xterm colors */
|
||||||
|
function xtermToRgb(n) {
|
||||||
|
// Custom overrides for Wordle colors
|
||||||
|
if (n === 10) return "rgb(44, 192, 44)"; // green
|
||||||
|
if (n === 3) return "rgb(176, 176, 33)"; // yellow
|
||||||
|
|
||||||
|
// Default behavior for all other xterm colors
|
||||||
|
if (n < 16) {
|
||||||
|
const base = [
|
||||||
|
'#000000','#800000','#008000','#808000',
|
||||||
|
'#000080','#800080','#008080','#c0c0c0',
|
||||||
|
'#808080','#ff0000','#00ff00','#ffff00',
|
||||||
|
'#0000ff','#ff00ff','#00ffff','#ffffff'
|
||||||
|
];
|
||||||
|
return base[n] || '#000';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n <= 231) {
|
||||||
|
n -= 16;
|
||||||
|
const r = Math.floor(n / 36) % 6;
|
||||||
|
const g = Math.floor(n / 6) % 6;
|
||||||
|
const b = n % 6;
|
||||||
|
const f = x => (x === 0 ? 0 : x * 40 + 55);
|
||||||
|
return `rgb(${f(r)}, ${f(g)}, ${f(b)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const c = (n - 232) * 10 + 8;
|
||||||
|
return `rgb(${c}, ${c}, ${c})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse ANSI board text into rows of {char, bg} */
|
||||||
|
function parseAnsiBoard(text) {
|
||||||
|
const lines = text.split("\n").filter(l => l.includes("│"));
|
||||||
|
const board = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const inside = line.replace(/.*│(.*)│.*/, "$1").trim();
|
||||||
|
let row = [];
|
||||||
|
let bg = "#000";
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < inside.length) {
|
||||||
|
if (inside[i] === "\x1b") {
|
||||||
|
const end = inside.indexOf("m", i);
|
||||||
|
const seq = inside.substring(i+2, end).split(";").map(Number);
|
||||||
|
|
||||||
|
if (seq[0] === 48 && seq[1] === 5) {
|
||||||
|
bg = xtermToRgb(seq[2]);
|
||||||
|
} else if (seq[0] === 0) {
|
||||||
|
bg = "#000";
|
||||||
|
}
|
||||||
|
|
||||||
|
i = end + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/[A-Z]/.test(inside[i])) {
|
||||||
|
row.push({ char: inside[i], bg });
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.length === 5) board.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return board;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fetch an epoch-day-based board (user-<day>.board) */
|
||||||
|
async function fetchBoard(user, epochDay) {
|
||||||
|
const boardName = `${user}-${epochDay}.board`;
|
||||||
|
const url = `/wordle/rawboard/${boardName}`;
|
||||||
|
const resp = await fetch(url);
|
||||||
|
if (!resp.ok) return null;
|
||||||
|
const text = await resp.text();
|
||||||
|
return { boardName, text };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBoard() {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const boardName = params.get("board")?.toLowerCase(); // game saves names in lowercase
|
||||||
|
|
||||||
|
const errorBox = document.getElementById("error");
|
||||||
|
const dateEl = document.getElementById("date");
|
||||||
|
const titleEl = document.getElementById("title");
|
||||||
|
const gridContainer = document.getElementById("grid-container");
|
||||||
|
const rawOutput = document.getElementById("raw-output");
|
||||||
|
const revealBtn = document.getElementById("reveal");
|
||||||
|
|
||||||
|
// Hide button until we know we have a board
|
||||||
|
revealBtn.style.display = "none";
|
||||||
|
|
||||||
|
if (!boardName) {
|
||||||
|
errorBox.style.display = "block";
|
||||||
|
errorBox.textContent = "No board specified.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = boardName.replace(/-.*$/, "").replace(".board", "");
|
||||||
|
titleEl.textContent = `Wordle Board for ${user}`;
|
||||||
|
|
||||||
|
// Wire nav
|
||||||
|
document.getElementById("stats-link").href = `/stats.html?user=${encodeURIComponent(user)}`;
|
||||||
|
document.getElementById("games-link").href = `/games.html?user=${encodeURIComponent(user)}`;
|
||||||
|
|
||||||
|
let finalText = "";
|
||||||
|
let finalEpoch = null;
|
||||||
|
|
||||||
|
const idMatch = boardName.match(/-(\d+)\.board$/);
|
||||||
|
|
||||||
|
// Case 1: boardName has ID → user-<epochDay>.board
|
||||||
|
if (idMatch) {
|
||||||
|
const epochDay = Number(idMatch[1]);
|
||||||
|
const r = await fetchBoard(user, epochDay);
|
||||||
|
|
||||||
|
// If not found OR text-only (no ANSI), this day has no board
|
||||||
|
if (!r || !r.text.includes("\x1b[")) {
|
||||||
|
errorBox.style.display = "block";
|
||||||
|
errorBox.textContent = "This board does not exist for that day.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
finalText = r.text;
|
||||||
|
finalEpoch = epochDay;
|
||||||
|
}
|
||||||
|
// Case 2: boardName has NO ID → "user.board" semantics
|
||||||
|
else {
|
||||||
|
const today = Math.floor(Date.now() / 1000 / 86400);
|
||||||
|
|
||||||
|
// First: check today's epoch board (user-<today>.board)
|
||||||
|
const rToday = await fetchBoard(user, today);
|
||||||
|
if (rToday && rToday.text.includes("\x1b[")) {
|
||||||
|
finalText = rToday.text;
|
||||||
|
finalEpoch = today;
|
||||||
|
} else {
|
||||||
|
// Fallback: last board played via /wordle/rawboard/user.board
|
||||||
|
const latestUrl = `/wordle/rawboard/${encodeURIComponent(user)}.board`;
|
||||||
|
const latestResp = await fetch(latestUrl);
|
||||||
|
|
||||||
|
if (!latestResp.ok) {
|
||||||
|
errorBox.style.display = "block";
|
||||||
|
errorBox.textContent = "This user has no recorded boards.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestText = await latestResp.text();
|
||||||
|
|
||||||
|
// If that "latest" file is only text (no ANSI), treat as no boards
|
||||||
|
if (!latestText.includes("\x1b[")) {
|
||||||
|
errorBox.style.display = "block";
|
||||||
|
errorBox.textContent = "This user has no recorded boards.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
finalText = latestText;
|
||||||
|
finalEpoch = null;
|
||||||
|
dateEl.textContent = "Last board played";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we know the exact epoch day and haven't set date text yet, show it
|
||||||
|
if (finalEpoch !== null && !dateEl.textContent) {
|
||||||
|
// Epoch days start at midnight UTC, so format in UTC or viewers
|
||||||
|
// west of UTC see the previous day
|
||||||
|
dateEl.textContent = new Date(finalEpoch * 86400 * 1000)
|
||||||
|
.toLocaleDateString(undefined, {
|
||||||
|
timeZone: "UTC",
|
||||||
|
weekday: "long",
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
rawOutput.textContent = finalText;
|
||||||
|
|
||||||
|
let parsed = parseAnsiBoard(finalText);
|
||||||
|
|
||||||
|
// If somehow there is no parsed grid (safety), treat as "no boards"
|
||||||
|
if (!parsed.length) {
|
||||||
|
errorBox.style.display = "block";
|
||||||
|
errorBox.textContent = "This user has no recorded boards.";
|
||||||
|
gridContainer.innerHTML = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always fill to 6 rows with empties for a full Wordle grid
|
||||||
|
while (parsed.length < 6) {
|
||||||
|
parsed.push([
|
||||||
|
{ char: " ", bg: "#333", empty: true },
|
||||||
|
{ char: " ", bg: "#333", empty: true },
|
||||||
|
{ char: " ", bg: "#333", empty: true },
|
||||||
|
{ char: " ", bg: "#333", empty: true },
|
||||||
|
{ char: " ", bg: "#333", empty: true }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const grid = document.createElement("div");
|
||||||
|
grid.className = "grid";
|
||||||
|
|
||||||
|
parsed.forEach(row => {
|
||||||
|
row.forEach(cell => {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.className = "tile hidden";
|
||||||
|
div.textContent = cell.char.trim() === "" ? "" : cell.char;
|
||||||
|
if (cell.empty) {
|
||||||
|
div.classList.add("empty");
|
||||||
|
} else {
|
||||||
|
div.style.backgroundColor = cell.bg;
|
||||||
|
}
|
||||||
|
grid.appendChild(div);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
gridContainer.innerHTML = "";
|
||||||
|
gridContainer.appendChild(grid);
|
||||||
|
|
||||||
|
// Now that we have a real board, show reveal button and wire press-to-reveal behavior
|
||||||
|
revealBtn.style.display = "inline-block";
|
||||||
|
|
||||||
|
const tiles = document.querySelectorAll(".tile");
|
||||||
|
|
||||||
|
function showLetters() {
|
||||||
|
tiles.forEach(t => t.classList.remove("hidden"));
|
||||||
|
revealBtn.classList.add("pressed");
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLetters() {
|
||||||
|
tiles.forEach(t => t.classList.add("hidden"));
|
||||||
|
revealBtn.classList.remove("pressed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse support
|
||||||
|
revealBtn.addEventListener("mousedown", showLetters);
|
||||||
|
document.addEventListener("mouseup", hideLetters);
|
||||||
|
|
||||||
|
// Touch support
|
||||||
|
revealBtn.addEventListener("touchstart", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
showLetters();
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
document.addEventListener("touchend", hideLetters);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBoard();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Recent Wordle Games</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: #0d0d0d;
|
||||||
|
color: #f2f2f2;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
padding: 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 700px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
animation: fadein 0.7s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #51ff7a;
|
||||||
|
text-shadow: 0 0 8px rgba(81,255,122,0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
color: #b0ffca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a {
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0.4rem 0.9rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #2f5740;
|
||||||
|
background: #162218;
|
||||||
|
color: #9af7b8;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
transition: background 0.2s, transform 0.1s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a:hover {
|
||||||
|
background: #1c3421;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 0 6px rgba(81,255,122,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.games-card {
|
||||||
|
background: #141414;
|
||||||
|
border: 1px solid #2f5740;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 0 20px #000 inset, 0 0 15px rgba(81,255,122,0.12);
|
||||||
|
text-align: left;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.games {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.games li {
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
border-bottom: 1px solid #252525;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.games li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-date {
|
||||||
|
color: #e0ffe8;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #51ff7a;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-msg {
|
||||||
|
color: #aaa;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<link rel="icon" type="image/png" href="https://www.adbraun.lol/wordle/favicon.ico"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1>Recent Games for <span id="username">...</span></h1>
|
||||||
|
<div class="subtitle">Last 30 days (only days actually played)</div>
|
||||||
|
|
||||||
|
<div class="nav">
|
||||||
|
<a href="/wordle" id="search-link">🔍 Search users</a>
|
||||||
|
<a href="#" id="stats-link">📊 Back to stats</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="games-card">
|
||||||
|
<ul class="games" id="games-list"></ul>
|
||||||
|
<div id="empty" class="empty-msg" style="display:none;">No games in the last 30 days.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function formatDateFromEpochDays(epochDays) {
|
||||||
|
// Epoch days start at midnight UTC, so format in UTC or viewers
|
||||||
|
// west of UTC see the previous day
|
||||||
|
const date = new Date(epochDays * 86400 * 1000);
|
||||||
|
return date.toLocaleDateString(undefined, {
|
||||||
|
timeZone: "UTC",
|
||||||
|
weekday: "short",
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const user = params.get("user")?.toLowerCase(); // game saves names in lowercase
|
||||||
|
const usernameSpan = document.getElementById("username");
|
||||||
|
const gamesList = document.getElementById("games-list");
|
||||||
|
const emptyMsg = document.getElementById("empty");
|
||||||
|
const statsLink = document.getElementById("stats-link");
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
usernameSpan.textContent = "Unknown";
|
||||||
|
emptyMsg.style.display = "block";
|
||||||
|
emptyMsg.textContent = "Error: missing ?user=username in URL.";
|
||||||
|
statsLink.removeAttribute("href");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
usernameSpan.textContent = user;
|
||||||
|
statsLink.href = `/stats.html?user=${encodeURIComponent(user)}`;
|
||||||
|
|
||||||
|
const todayEpochDays = Math.floor(Date.now() / 1000 / 86400);
|
||||||
|
const games = [];
|
||||||
|
|
||||||
|
// Look back over last 30 days
|
||||||
|
for (let d = todayEpochDays; d > todayEpochDays - 30; d--) {
|
||||||
|
const boardName = `${user}-${d}.board`;
|
||||||
|
const url = `/wordle/rawboard/${encodeURIComponent(boardName)}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url);
|
||||||
|
if (!resp.ok) continue;
|
||||||
|
|
||||||
|
const text = await resp.text();
|
||||||
|
|
||||||
|
// A "real" game day has ANSI escape codes
|
||||||
|
if (text.includes("\x1b[")) {
|
||||||
|
games.push({ epochDays: d, boardName });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore errors for individual days
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!games.length) {
|
||||||
|
emptyMsg.style.display = "block";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gamesList.innerHTML = "";
|
||||||
|
games.forEach(game => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
|
||||||
|
const dateSpan = document.createElement("span");
|
||||||
|
dateSpan.className = "game-date";
|
||||||
|
dateSpan.textContent = formatDateFromEpochDays(game.epochDays);
|
||||||
|
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.className = "game-link";
|
||||||
|
link.href = `/game.html?board=${encodeURIComponent(game.boardName)}`;
|
||||||
|
link.textContent = "View board";
|
||||||
|
|
||||||
|
li.appendChild(dateSpan);
|
||||||
|
li.appendChild(link);
|
||||||
|
gamesList.appendChild(li);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Wordle Viewer</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: #0b0b0b;
|
||||||
|
color: #f2f2f2;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: #141414;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem 2.5rem;
|
||||||
|
box-shadow: 0 0 30px rgba(0,0,0,0.7);
|
||||||
|
max-width: 420px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #51ff7a;
|
||||||
|
text-shadow: 0 0 10px rgba(81,255,122,0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.6rem 0.7rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
background: #1c1c1c;
|
||||||
|
color: #f2f2f2;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #51ff7a;
|
||||||
|
box-shadow: 0 0 6px rgba(81,255,122,0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
|
background: #51ff7a;
|
||||||
|
color: #061006;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #3ee067;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<link rel="icon" type="image/png" href="https://www.adbraun.lol/wordle/favicon.ico"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1>Wordle Viewer</h1>
|
||||||
|
<p>Enter a username to view their stats and games.</p>
|
||||||
|
|
||||||
|
<form id="search-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<input id="user-input" type="text" placeholder="Username (e.g. adam)" autocomplete="off" />
|
||||||
|
<button type="submit">View stats</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="hint">
|
||||||
|
You’ll be taken to <code>stats.html?user=<name></code>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const form = document.getElementById("search-form");
|
||||||
|
const input = document.getElementById("user-input");
|
||||||
|
|
||||||
|
form.addEventListener("submit", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// The game saves usernames in lowercase
|
||||||
|
const user = input.value.trim().toLowerCase();
|
||||||
|
if (!user) return;
|
||||||
|
window.location.href = `/stats.html?user=${encodeURIComponent(user)}`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Wordle Stats</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: #0d0d0d;
|
||||||
|
color: #f2f2f2;
|
||||||
|
font-family: monospace;
|
||||||
|
padding: 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 700px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
animation: fadein 0.7s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #51ff7a;
|
||||||
|
text-shadow: 0 0 8px rgba(81,255,122,0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
color: #b0ffca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a {
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0.4rem 0.9rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #2f5740;
|
||||||
|
background: #162218;
|
||||||
|
color: #9af7b8;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
transition: background 0.2s, transform 0.1s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a:hover {
|
||||||
|
background: #1c3421;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 0 6px rgba(81,255,122,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
background: #141414;
|
||||||
|
border: 1px solid #2f5740;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 0 20px #000 inset, 0 0 15px rgba(81,255,122,0.12);
|
||||||
|
text-align: left;
|
||||||
|
margin: 0 auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: inherit;
|
||||||
|
margin-top: 1rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ANSI color classes */
|
||||||
|
.ansi-black { color: #000; }
|
||||||
|
.ansi-red { color: #ff6b6b; }
|
||||||
|
.ansi-green { color: #51ff7a; }
|
||||||
|
.ansi-yellow { color: #ffe66d; }
|
||||||
|
.ansi-blue { color: #4dc0ff; }
|
||||||
|
.ansi-magenta { color: #ff70ff; }
|
||||||
|
.ansi-cyan { color: #63e6ff; }
|
||||||
|
.ansi-white { color: #ddd; }
|
||||||
|
</style>
|
||||||
|
<link rel="icon" type="image/png" href="https://www.adbraun.lol/wordle/favicon.ico"/>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<h1>Wordle Stats for <span id="username">...</span></h1>
|
||||||
|
<div class="subtitle">Daily performance summary</div>
|
||||||
|
|
||||||
|
<div class="nav">
|
||||||
|
<a href="/wordle" id="search-link">🔍 Search users</a>
|
||||||
|
<a href="#" id="games-link">📅 Recent games</a>
|
||||||
|
<a href="#" id="latest-board-link">🧩 Latest board</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-card">
|
||||||
|
<pre id="output">Loading…</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(async () => {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const user = params.get("user")?.toLowerCase(); // game saves names in lowercase
|
||||||
|
const output = document.getElementById("output");
|
||||||
|
const usernameSpan = document.getElementById("username");
|
||||||
|
const gamesLink = document.getElementById("games-link");
|
||||||
|
const latestBoardLink = document.getElementById("latest-board-link");
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
usernameSpan.textContent = "Unknown";
|
||||||
|
output.textContent = "Error: missing ?user=username in URL.";
|
||||||
|
gamesLink.removeAttribute("href");
|
||||||
|
latestBoardLink.removeAttribute("href");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
usernameSpan.textContent = user;
|
||||||
|
|
||||||
|
// Wire nav links for this user
|
||||||
|
gamesLink.href = `/games.html?user=${encodeURIComponent(user)}`;
|
||||||
|
latestBoardLink.href = `/game.html?board=${encodeURIComponent(user)}.board`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/wordle/rawstats/${encodeURIComponent(user)}`);
|
||||||
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
|
|
||||||
|
let text = await resp.text();
|
||||||
|
|
||||||
|
const ansiMap = {
|
||||||
|
'30': 'ansi-black',
|
||||||
|
'31': 'ansi-red',
|
||||||
|
'32': 'ansi-green',
|
||||||
|
'33': 'ansi-yellow',
|
||||||
|
'34': 'ansi-blue',
|
||||||
|
'35': 'ansi-magenta',
|
||||||
|
'36': 'ansi-cyan',
|
||||||
|
'37': 'ansi-white'
|
||||||
|
};
|
||||||
|
|
||||||
|
text = text.replace(/\x1b\[(\d+)m/g, (match, code) => {
|
||||||
|
if (code === "0") return "</span>";
|
||||||
|
const cls = ansiMap[code];
|
||||||
|
return cls ? `<span class="${cls}">` : "";
|
||||||
|
});
|
||||||
|
|
||||||
|
if ((text.match(/<span/g) || []).length > (text.match(/<\/span>/g) || []).length) {
|
||||||
|
text += "</span>";
|
||||||
|
}
|
||||||
|
|
||||||
|
output.innerHTML = text;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
output.textContent = `Error loading stats for ${user}: ${err.message}`;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
Reference in new issue
Block a user