Linux.org.ru chess tournament logs and tables
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

375 lines
11KB

  1. #!/usr/bin/env bash
  2. # Copyright 2014, 2015 Vladimir Ivanov <ivvl82@gmail.com>
  3. # Distributed under the terms of the GNU General Public License v2
  4. REPO_DIR=
  5. # Specify the tournament here
  6. TOURNAMENT=
  7. # Version information
  8. VERSION="0.5"
  9. # Firefox User Agent
  10. FIREFOX_UA="Mozilla/5.0 (X11; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0"
  11. # Regular expressions
  12. URL_RE="^(https?://[^/]*)/(.{8}).*"
  13. DATE_RE="[0-9?]{2}\.[0-9?]{2}\.[0-9?]{4}"
  14. argv0=${0##*/}
  15. function usage {
  16. cat <<EOF
  17. Store chess games played on lichess.org
  18. Usage:
  19. $argv0 [options] -t <num> <url>...
  20. $argv0 -h
  21. $argv0 -v
  22. Put the script under the root directory of your repository or set
  23. inner variable REPO_DIR to it. If the tournament is not the last one
  24. (default), store its sub-directory in inner variable TOURNAMENT.
  25. The first form fills the results of chess games and stores their PGN
  26. files, assuming that all the games were played by the same pair of
  27. players on tour <num> of the tournament. Each game should be
  28. available at the corresponding <url> (lichess.org). The PGN files
  29. are sorted by their timestamps, unless '-u' is set. The date of
  30. games is assigned to that of the game closing the tour, i.e., the
  31. last one.
  32. The second form shows this help output. The third form shows version
  33. information.
  34. Options:
  35. -a Add current games to existing games
  36. -c Clean up existing games before adding
  37. -u Don't sort games by their timestamps
  38. EOF
  39. exit "${1:-0}"
  40. }
  41. function version {
  42. exec echo "${argv0}-${VERSION}"
  43. }
  44. function game_check_args {
  45. # For now, tour number should be given explicitly
  46. [[ -z $TOUR || $# == 0 ]] && usage 1
  47. # Don't add and clean up games simultaneously
  48. $ADD_GAMES && $CLEANUP_GAMES && usage 1
  49. # Check for duplicate game ID
  50. local uniq_ids=$(xargs -n1 <<< $@ | sed -E "s|${URL_RE}|\2|" | sort -u | wc -l)
  51. (( $# == $uniq_ids )) || die "Game IDs not unique."
  52. }
  53. function game_setup {
  54. : ${REPO_DIR:=`dirname "$0"`}
  55. # Synchronize the repository
  56. git --git-dir=${REPO_DIR}/.git pull
  57. # If no tournament given, set it to the last one
  58. if [[ -z $TOURNAMENT ]]; then
  59. local year_dir=$(ls -1 -d ${REPO_DIR}/[0-9][0-9][0-9][0-9]/ | tail -1)
  60. TOURNAMENT=$(ls -1 -d ${year_dir}[0-9]-*/ | tail -1 \
  61. | sed -E "s|${REPO_DIR}/(.*)/|\1|")
  62. fi
  63. # Configuration file for players
  64. ply_ini=${REPO_DIR}/${TOURNAMENT}/players.ini
  65. [[ -f $ply_ini ]] || die "File ${ply_ini} not found."
  66. # Pairs of players on the tour
  67. tour_info=${REPO_DIR}/${TOURNAMENT}/tours/${TOUR}/tour_info
  68. [[ -f $tour_info ]] || die "File ${tour_info} not found."
  69. }
  70. function game_tmp_pgns {
  71. # Don't sort just one game
  72. [[ $# == 1 ]] && SORT_GAMES=false
  73. for url in $@; do
  74. local timestamp=
  75. [[ $url =~ $URL_RE ]]
  76. # Link to annotated game PGN
  77. local game_url=${BASH_REMATCH[1]}/game/export/${BASH_REMATCH[2]}.pgn
  78. # Get the timestamp of game for sorting
  79. if $SORT_GAMES; then
  80. local game_api=${BASH_REMATCH[1]}/api/game/${BASH_REMATCH[2]}
  81. local api_response=$(curl -q --fail --location --silent "$game_api")
  82. [[ -z $api_response ]] && die "Unreachable game API ${game_api}"
  83. timestamp=$(sed -En "s/.*\"timestamp\":([0-9]+).*/\1/p" <<< "$api_response")
  84. fi
  85. # Store PGN file in a temporal location
  86. local tmp_pgn=$(mktemp -t ${timestamp:-1}.pgn-XXXXXX)
  87. TMP_PGN_FILES+=($tmp_pgn)
  88. trap "rm ${TMP_PGN_FILES[*]}" EXIT
  89. wget -q -U "$FIREFOX_UA" -O $tmp_pgn "$game_url" \
  90. || die "Unreachable game PGN ${game_url}"
  91. done
  92. $SORT_GAMES && TMP_PGN_FILES=( $(xargs -n1 <<< ${TMP_PGN_FILES[@]} | sort -n) )
  93. }
  94. function game_get_info {
  95. # Get the names of players, whose games are in PGN files
  96. ply_names=
  97. game_get_names
  98. # Select the names of two players
  99. local players=( $(echo -en "${ply_names// /$'\n'}" | sort -u) )
  100. [[ ${#players[@]} == 2 ]] || die "Players of the games are not the same."
  101. # Find the white and black players
  102. local line0=$(grep " ${players[0]}" $tour_info)
  103. local line1=$(grep " ${players[1]}" $tour_info)
  104. [[ -z $line0 || -z $line1 || $line0 != $line1 ]] \
  105. && die "No game between ${players[0]} and ${players[1]} found in ${tour_info}."
  106. [[ $line0 =~ ^(${DATE_RE}\ +([^\ ]+)\ +-\ +([^\ ]+))(.*)$ ]]
  107. white=${BASH_REMATCH[2]}
  108. black=${BASH_REMATCH[3]}
  109. # Additional information about the game record
  110. rec_length=${#BASH_REMATCH[1]}
  111. res_old=$(xargs <<< ${BASH_REMATCH[4]})
  112. game_validate
  113. # Assign the date of games to the date of the last game
  114. local pgn_last=${TMP_PGN_FILES[${#TMP_PGN_FILES[@]} - 1]}
  115. game_date=$(sed -En "s/\[Date \"([^\"]*)\"\]/\1/p" $pgn_last)
  116. }
  117. function game_get_names {
  118. # Make an associative array from Lichess nicks to players' names
  119. game_parse_config
  120. local sections=$(grep -o "config_section_player[0-9]*" $tmp_ini)
  121. declare -A NAMES
  122. for sect in $sections; do
  123. eval $sect
  124. NAMES+=( [$lichess]=$name )
  125. done
  126. for pgn in ${TMP_PGN_FILES[@]}; do
  127. # Extract players on Lichess
  128. local wt_lichess=$(sed -En "s/\[White \"([^\"]*)\"\]/\1/p" $pgn)
  129. local bk_lichess=$(sed -En "s/\[Black \"([^\"]*)\"\]/\1/p" $pgn)
  130. game_add_player $wt_lichess
  131. ply_names+=" "
  132. game_add_player $bk_lichess
  133. ply_names+="\n"
  134. done
  135. }
  136. function game_parse_config {
  137. # Temporary files
  138. tmp_ini=$(mktemp -t `basename $ply_ini`.XXXXXX)
  139. TMP_INI_FILES="${tmp_ini} ${tmp_ini}.prev"
  140. trap "rm $TMP_INI_FILES ${TMP_PGN_FILES[*]}" EXIT
  141. # Copy player INI file to the temporary location
  142. # NOTE: an empty line is added to the file beginning in order to
  143. # match the only first occurrence for non-GNU sed
  144. echo > $tmp_ini
  145. cat "$ply_ini" >> $tmp_ini
  146. # Remove tabs or spaces around the `='
  147. sed -E -i.prev "s/[[:blank:]]*=[[:blank:]]*/=/" $tmp_ini
  148. # Transform section labels into function declaration
  149. sed -E -i.prev "1,/^\[.*\]/s/^\[([^]]*)\]/config_section_\1() {/" $tmp_ini
  150. sed -E -i.prev "s/^\[([^]]*)\]/}\\"$'\n'"config_section_\1() {/" $tmp_ini
  151. echo -e "\n}" >> $tmp_ini
  152. # Source the file
  153. source $tmp_ini
  154. }
  155. function game_add_player {
  156. local lichess_ply=$1
  157. local ply=${NAMES[$lichess_ply]}
  158. while [[ ! " ${NAMES[*]} " =~ \ $ply\ ]]; do
  159. echo -n "$(tput setaf 2)The list of players:$(tput sgr0)"
  160. sed "s/ /\\"$'\n'"$(tput setaf 6)*$(tput sgr0) /g" <<< " ${NAMES[*]}"
  161. echo -n "Type the name of ${lichess_ply}> "
  162. read ply
  163. done
  164. ply_names+=$ply
  165. }
  166. function game_validate {
  167. # By default, other games between the players are not allowed
  168. if ! $ADD_GAMES && ! $CLEANUP_GAMES && [[ -n $res_old ]]; then
  169. die "Results of some games between ${white} and ${black} already recorded."
  170. fi
  171. # Players' sides should interchange
  172. local length=$(echo -en "$ply_names" | wc -l)
  173. if $ADD_GAMES && [[ -n $res_old ]]; then
  174. local residue=$(( $(wc -w <<< "$res_old") % 2 ))
  175. else
  176. local residue=0
  177. fi
  178. local ply_ordered=
  179. for ((i = 0; i < length; i++)); do
  180. if (( i % 2 == residue )); then
  181. ply_ordered+="${white} ${black}\n"
  182. else
  183. ply_ordered+="${black} ${white}\n"
  184. fi
  185. done
  186. # List found games
  187. echo "$(tput setaf 2)Found games:$(tput sgr0)"
  188. echo -en "$ply_names" \
  189. | sed "s/ / - /;s/^/$(tput setaf 6)*$(tput sgr0) /"
  190. if [[ "$ply_names" != "$ply_ordered" ]]; then
  191. # List expected games
  192. echo "$(tput setaf 2)Expected games:$(tput sgr0)"
  193. echo -en "$ply_ordered" \
  194. | sed "s/ / - /;s/^/$(tput setaf 6)*$(tput sgr0) /"
  195. local answer
  196. echo -n "Approve games with wrong players' sides? (Y/n)> "
  197. read answer
  198. [[ $answer =~ ^(Y|y|Yes|yes)$ ]] || exit 1
  199. fi
  200. }
  201. function game_store_pgns {
  202. local game_dir_old=$(ls -1 -d 2>/dev/null \
  203. ${REPO_DIR}/${TOURNAMENT}/tours/${TOUR}/*-${white}-vs-${black})
  204. if [[ -n $game_dir_old ]]; then
  205. if $CLEANUP_GAMES; then
  206. git --git-dir=${REPO_DIR}/.git rm --ignore-unmatch -q ${game_dir_old}/*.pgn
  207. [[ -d $game_dir_old ]] || game_dir_old=
  208. else
  209. $ADD_GAMES || die "Directory ${game_dir_old} already exist."
  210. fi
  211. fi
  212. local pgn_dir=${game_date//./-}-${white}-vs-${black}
  213. local game_dir=${REPO_DIR}/${TOURNAMENT}/tours/${TOUR}/${pgn_dir}
  214. if [[ $game_dir != $game_dir_old ]]; then
  215. echo "Creating directory ${game_dir}..."
  216. mkdir $game_dir
  217. if [[ -n $game_dir_old ]]; then
  218. git --git-dir=${REPO_DIR}/.git mv ${game_dir_old}/* ${game_dir}
  219. rm -r $game_dir_old
  220. fi
  221. fi
  222. local pgn_index=0
  223. if $ADD_GAMES; then
  224. local old_pgns=$(ls -1 2>/dev/null ${game_dir}/*.pgn)
  225. [[ -n $old_pgns ]] && pgn_index=$(wc -l <<< "$old_pgns")
  226. fi
  227. for pgn in ${TMP_PGN_FILES[@]}; do
  228. (( pgn_index++ ))
  229. echo "Storing file ${game_dir}/${pgn_index}.pgn..."
  230. cp $pgn ${game_dir}/${pgn_index}.pgn
  231. done
  232. git --git-dir=${REPO_DIR}/.git add ${game_dir}
  233. }
  234. function game_update_info {
  235. # The maximal length of game records, excepting results
  236. local length_max=$(grep -Eo "^${DATE_RE} +[^ ]+ +- +[^ ]+" $tour_info \
  237. | awk '{print length}' | sort -nr | head -1)
  238. local spaces=$(( length_max - rec_length )) sep=
  239. (( spaces == 0 )) || sep=$(printf "%${spaces}s" " ")
  240. local result=
  241. game_get_result
  242. echo "Updating file ${tour_info}..."
  243. local date=${game_date:8:2}.${game_date:5:2}.${game_date::4}
  244. sed -E -i.orig \
  245. "s/^${DATE_RE}( +${white} +- +${black}).*/${date}\1${sep}${result}/" \
  246. $tour_info
  247. rm ${tour_info}.orig
  248. git --git-dir=${REPO_DIR}/.git add $tour_info
  249. }
  250. function game_get_result {
  251. local res_index=0
  252. while read ply_fst ply_snd; do
  253. local pgn=${TMP_PGN_FILES[$res_index]}
  254. local res=$(sed -En "s/\[Result \"([^\"]*)\"\]/\1/p" $pgn)
  255. if [[ $res == 1/2-1/2 ]]; then
  256. # Representation of draw
  257. res="½-½"
  258. elif [[ $ply_fst != $white ]]; then
  259. res=$(rev <<< $res)
  260. fi
  261. result+=" $res"
  262. (( res_index++ ))
  263. done <<< "$(echo -e "$ply_names")"
  264. $ADD_GAMES && [[ -n $res_old ]] && result=" ${res_old}${result}"
  265. }
  266. function game_git_commit {
  267. local title=$(awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}' \
  268. <<< ${TOURNAMENT##*-})
  269. local git_message="Tournament ${title}, tour ${TOUR#0}: "
  270. if $ADD_GAMES; then
  271. git_message+="add"
  272. elif $CLEANUP_GAMES; then
  273. git_message+="replace"
  274. else
  275. git_message+="new"
  276. fi
  277. git_message+=" games ${white} vs. ${black}."
  278. git --git-dir=${REPO_DIR}/.git commit -m "$git_message"
  279. git --git-dir=${REPO_DIR}/.git push
  280. }
  281. function die {
  282. echo "$@" >&2
  283. exit 1
  284. }
  285. function game_tour {
  286. [[ $OPTARG =~ ^[0-9]+$ ]] || die "Incorrect tour number."
  287. TOUR=$(printf "%02g" $OPTARG)
  288. }
  289. ADD_GAMES=false
  290. CLEANUP_GAMES=false
  291. SORT_GAMES=true
  292. while getopts act:uhv opt; do
  293. case $opt in
  294. a) ADD_GAMES=true ;;
  295. c) CLEANUP_GAMES=true ;;
  296. t) game_tour ;;
  297. u) SORT_GAMES=false ;;
  298. h) usage ;;
  299. v) version ;;
  300. *) usage 1 ;;
  301. esac
  302. done
  303. shift $(($OPTIND - 1))
  304. game_check_args $@
  305. game_setup
  306. declare -a TMP_PGN_FILES
  307. game_tmp_pgns $@
  308. game_get_info
  309. game_store_pgns
  310. game_update_info
  311. game_git_commit
  312. exit 0