375 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			375 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
| #!/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="^(http://[^/]*)/(.{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
 |