374 lines
12 KiB
Bash
Executable File
374 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Copyright 2014 Vladimir Ivanov <ivvl82@gmail.com>
|
|
# Distributed under the terms of the GNU General Public License v2
|
|
|
|
REPO_DIR=
|
|
|
|
# Specify the tournament here
|
|
TOURNAMENT=
|
|
|
|
# Version information
|
|
VERSION="0.4"
|
|
|
|
# Firefox User Agent
|
|
FIREFOX_UA="Mozilla/5.0 (X11; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0"
|
|
|
|
argv0=${0##*/}
|
|
|
|
function usage {
|
|
cat <<EOF
|
|
Store chess games played on lichess.org
|
|
|
|
Usage:
|
|
$argv0 [options] -t <num> <url>...
|
|
$argv0 -h
|
|
$argv0 -v
|
|
|
|
Put the script under the root directory of your repository or set
|
|
inner variable REPO_DIR to it. If the tournament is not the last one
|
|
(default), store its sub-directory in inner variable TOURNAMENT.
|
|
|
|
The first form fills the results of chess games and stores their PGN
|
|
files, assuming that all the games were played by the same pair of
|
|
players on tour <num> of the tournament. Each game should be
|
|
available at the corresponding <url> (lichess.org). The PGN files
|
|
are sorted by their timestamps, unless '-u' is set. The date of
|
|
games is assigned to that of the game closing the tour, i.e., the
|
|
last one.
|
|
|
|
The second form shows this help output. The third form shows version
|
|
information.
|
|
|
|
Options:
|
|
-a Add current games to existing games
|
|
-c Clean up existing games before adding
|
|
-u Don't sort games by their timestamps
|
|
EOF
|
|
|
|
exit "${1:-0}"
|
|
}
|
|
|
|
function version {
|
|
exec echo "${argv0}-${VERSION}"
|
|
}
|
|
|
|
function game_check_args {
|
|
# For now, tour number should be given explicitly
|
|
[[ -z $TOUR || $# == 0 ]] && usage 1
|
|
# Don't add and clean up games simultaneously
|
|
$ADD_GAMES && $CLEANUP_GAMES && usage 1
|
|
# Check for duplicate game ID
|
|
local uniq_ids=$(xargs -n1 <<< $@ | sed -E "s|^http://[^/]*/([^/]{8}).*|\1|" \
|
|
| sort -u | wc -l)
|
|
(( $# == $uniq_ids )) || die "Game IDs not unique."
|
|
}
|
|
|
|
function game_setup {
|
|
date_re="[0-9?]{2}\.[0-9?]{2}\.[0-9?]{4}"
|
|
: ${REPO_DIR:=`dirname "$0"`}
|
|
# Synchronize the repository
|
|
git --git-dir=${REPO_DIR}/.git pull
|
|
|
|
# If no tournament given, set it to the last one
|
|
if [[ -z $TOURNAMENT ]]; then
|
|
local year_dir=$(ls -1 -d ${REPO_DIR}/[0-9][0-9][0-9][0-9]/ | tail -1)
|
|
TOURNAMENT=$(ls -1 -d ${year_dir}[0-9]-*/ | tail -1 \
|
|
| sed -E "s|${REPO_DIR}/(.*)/|\1|")
|
|
fi
|
|
|
|
# Configuration file for players
|
|
ply_ini=${REPO_DIR}/${TOURNAMENT}/players.ini
|
|
[[ -f $ply_ini ]] || die "File ${ply_ini} not found."
|
|
# Pairs of players on the tour
|
|
tour_info=${REPO_DIR}/${TOURNAMENT}/tours/${TOUR}/tour_info
|
|
[[ -f $tour_info ]] || die "File ${tour_info} not found."
|
|
}
|
|
|
|
function game_tmp_pgns {
|
|
# Don't sort just one game
|
|
[[ $# == 1 ]] && SORT_GAMES=false
|
|
|
|
for url in $@; do
|
|
local timestamp=
|
|
[[ $url =~ ^(http://[^/]*)/([^/]*) ]]
|
|
# Link to annotated game PGN
|
|
local game_url=${BASH_REMATCH[1]}/game/export/${BASH_REMATCH[2]::8}.pgn
|
|
|
|
# Get the timestamp of game for sorting
|
|
if $SORT_GAMES; then
|
|
local game_api=${BASH_REMATCH[1]}/api/game/${BASH_REMATCH[2]::8}
|
|
local api_response=$(curl -q --fail --location --silent "$game_api")
|
|
[[ -z $api_response ]] && die "Unreachable game API ${game_api}"
|
|
timestamp=$(sed -En "s/.*\"timestamp\":([0-9]+).*/\1/p" <<< "$api_response")
|
|
fi
|
|
|
|
# Store PGN file in a temporal location
|
|
local tmp_pgn=$(mktemp -t ${timestamp:-1}.pgn-XXXXXX)
|
|
TMP_PGN_FILES+=($tmp_pgn)
|
|
trap "rm ${TMP_PGN_FILES[*]}" EXIT
|
|
wget -q -U "$FIREFOX_UA" -O $tmp_pgn "$game_url" \
|
|
|| die "Unreachable game PGN ${game_url}"
|
|
done
|
|
|
|
$SORT_GAMES && TMP_PGN_FILES=( $(xargs -n1 <<< ${TMP_PGN_FILES[@]} | sort -n) )
|
|
}
|
|
|
|
function game_get_info {
|
|
# Get the names of players, whose games are in PGN files
|
|
ply_names=
|
|
game_get_names
|
|
|
|
# Select the names of two players
|
|
local players=( $(echo -en "${ply_names// /\n}" | sort -u) )
|
|
[[ ${#players[@]} == 2 ]] || die "Players of the games are not the same."
|
|
|
|
# Find the white and black players
|
|
local line0=$(grep " ${players[0]}" $tour_info)
|
|
local line1=$(grep " ${players[1]}" $tour_info)
|
|
[[ -z $line0 || -z $line1 || $line0 != $line1 ]] \
|
|
&& die "No game between ${players[0]} and ${players[1]} found in ${tour_info}."
|
|
[[ $line0 =~ ^(${date_re}\ +([^\ ]+)\ +-\ +([^\ ]+))(.*)$ ]]
|
|
white=${BASH_REMATCH[2]}
|
|
black=${BASH_REMATCH[3]}
|
|
|
|
# Additional information about the game record
|
|
rec_length=${#BASH_REMATCH[1]}
|
|
res_old=$(xargs <<< ${BASH_REMATCH[4]})
|
|
game_validate
|
|
|
|
# Assign the date of games to the date of the last game
|
|
local pgn_last=${TMP_PGN_FILES[${#TMP_PGN_FILES[@]} - 1]}
|
|
game_date=$(sed -En "s/\[Date \"([^\"]*)\"\]/\1/p" $pgn_last)
|
|
}
|
|
|
|
function game_get_names {
|
|
# Make an associative array from Lichess nicks to players' names
|
|
game_parse_config
|
|
local sections=$(grep -o "config_section_player[0-9]*" $tmp_ini)
|
|
declare -A NAMES
|
|
for sect in $sections; do
|
|
eval $sect
|
|
NAMES+=( [$lichess]=$name )
|
|
done
|
|
|
|
for pgn in ${TMP_PGN_FILES[@]}; do
|
|
# Extract players on Lichess
|
|
local wt_lichess=$(sed -En "s/\[White \"([^\"]*)\"\]/\1/p" $pgn)
|
|
local bk_lichess=$(sed -En "s/\[Black \"([^\"]*)\"\]/\1/p" $pgn)
|
|
|
|
game_add_player $wt_lichess
|
|
ply_names+=" "
|
|
game_add_player $bk_lichess
|
|
ply_names+="\n"
|
|
done
|
|
}
|
|
|
|
function game_parse_config {
|
|
# Temporary files
|
|
tmp_ini=$(mktemp -t `basename $ply_ini`.XXXXXX)
|
|
TMP_INI_FILES="${tmp_ini} ${tmp_ini}.prev"
|
|
trap "rm $TMP_INI_FILES ${TMP_PGN_FILES[*]}" EXIT
|
|
|
|
# Copy player INI file to the temporary location
|
|
# NOTE: an empty line is added to the file beginning in order to
|
|
# match the only first occurrence for non-GNU sed
|
|
echo > $tmp_ini
|
|
cat "$ply_ini" >> $tmp_ini
|
|
|
|
# Remove tabs or spaces around the `='
|
|
sed -E -i.prev "s/[[:blank:]]*=[[:blank:]]*/=/" $tmp_ini
|
|
|
|
# Transform section labels into function declaration
|
|
sed -E -i.prev "1,/^\[.*\]/s/^\[([^]]*)\]/config_section_\1() {/" $tmp_ini
|
|
sed -E -i.prev "s/^\[([^]]*)\]/}\\"$'\n'"config_section_\1() {/" $tmp_ini
|
|
echo -e "\n}" >> $tmp_ini
|
|
|
|
# Source the file
|
|
source $tmp_ini
|
|
}
|
|
|
|
function game_add_player {
|
|
local lichess_ply=$1
|
|
local ply=${NAMES[$lichess_ply]}
|
|
while [[ ! " ${NAMES[*]} " =~ \ $ply\ ]]; do
|
|
echo -n "$(tput setaf 2)The list of players:$(tput sgr0)"
|
|
sed "s/ /\\"$'\n'"$(tput setaf 6)*$(tput sgr0) /g" <<< " ${NAMES[*]}"
|
|
echo -n "Type the name of ${lichess_ply}> "
|
|
read ply
|
|
done
|
|
ply_names+=$ply
|
|
}
|
|
|
|
function game_validate {
|
|
# By default, other games between the players are not allowed
|
|
if ! $ADD_GAMES && ! $CLEANUP_GAMES && [[ -n $res_old ]]; then
|
|
die "Results of some games between ${white} and ${black} already recorded."
|
|
fi
|
|
|
|
# Players' sides should interchange
|
|
|
|
local length=$(echo -en "$ply_names" | wc -l)
|
|
if $ADD_GAMES && [[ -n $res_old ]]; then
|
|
local residue=$(( $(wc -w <<< "$res_old") % 2 ))
|
|
else
|
|
local residue=0
|
|
fi
|
|
|
|
local ply_ordered=
|
|
for ((i = 0; i < length; i++)); do
|
|
if (( i % 2 == residue )); then
|
|
ply_ordered+="${white} ${black}\n"
|
|
else
|
|
ply_ordered+="${black} ${white}\n"
|
|
fi
|
|
done
|
|
|
|
# List found games
|
|
echo "$(tput setaf 2)Found games:$(tput sgr0)"
|
|
echo -en "$ply_names" \
|
|
| sed "s/ / - /;s/^/$(tput setaf 6)*$(tput sgr0) /"
|
|
|
|
if [[ "$ply_names" != "$ply_ordered" ]]; then
|
|
# List expected games
|
|
echo "$(tput setaf 2)Expected games:$(tput sgr0)"
|
|
echo -en "$ply_ordered" \
|
|
| sed "s/ / - /;s/^/$(tput setaf 6)*$(tput sgr0) /"
|
|
|
|
local answer
|
|
echo -n "Approve games with wrong players' sides? (Y/n)> "
|
|
read answer
|
|
[[ $answer =~ ^(Y|y|Yes|yes)$ ]] || exit 1
|
|
fi
|
|
}
|
|
|
|
function game_store_pgns {
|
|
local game_dir_old=$(ls -1 -d 2>/dev/null \
|
|
${REPO_DIR}/${TOURNAMENT}/tours/${TOUR}/*-${white}-vs-${black})
|
|
if [[ -n $game_dir_old ]]; then
|
|
if $CLEANUP_GAMES; then
|
|
git --git-dir=${REPO_DIR}/.git rm --ignore-unmatch -q ${game_dir_old}/*.pgn
|
|
[[ -d $game_dir_old ]] || game_dir_old=
|
|
else
|
|
$ADD_GAMES || die "Directory ${game_dir_old} already exist."
|
|
fi
|
|
fi
|
|
|
|
local pgn_dir=${game_date//./-}-${white}-vs-${black}
|
|
local game_dir=${REPO_DIR}/${TOURNAMENT}/tours/${TOUR}/${pgn_dir}
|
|
if [[ $game_dir != $game_dir_old ]]; then
|
|
echo "Creating directory ${game_dir}..."
|
|
mkdir $game_dir
|
|
if [[ -n $game_dir_old ]]; then
|
|
git --git-dir=${REPO_DIR}/.git mv ${game_dir_old}/* ${game_dir}
|
|
rm -r $game_dir_old
|
|
fi
|
|
fi
|
|
|
|
local pgn_index=0
|
|
if $ADD_GAMES; then
|
|
local old_pgns=$(ls -1 2>/dev/null ${game_dir}/*.pgn)
|
|
[[ -n $old_pgns ]] && pgn_index=$(wc -l <<< "$old_pgns")
|
|
fi
|
|
for pgn in ${TMP_PGN_FILES[@]}; do
|
|
(( pgn_index += 1 ))
|
|
echo "Storing file ${game_dir}/${pgn_index}.pgn..."
|
|
cp $pgn ${game_dir}/${pgn_index}.pgn
|
|
done
|
|
|
|
git --git-dir=${REPO_DIR}/.git add ${game_dir}
|
|
}
|
|
|
|
function game_update_info {
|
|
# The maximal length of game records, excepting results
|
|
local length_max=$(grep -Eo "^${date_re} +[^ ]+ +- +[^ ]+" $tour_info \
|
|
| awk '{print length}' | sort -nr | head -1)
|
|
local spaces=$(( length_max - rec_length )) sep=
|
|
(( spaces == 0 )) || sep=$(printf "%${spaces}s" " ")
|
|
|
|
local result=
|
|
game_get_result
|
|
|
|
echo "Updating file ${tour_info}..."
|
|
local date=${game_date:8:2}.${game_date:5:2}.${game_date::4}
|
|
sed -E -i.orig \
|
|
"s|^${date_re}( +${white} +- +${black}).*|${date}\1${sep}${result}|" \
|
|
$tour_info
|
|
rm ${tour_info}.orig
|
|
|
|
git --git-dir=${REPO_DIR}/.git add $tour_info
|
|
}
|
|
|
|
function game_get_result {
|
|
local res_index=0
|
|
while read ply_fst ply_snd; do
|
|
local pgn=${TMP_PGN_FILES[$res_index]}
|
|
local res=$(sed -En "s/\[Result \"([^\"]*)\"\]/\1/p" $pgn)
|
|
|
|
if [[ $res == 1/2-1/2 ]]; then
|
|
# Representation of draw
|
|
res=1/2
|
|
elif [[ $ply_fst != $white ]]; then
|
|
res=$(rev <<< $res)
|
|
fi
|
|
|
|
result+=" $res"
|
|
(( res_index += 1 ))
|
|
done <<< "$(echo -e "$ply_names")"
|
|
|
|
$ADD_GAMES && [[ -n $res_old ]] && result=" ${res_old}${result}"
|
|
}
|
|
|
|
function game_git_commit {
|
|
local title=$(awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}' \
|
|
<<< ${TOURNAMENT##*-})
|
|
local git_message="Tournament ${title}, tour ${TOUR#0}: "
|
|
if $ADD_GAMES; then
|
|
git_message+="add"
|
|
elif $CLEANUP_GAMES; then
|
|
git_message+="replace"
|
|
else
|
|
git_message+="new"
|
|
fi
|
|
git_message+=" games ${white} vs. ${black}."
|
|
|
|
git --git-dir=${REPO_DIR}/.git commit -m "$git_message"
|
|
git --git-dir=${REPO_DIR}/.git push
|
|
}
|
|
|
|
function die {
|
|
echo "$@" >&2
|
|
exit 1
|
|
}
|
|
|
|
function game_tour {
|
|
[[ $OPTARG =~ ^[0-9]+$ ]] || die "Incorrect tour number."
|
|
TOUR=$(printf "%02g" $OPTARG)
|
|
}
|
|
|
|
ADD_GAMES=false
|
|
CLEANUP_GAMES=false
|
|
SORT_GAMES=true
|
|
while getopts act:uhv opt; do
|
|
case $opt in
|
|
a) ADD_GAMES=true ;;
|
|
c) CLEANUP_GAMES=true ;;
|
|
t) game_tour ;;
|
|
u) SORT_GAMES=false ;;
|
|
h) usage ;;
|
|
v) version ;;
|
|
*) usage 1 ;;
|
|
esac
|
|
done
|
|
shift $(($OPTIND - 1))
|
|
|
|
game_check_args $@
|
|
game_setup
|
|
declare -a TMP_PGN_FILES
|
|
game_tmp_pgns $@
|
|
game_get_info
|
|
game_store_pgns
|
|
game_update_info
|
|
game_git_commit
|
|
|
|
exit 0
|