tournamentinfo.txt is a fixed-width text export of a
chess tournament cross-table: two lines per player, with each player’s
name, state, total points, and pre/post ratings on the first line, and
their per-round results and colors on the second. This report parses
that file into a tidy data frame and produces a CSV with each player’s
name, state, total points, pre-tournament rating, and the average
pre-tournament rating of the opponents they actually played. Source: tournamentinfo.txt.
url <- "https://raw.githubusercontent.com/NawelMe/DATA607-Fall-2026/main/project-01/tournamentinfo.txt"
raw_lines <- readLines(url, warn = FALSE)
# Drop the dashed separator lines and the 2-line column header, leaving
# exactly two lines per player.
lines <- raw_lines[!str_starts(raw_lines, "-----")]
lines <- lines[-(1:2)]
length(lines) / 2 # should be a whole number: one pair of lines per player
## [1] 64
Each player spans two consecutive lines, so this is one parsing
function applied to line pairs
(1,2), (3,4), (5,6), ...:
Pair | Name | Total Pts | Round results...):
pipe-delimited. The round columns look like W 39,
D 12, or U/H for an unplayed or
bye round — the leading letter is the result, the number (if present) is
the pair number of the opponent faced that round.State | USCF ID / Rating (Pre->Post) | ...): the rating
field looks like R: 1794 or, for a provisional rating,
R: 955P11-> 979P18 — a regex pulls out just the digits
right after R:, ignoring the P11
provisional-games marker so both formats parse the same way.parse_player <- function(i) {
l1 <- lines[i]
l2 <- lines[i + 1]
parts1 <- str_split(l1, "\\|")[[1]]
pair_num <- as.integer(str_trim(parts1[1]))
name <- str_trim(parts1[2])
points <- as.numeric(str_trim(parts1[3]))
round_cols <- str_trim(parts1[4:length(parts1)])
parts2 <- str_split(l2, "\\|")[[1]]
state <- str_trim(parts2[1])
rating_field <- str_trim(parts2[2])
pre_rating <- as.integer(str_match(rating_field, "R:\\s*(\\d+)")[, 2])
# Only rounds with an actual opponent (W/L/D followed by a number) count;
# "U" (unplayed) and "H" (half-point bye) have no opponent to average in.
opp_match <- str_match(round_cols, "^[WLD]\\s*(\\d+)")[, 2]
opponents <- as.integer(opp_match[!is.na(opp_match)])
tibble(pair = pair_num, name = name, state = state,
points = points, pre_rating = pre_rating,
opponents = list(opponents))
}
players <- map_dfr(seq(1, length(lines) - 1, by = 2), parse_player)
nrow(players)
## [1] 64
With every player’s own pre-rating known, a lookup table maps pair number to pre-rating, and each player’s list of opponent pair numbers (built above) is resolved through that lookup and averaged.
rating_by_pair <- set_names(players$pre_rating, players$pair)
players <- players |>
mutate(
avg_opponent_rating = map_dbl(
opponents,
~ if (length(.x) == 0) NA_real_ else round(mean(rating_by_pair[as.character(.x)]))
)
)
The assignment states the first player’s (Gary Hua) average opponent rating should be 1605, computed from opponents rated 1436, 1563, 1600, 1610, 1649, 1663, and 1716.
players |> filter(pair == 1) |> select(name, state, points, pre_rating, avg_opponent_rating)
## # A tibble: 1 × 5
## name state points pre_rating avg_opponent_rating
## <chr> <chr> <dbl> <int> <dbl>
## 1 GARY HUA ON 6 1794 1605
This matches the assignment’s worked example exactly, which confirms the parsing regex and the opponent-lookup logic are both correct — not just that the code runs without erroring.
result <- players |>
transmute(
`Player's Name` = name,
`Player's State` = state,
`Total Points` = points,
`Player's Pre-Rating` = pre_rating,
`Average Pre-Rating of Opponents` = avg_opponent_rating
)
write_csv(result, "tournament_results.csv")
head(result, 10)
## # A tibble: 10 × 5
## `Player's Name` `Player's State` `Total Points` `Player's Pre-Rating`
## <chr> <chr> <dbl> <int>
## 1 GARY HUA ON 6 1794
## 2 DAKSHESH DARURI MI 6 1553
## 3 ADITYA BAJAJ MI 6 1384
## 4 PATRICK H SCHILLING MI 5.5 1716
## 5 HANSHI ZUO MI 5.5 1655
## 6 HANSEN SONG OH 5 1686
## 7 GARY DEE SWATHELL MI 5 1649
## 8 EZEKIEL HOUGHTON MI 5 1641
## 9 STEFANO LEE ON 5 1411
## 10 ANVIT RAO MI 5 1365
## # ℹ 1 more variable: `Average Pre-Rating of Opponents` <dbl>
The 64-player cross-table parses cleanly into one row per player,
with the one worked example the assignment provides (Gary Hua, avg.
opponent rating 1605) reproducing exactly. Two structural quirks in the
raw format needed explicit handling rather than a generic parser:
unplayed/bye rounds (U, H) have no opponent
number and must be excluded from the average rather than treated as a
0-rated opponent, and provisional ratings are written as
955P11 rather than a plain integer, which a naive
as.integer() on the whole field would fail on. To extend
this work: compute each player’s rating change (post-rating
minus pre-rating) against their average opponent rating, to see whether
players who faced stronger opposition tended to gain more rating
regardless of their final point total.
Anthropic. (2026). Claude (model: claude-sonnet-5) [Large language model]. https://claude.ai/ — used as a support tool to help review the parsing approach, troubleshoot the regular expressions, and check the results against the assignment example. I tested, reviewed, and adjusted the code before completing the project.