Yesterday, I experienced a power outage where I’m left with just a laptop at 50% battery with Ruby interpreter and a mobile phone with limited internet access.

With nothing else to do I tried to do CS50’s Lab 6: World Cup problem, since I have no Python experience at this time, I instead tried to do it in Ruby.

I connected my laptop to my mobile data just to load the problem’s page and to download the zip file.

In the extracted zip file we have the following files:

1

answers.txt 2018m.csv 2019w.csv tournament.py

## Creating the Ruby file.

What I did:

- Created a file named
`tournament.rb`

. - Copied the content
`tournament.py`

to`tournament.rb`

. - Commented out codes that Ruby can’t interpret.
- Rewrite functions to Ruby syntax.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

# Simulate a sports tournament
# import csv
# import sys
# import random
# Number of simluations to run
N = 1000
def main()
# Ensure correct usage
# if len(sys.argv) != 2:
# sys.exit("Usage: python tournament.py FILENAME")
teams = []
# TODO: Read teams into memory from file
counts = {}
# TODO: Simulate N tournaments and keep track of win counts
# Print each team's chances of winning, according to simulation
# for team in sorted(counts, key=lambda team: counts[team], reverse=True):
# print(f"{team}: {counts[team] * 100 / N:.1f}% chance of winning")
end
def simulate_game(team1, team2)
"""Simulate a game. Return True if team1 wins, False otherwise."""
rating1 = team1["rating"]
rating2 = team2["rating"]
probability = 1 / (1 + 10 ** ((rating2 - rating1) / 600))
# return random.random() < probability
end
def simulate_round(teams)
"""Simulate a round. Return a list of winning teams."""
winners = []
# Simulate games for all pairs of teams
# for i in range(0, len(teams), 2):
# if simulate_game(teams[i], teams[i + 1]):
# winners.append(teams[i])
# else:
# winners.append(teams[i + 1])
return winners
end
def simulate_tournament(teams)
"""Simulate a tournament. Return name of winning team."""
# TODO
end
# if __name__ == "__main__":
# main()

## The `simulate_game`

function.

We need to rewrite some parts of this function to Ruby.

### What I did:

- Added
`.to_i`

to both team’s ratings, the CSV reader reads it as string, we need to convert it to integer. - Added
`.to_f`

on divisors since Ruby will return an integer instead if we keep it as is when we are expecting a float. - Removed
`return`

keyword since Ruby will always return the last line. - Replaced
`random.random()`

with Ruby equivalent`rand`

function.

1
2
3
4
5
6
7

def simulate_game(team1, team2)
"""Simulate a game. Return True if team1 wins, False otherwise."""
rating1 = team1['rating'].to_i
rating2 = team2['rating'].to_i
probability = 1 / (1 + 10 ** ((rating2 - rating1) / 600.to_f)).to_f
rand < probability
end

## The `simulate_round`

function.

What this function does is given an array of `teams`

, it iterates through `teams`

with a step of `2`

then calls `simulate_game`

function for the current team and the next team to get the winner between the 2 teams, the winner team then gets appended to the `winners`

array and returned.

### What I did:

Instead of iterating with a step of 2, I grouped the array of `teams`

by twos using `.each_slice(2).to_a`

method.

What it does:

1
2

[1, 2, 3, 4, 5, 6].each_slice(2).to_a
# => [[1, 2], [3, 4], [5, 6]]

Since we only have a pair, we can just use `first`

and `last`

to get the teams.

1
2
3
4
5
6
7
8
9
10
11
12
13
14

def simulate_round(teams)
"""Simulate a round. Return a list of winning teams."""
winners = []
teams.each_slice(2).to_a.each do |pair|
if simulate_game(pair.first, pair.last)
winners << pair.first
else
winners << pair.last
end
end
winners
end

Looking at the above code, we can even make it shorter as we don’t even need the `winners`

variable that came from the template.

1
2
3
4
5
6

def simulate_round(teams)
"""Simulate a round. Return a list of winning teams."""
teams.each_slice(2).to_a.map do |pair|
simulate_game(pair.first, pair.last) ? pair.first : pair.last
end
end

Instead of creating a new `winners`

array, we can just manipulate the grouped `teams`

array.

## The `simulate_tournament`

function.

This is where the challenge starts as we have to write the code instead of just rewriting.

To successfully simulate a tournament we need to only have one winner. Remember that the `simulate_round`

function already returns an array of `winners`

, but we just need one.

So how do we do that? We can just repeatedly call `simulate_round`

for winners only until there is only one team left.

1
2
3
4
5
6
7
8
9
10
11
12

def simulate_tournament(teams)
"""Simulate a tournament. Return name of winning team."""
winners = teams
loop do
winners = simulate_round(winners)
break if winners.length == 1
end
winners.first
end

I initially assigned `teams`

to `winners`

, I then simulated a round, where the `winners`

gets updated with only the winning teams removing the others, I then checks if we only have 1 team left, if it does then it must be the winner of the tournament, else just keep simulating rounds until 1 team is left.

Since we only have 1 winner we can just return `winners.first`

.

## The imports.

Aside from the `CSV`

library I don’t think we need anything else.

We replaced:

1
2
3

import csv
import sys
import random

With just:

1

require 'csv'

## Calling the `main`

function.

In Ruby, there really isn’t a `main`

function. All codes written outside `class .. end`

or `module .. end`

are executed in a special `main`

