|
#!/usr/bin/env bash |
|
|
|
# Fallback for systems without `tput` (man terminfo). |
|
type tput &>/dev/null || tput() { |
|
case "$1" in |
|
cols) echo 80;; |
|
lines) echo 24;; |
|
clear) printf '\033[2J\033[;H';; # Clear screen and move cursor top/left. |
|
civis) printf '\033[?25l';; # Hide cursor. |
|
cnorm) printf '\033[?25h';; # Restore cursor to normal. |
|
cup) printf '\033[%d;%dH' $(($2 + 1)) $(($3 + 1));; # Move to y/x. |
|
esac |
|
} |
|
|
|
# Restore terminal defaults and end quiz. |
|
restore() { |
|
# Clear screen and show cursor. |
|
tput clear |
|
tput cnorm |
|
# Reset to default values. |
|
stty sane |
|
} |
|
|
|
# Move cursor |
|
# |
|
# @param 1 - vertical coordinate |
|
# @param 2 - horizontal coordinate |
|
cup() { |
|
tput cup "$((PAD_TOP + $1))" "$((PAD_LEFT + $2))" |
|
} |
|
|
|
# Reveal current position. |
|
reveal() { |
|
local RADIUS=1 |
|
local TOP=$((PY - RADIUS)) |
|
TOP=$((TOP > -1 ? TOP : 0)) |
|
local LEFT=$((PX - RADIUS)) |
|
LEFT=$((LEFT > -1 ? LEFT : 0)) |
|
local BOTTOM=$((PY + RADIUS)) |
|
BOTTOM=$((BOTTOM < HEIGHT ? BOTTOM : HEIGHT - 1)) |
|
local RIGHT=$((PX + RADIUS)) |
|
RIGHT=$((RIGHT < WIDTH ? RIGHT : WIDTH - 1)) |
|
local LEN=$((RIGHT - LEFT + 1)) Y |
|
for ((Y=TOP; Y <= BOTTOM; ++Y)) |
|
do |
|
cup "$Y" "$LEFT" |
|
echo -n "${MAP:$((Y * WIDTH + LEFT)):$LEN}" |
|
done |
|
} |
|
|
|
# Move player |
|
# |
|
# @param 1 - horizontal displacement |
|
# @param 2 - vertical displacement |
|
move() { |
|
local X=$((PX + $1)) Y=$((PY + $2)) |
|
((X > -1 && X < WIDTH && Y > -1 && Y < HEIGHT)) || return |
|
PX=$X |
|
PY=$Y |
|
((++MOVES)) |
|
reveal |
|
} |
|
|
|
# Process input from STDIN. |
|
input() { |
|
read -r -n 1 |
|
case "$REPLY" in |
|
''|q) return 1;; |
|
[kw]) move 0 -1;; |
|
[ld]) move 1 0;; |
|
[ha]) move -1 0;; |
|
[js]) move 0 1;; |
|
esac |
|
return 0 |
|
} |
|
|
|
# Draw player. |
|
draw_player() { |
|
cup "$PY" "$PX" |
|
echo -n '@' |
|
} |
|
|
|
# Inspect map. |
|
inspect() { |
|
# Stop if STDIN isn't a terminal. |
|
[ -t 0 ] || return 1 |
|
|
|
# Stop echoing typed characters. |
|
stty -echo |
|
|
|
# Clear screen and hide cursor. |
|
tput clear |
|
tput civis |
|
|
|
# Reveal some random places to give some orientation. |
|
local PX PY |
|
for ((I=10; I-- > 0;)) |
|
do |
|
PX=$((RANDOM % WIDTH)) |
|
PY=$((RANDOM % HEIGHT)) |
|
cup "$PY" "$PX" |
|
echo -n "${MAP:$((PY * WIDTH + PX)):1}" |
|
done |
|
|
|
# Center player on screen. |
|
PX=$((WIDTH / 2)) |
|
PY=$((HEIGHT / 2)) |
|
reveal |
|
|
|
while true |
|
do |
|
draw_player |
|
input || break |
|
done |
|
restore |
|
} |
|
|
|
# Ask the user what file it was. |
|
query() { |
|
echo 'Was it...' |
|
echo |
|
local ANSWERS=3 |
|
local CORRECT=$((RANDOM % ANSWERS)) |
|
local WRONG=$((RANDOM % URLS_SIZE)) |
|
local I=0 |
|
for ((I=0; I < ANSWERS; ++I, ++WRONG)) |
|
do |
|
local OPTION |
|
if ((I == CORRECT)) |
|
then |
|
OPTION=$URL |
|
else |
|
# Search for a wrong answer that has not come before. |
|
while true |
|
do |
|
OPTION=${URLS[$((WRONG % URLS_SIZE))]} |
|
[[ $OPTION != "$URL" ]] && break |
|
((++WRONG)) |
|
done |
|
fi |
|
OPTION=${OPTION%%#*} |
|
echo "$I) ${OPTION##*/}" |
|
done |
|
|
|
# Read answer. |
|
echo |
|
while true |
|
do |
|
read -r -n 1 -p 'Your choice? (q to give up and quit) ' |
|
echo |
|
case "$REPLY" in |
|
[0-9]) |
|
if ((REPLY == CORRECT)) |
|
then |
|
echo 'Right!' |
|
((++HITS)) |
|
else |
|
echo "That was WRONG. It was $CORRECT." |
|
fi |
|
break |
|
;; |
|
q) |
|
exit |
|
;; |
|
*) |
|
echo 'Beg your pardon?' |
|
;; |
|
esac |
|
done |
|
sleep 1 |
|
} |
|
|
|
# Read from STDIN into map. |
|
map_file() { |
|
# Create blank backstore. |
|
local BLANKS='.' |
|
while ((${#BLANKS} < WIDTH)) |
|
do |
|
BLANKS="$BLANKS$BLANKS" |
|
done |
|
|
|
# Read snippet into map. |
|
MAP= |
|
local I=0 |
|
while read -r |
|
do |
|
# Skip over all lines before OFFSET. |
|
((--OFFSET > 0)) && continue |
|
# Expand tabs. |
|
REPLY=${REPLY//$'\t'/${BLANKS:0:4}} |
|
# Fill to fit width. |
|
local FILL=$((WIDTH - ${#REPLY})) |
|
((FILL > 0)) && REPLY=$REPLY${BLANKS:0:$FILL} |
|
MAP="$MAP${REPLY:0:WIDTH}" |
|
((++I > HEIGHT)) && break |
|
done |
|
|
|
# Fill the remaining lines of the map. |
|
for ((; I < HEIGHT; ++I)) |
|
do |
|
MAP="$MAP${BLANKS:0:WIDTH}" |
|
done |
|
} |
|
|
|
# Download and map file. |
|
download() { |
|
echo 'Downloading challenge ...' |
|
local TMP="${0##*/}_challenge_$$" |
|
curl "${URL%#*}" > "$TMP" || exit 1 |
|
local OFFSET=${URL##*#L} |
|
map_file < "$TMP" |
|
rm -f "$TMP" |
|
} |
|
|
|
# Build and run the quiz. |
|
# |
|
# @param 1 - file with source URLs (optional) |
|
lobby() { |
|
declare -a URLS=( |
|
'https://raw.githubusercontent.com/numpy/numpy/0032ede015c9b06f88cc7f9b07138ce35f4357ae/numpy/matlib.py#L24' |
|
'https://raw.githubusercontent.com/pointfreeco/swift-snapshot-testing/59b663f68e69f27a87b45de48cb63264b8194605/Sources/SnapshotTesting/Snapshotting.swift#L4' |
|
'https://raw.githubusercontent.com/Javacord/Javacord/971ea44e5d671d380382e0617fcaeaad070c8d02/javacord-core/src/main/java/org/javacord/core/util/FileContainer.java#L29' |
|
'https://raw.githubusercontent.com/flutter/flutter/62d699961f5a9c7e56a61c5acfcc86518458756f/packages/flutter/lib/src/widgets/async.dart#L383' |
|
'https://raw.githubusercontent.com/facebook/react/c5b9375767e2c4102d7e5559d383523736f1c902/packages/react-is/src/ReactIs.js#L29' |
|
) |
|
(($# > 0)) && IFS=$'\n' read -d '' -r -a URLS < "$1" |
|
local URLS_SIZE=${#URLS[@]} |
|
((URLS_SIZE < 3)) && { |
|
echo 'error: you need to have at least 3 files' >&2 |
|
exit 1 |
|
} |
|
|
|
# Show instructions. |
|
tput clear |
|
cat << EOL |
|
Explore a $URLS_SIZE snippets of source code like a dungeon in a roguelike. |
|
|
|
Can you guess which programming language or project a file comes from? |
|
|
|
Move with WASD or HJKL, and press Enter as soon as you think you know what |
|
file it is. You will see a range of options from which you can choose. |
|
|
|
The character '.' is displayed after the end of a line. |
|
All your steps are counted. |
|
|
|
EOL |
|
read -r -n 1 -p 'Hit a key to start...' |
|
echo |
|
|
|
# Seed random generator so everybody gets the same game. |
|
RANDOM=1 |
|
|
|
# Run the quiz. |
|
readonly MAX_WIDTH=40 MAX_HEIGHT=30 |
|
local WIDTH=${COLUMNS:-$(tput cols)} |
|
local PAD_LEFT=$(((WIDTH - MAX_WIDTH) / 2)) |
|
PAD_LEFT=$((PAD_LEFT < 0 ? 0 : PAD_LEFT)) |
|
WIDTH=$((WIDTH > MAX_WIDTH ? MAX_WIDTH : WIDTH)) |
|
local HEIGHT=${LINES:-$(tput lines)} |
|
local PAD_TOP=$(((HEIGHT - MAX_HEIGHT) / 2)) |
|
PAD_TOP=$((PAD_TOP < 0 ? 0 : PAD_TOP)) |
|
HEIGHT=$((HEIGHT > MAX_HEIGHT ? MAX_HEIGHT : HEIGHT)) |
|
local MOVES=0 |
|
local HITS=0 |
|
local MAP |
|
local URL |
|
for URL in "${URLS[@]}" |
|
do |
|
download |
|
inspect |
|
query |
|
done |
|
echo |
|
echo "You got $HITS of $URLS_SIZE correct and made $MOVES moves." |
|
} |
|
|
|
if [ "${BASH_SOURCE[0]}" == "$0" ] |
|
then |
|
lobby "$@" |
|
fi |