315 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			315 lines
		
	
	
		
			9.7 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. 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 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 | xargs)
 | 
						|
}
 | 
						|
 | 
						|
function game_get_info {
 | 
						|
    # Get the names of players, whose games are in PGN files
 | 
						|
    local ply_names=
 | 
						|
    game_get_names
 | 
						|
 | 
						|
    # 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 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[3]}
 | 
						|
    black=${BASH_REMATCH[4]}
 | 
						|
 | 
						|
    # Additional information about the game record
 | 
						|
    rec_length=${#BASH_REMATCH[1]}
 | 
						|
    game_date=${BASH_REMATCH[2]}
 | 
						|
    res_old=$(xargs <<< ${BASH_REMATCH[5]})
 | 
						|
    game_validate
 | 
						|
 | 
						|
    if ! $ADD_GAMES || [[ $game_date =~ \? ]]; then
 | 
						|
        local fst_pgn=$(awk '{print $1}' <<< "$TMP_PGN_FILES")
 | 
						|
        local date=$(sed -En "s/\[Date \"([^\"]*)\"\]/\1/p" $fst_pgn)
 | 
						|
        game_date=${date:8:2}.${date:5:2}.${date::4}
 | 
						|
    fi
 | 
						|
}
 | 
						|
 | 
						|
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
 | 
						|
        game_add_player $bk_lichess
 | 
						|
    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 "The list of players:"
 | 
						|
        sed "s/ /\\"$'\n'"$(tput setaf 6)*$(tput sgr0) /g" <<< " ${NAMES[@]}"
 | 
						|
        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 ! $ADD_GAMES && ! $CLEANUP_GAMES; then
 | 
						|
        if ! [[ $game_date =~ \? && -z $res_old ]]; then
 | 
						|
            die "Some games between ${white} and ${black} already recorded."
 | 
						|
        fi
 | 
						|
    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}\n"
 | 
						|
        else
 | 
						|
            ply_ordered+="${black}\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 {
 | 
						|
    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
 | 
						|
 | 
						|
    local pgn_index=0
 | 
						|
    if [[ -z $game_dir ]]; then
 | 
						|
        local date=${game_date:6:4}-${game_date:3:2}-${game_date::2}
 | 
						|
        game_dir=${REPO_DIR}/${TOURNAMENT}/tours/${TOUR}/${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}/*.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
 | 
						|
}
 | 
						|
 | 
						|
function game_update_info {
 | 
						|
    # Get 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 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
 | 
						|
    $ADD_GAMES && [[ -n $res_old ]] && result=" ${res_old}${result}"
 | 
						|
 | 
						|
    local spaces=$(( length_max - rec_length ))
 | 
						|
    local sep=$(printf "%${spaces}s" " ")
 | 
						|
    echo "Updating file ${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}/*.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
 | 
						|
# Don't add and clean up games simultaneously
 | 
						|
$ADD_GAMES && $CLEANUP_GAMES && usage 1
 | 
						|
 | 
						|
game_setup
 | 
						|
git --git-dir=${REPO_DIR}/.git pull # synchronize the repository
 | 
						|
game_tmp_pgns $@
 | 
						|
game_get_info
 | 
						|
game_store_pgns
 | 
						|
game_update_info
 | 
						|
game_git_commit
 | 
						|
 | 
						|
exit 0
 |