object.

Looking at this question: Should I define a main method in my ruby scripts?, I decided to follow one of the answers.

We simply replaced:

1
2

if __name__ == "__main__":
main()

With:

1
2
3

if __FILE__ == $PROGRAM_NAME
main
end

## The `main`

function.

### Ensuring the correct usage.

We replaced:

1
2
3

# Ensure correct usage
if len(sys.argv) != 2:
sys.exit("Usage: python tournament.py FILENAME")

With:

1
2
3
4

if ARGV.first.nil?
puts "Usage: ruby tournament.rb FILENAME"
exit
end

In Python `sys.argv`

is an array of strings separated by space that came from the command line arguments. In the case of `python tournament.py FILENAME`

, `sys.argv[0]`

has a value `tournament.py`

, `sys.argv[1]`

has a value `FILENAME`

and so on.

While in Ruby `ARGV`

is almost the same as Python’s but it does not include the current file. In the case of `ruby tournament.rb FILENAME`

, `ARGV[0]`

is `FILENAME`

and not `tournament.rb`

.

Both `sys.exit`

and `exit`

functions defaults in exiting with code `0`

.

### Reading the teams into memory from file.

We can just do it one line in Ruby.

1

teams = CSV.foreach(ARGV.first, :headers => true).map(&:to_h)

The `CSV`

library reads the CSV file from the file path given in the command line arguments. We added `:headers => true`

so that it generates a key-value pair where the key came from the first row of the CSV, then we mapped all the rows to make it a hash.

The `teams`

array should look like this:

1

[{"team"=>"Uruguay", "rating"=>"976"}, {"team"=>"Portugal", "rating"=>"1306"}, ...]

As you can see, `rating`

here is a string that is why we added `to_i`

in `simulate_game`

.

### Simulating the tournament `N`

number of times.

1

counts = {}

The `counts`

in Ruby is an instance of `Hash`

where the default value of non-existent keys are `nil`

.

Example:

1
2

counts['non_existent_key']
# => nil

But since we need to dynamically add keys and increment their values, we can’t have `nil`

as the default value for non-existent keys as the following code will raise an error.

1
2

counts['team_name'] += 1
# => undefined method `+' for nil:NilClass (NoMethodError)

We can do the long method:

1
2
3
4
5

if counts['team_name'].nil?
counts['team_name'] = 1
else
counts['team_name'] += 1
end

But we can also do the better method:

1
2
3
4
5

counts = Hash.new(0)
# or
counts = {}
counts.default = 0

The default value is now `0`

instead of `nil`

. The previous code will now work as expected.

1
2

counts['team_name'] += 1
# => 1

Integers in Ruby has a `times`

method where it creates a loop `N`

number of times.

We know that the `simulate_tournament`

we created returns a single winner of the tournament and we just have to run it `N`

number of times and then increment their score in the `counts`

object.

1
2
3
4
5

N.times do
winner = simulate_tournament teams
counts[winner['team']] += 1
end

Now that we have their scores after doing `N`

number of simulations we just need to sort it where the teams with the most score first.

1

counts = Hash[counts.sort_by { |key, value| value }.reverse]

### Printing each team’s chances of winning, according to simulation

1
2
3

counts.keys.each do |key|
puts "#{key}: #{counts[key] * 100 / N.to_f}% chance of winning"
end

## The output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

MacBook-Pro:world-cup davidangulo$ ruby tournament.rb 2018m.csv
Belgium: 21.6% chance of winning
Brazil: 18.9% chance of winning
Portugal: 15.3% chance of winning
Spain: 11.6% chance of winning
Switzerland: 10.2% chance of winning
Argentina: 7.9% chance of winning
France: 4.0% chance of winning
England: 3.3% chance of winning
Colombia: 2.2% chance of winning
Denmark: 1.9% chance of winning
Croatia: 1.2% chance of winning
Uruguay: 0.7% chance of winning
Sweden: 0.6% chance of winning
Mexico: 0.6% chance of winning

## The full script.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

# Simulate a sports tournament
require 'csv'
# Number of simluations to run
N = 1000
def main
# Ensure correct usage
if ARGV.first.nil?
puts "Usage: ruby tournament.rb FILENAME"
exit
end
teams = CSV.foreach(ARGV.first, :headers => true).map(&:to_h)
counts = Hash.new(0)
N.times do
winner = simulate_tournament(teams)
counts[winner['team']] += 1
end
counts = Hash[counts.sort_by { |key, value| value }.reverse]
# Print each team's chances of winning, according to simulation
counts.keys.each do |key|
puts "#{key}: #{counts[key] * 100 / N.to_f}% chance of winning"
end
end
def simulate_game(team1, team2)
"""Simulate a game. Return True if team1 wins, False otherwise."""
rating1 = team1['rating'].to_i
rating2 = team2['rating'].to_i
probability = 1 / (1 + 10 ** ((rating2 - rating1) / 600.to_f)).to_f
rand < probability
end
def simulate_round(teams)
"""Simulate a round. Return a list of winning teams."""
teams.each_slice(2).to_a.map do |pair|
simulate_game(pair.first, pair.last) ? pair.first : pair.last
end
end
def simulate_tournament(teams)
"""Simulate a tournament. Return name of winning team."""
winners = teams
loop do
winners = simulate_round(winners)
break if winners.length == 1
end
winners.first
end
if __FILE__ == $PROGRAM_NAME
main
end