lorchess/game-add
2014-12-07 00:26:07 +03:00

323 lines
9.6 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 its PGN
files, assuming that all the games were played by the same pair of
players on tour <num> of the tournament. The PGN files are sorted by
their timestamps, unless '-u' is set. Each game should be available
at the corresponding <url> (lichess.org).
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_setup {
date_re="[0-9?]{2}\.[0-9?]{2}\.[0-9?]{4}"
: ${REPO_DIR:=`dirname "$0"`}
# 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 {
TMP_PGN_FILES=
# Don't sort just one game
[[ $# == 1 ]] && SORT_GAMES=false
for url in $@; do
local pgn_template="pgn"
[[ $url =~ ^(http://[^/]*)/([^/]*) ]]
# Link to annotated game PGN
local game_url=${BASH_REMATCH[1]}/game/export/${BASH_REMATCH[2]::8}.pgn
# Append 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}"
local game_time=$(sed -En "s/.*\"timestamp\":([0-9]+).*/\1/p" <<< "$api_response")
pgn_template+="-${game_time}"
fi
# Store PGN file in a temporal location
local tmp_pgn=$(mktemp -t ${pgn_template}.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 | xargs)
}
function game_get_players {
# Make an associative array from Lichess nicks to players' names
declare -A NAMES=()
game_assoc_names
local ply_names=
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
game_add_player $bk_lichess
done
# Select the names of two players
local players=( $(echo -en "$ply_names" | sort -u) )
[[ ${#players[@]} == 2 ]] || die "Players of the games are not the same."
# Find the white and black players
local line=$(grep -E "( ${players[0]} | ${players[1]} )" $tour_info)
[[ -n $line && $(wc -l <<< "$line") == 1 ]] \
|| die "No game between ${players[0]} and ${players[1]} found in ${tour_info}."
[[ $line =~ ^${date_re}\ +([^\ ]+)\ +-\ +([^\ ]+)(.*)$ ]]
white=${BASH_REMATCH[1]}
black=${BASH_REMATCH[2]}
res_old=$(xargs <<< ${BASH_REMATCH[3]})
game_validate
}
function game_assoc_names {
game_parse_config
local sections=$(grep -o "config_section_player[0-9]*" $tmp_ini)
for sect in $sections; do
eval $sect
NAMES+=( [$lichess]=$name )
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 "The list of players:"
echo "${NAMES[@]}" | tr ' ' '\n' | sed "s/^/$(tput setaf 6)*$(tput sgr0) /"
echo -n "Type the name of ${lichess_ply}> "
read ply
done
ply_names+="${ply}\n"
}
function game_validate {
# By default, other games between the players are not allowed
if [[ -n $res_old ]]; then
$ADD_GAMES || $CLEANUP_GAMES || die "Results of some games already stored."
local index=$(wc -w <<< "$res_old")
else
local index=0
fi
# Players' sides should interchange
local length=$(echo -en "$ply_names" | wc -l)
local ply_ordered=
for ((i = index; i < length; i++)); do
if (( i % 2 == 1 )); then
ply_ordered="${white}\n${black}\n"
else
ply_ordered="${black}\n${white}\n"
fi
done
if [[ "$ply_names" != "$ply_ordered" ]]; then
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 pgn_index=0
game_dir=$(ls -1 -d 2>/dev/null \
${REPO_DIR}/${TOURNAMENT}/tours/${TOUR}/*-${white}-vs-${black})
if $CLEANUP_GAMES && [[ -n $game_dir ]]; then
rm -r "$game_dir"
game_dir=
fi
if [[ -z $game_dir ]]; then
# Get the game date
date=
game_get_date
game_date=$(tr '.' '-' <<< "$date")
game_dir=${REPO_DIR}/${TOURNAMENT}/tours/${TOUR}/${game_date}-${white}-vs-${black}
echo "Creating directory ${game_dir}..."
mkdir "$game_dir"
else
$ADD_GAMES || die "Directory ${game_dir} already exist."
local old_pgns=$(ls -1 -p 2>/dev/null ${game_dir}/[0-9]*.pgn | grep -v "/$")
[[ -n $old_pgns ]] && pgn_index=$(wc -l <<< "$old_pgns")
fi
for pgn in $TMP_PGN_FILES; do
(( pgn_index += 1 ))
cp $pgn ${game_dir}/${pgn_index}.pgn
done
}
function game_get_date {
local fst_pgn others
read fst_pgn others <<< "$TMP_PGN_FILES"
date=$(sed -En "s/\[Date \"([^\"]*)\"\]/\1/p" $fst_pgn)
}
function game_update_info {
local length_max=0
while read line; do
[[ $line =~ ^((${date_re})\ +([^\ ]+)\ +-\ +([^\ ]+)) ]]
# Get the maximal length of game records, excepting results
local length=${#BASH_REMATCH[1]}
(( $length > $length_max )) && length_max=$length
if [[ ${BASH_REMATCH[3]} == $white && ${BASH_REMATCH[4]} == $black ]]; then
local length_rec=${#BASH_REMATCH[1]} game_date=${BASH_REMATCH[2]}
fi
done < $tour_info
local result=
for pgn in $TMP_PGN_FILES; do
local res=$(sed -En "s/\[Result \"([^\"]*)\"\]/\1/p" $pgn)
[[ $res == 1/2-1/2 ]] && res=1/2 # representation of a draw
result+=" $res"
done
if $ADD_GAMES && [[ -n $res_old ]]; then
result=" ${res_old}${result}"
else
date=
game_get_date
game_date=${date:8:2}.${date:5:2}.${date::4}
fi
local spaces=$(( length_max - length_rec ))
local sep=$(printf "%${spaces}s" " ")
echo "Updating ${tour_info}..."
sed -E -i.orig \
"s|^${date_re}( +${white} +- +${black}).*|${game_date}\1${sep}${result}|" \
$tour_info
rm ${tour_info}.orig
}
function game_git_commit {
git --git-dir=${REPO_DIR}/.git add ${game_dir}/[0-9]*.pgn $tour_info
git --git-dir=${REPO_DIR}/.git commit -m "Tour ${TOUR#0}: ${white} vs. ${black}."
git --git-dir=${REPO_DIR}/.git push
}
function die {
echo "$@" >&2
exit 1
}
function checkargs {
[[ $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) checkargs ;;
u) SORT_GAMES=false ;;
h) usage ;;
v) version ;;
*) usage 1 ;;
esac
done
shift $(($OPTIND - 1))
# For now, tour number should be given explicitly
[[ -z $TOUR || $# == 0 ]] && usage 1
game_setup
git --git-dir=${REPO_DIR}/.git pull # synchronize the repository
game_tmp_pgns $@
game_get_players
game_store_pgns
game_update_info
game_git_commit
exit 0