|
- #!/usr/bin/env bash
- # Copyright 2014, 2015 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.5"
-
- # Firefox User Agent
- FIREFOX_UA="Mozilla/5.0 (X11; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0"
- # Regular expressions
- URL_RE="^(https?://[^/]*)/(.{8}).*"
- DATE_RE="[0-9?]{2}\.[0-9?]{2}\.[0-9?]{4}"
-
- 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|${URL_RE}|\2|" | sort -u | wc -l)
- (( $# == $uniq_ids )) || die "Game IDs not unique."
- }
-
- function game_setup {
- : ${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 =~ $URL_RE ]]
- # Link to annotated game PGN
- local game_url=${BASH_REMATCH[1]}/game/export/${BASH_REMATCH[2]}.pgn
-
- # Get the timestamp of game for sorting
- if $SORT_GAMES; then
- local game_api=${BASH_REMATCH[1]}/api/game/${BASH_REMATCH[2]}
- 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++ ))
- 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="½-½"
- elif [[ $ply_fst != $white ]]; then
- res=$(rev <<< $res)
- fi
-
- result+=" $res"
- (( res_index++ ))
- 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
|