Trying to cull a set but the set keeps disappearing - ruby

I'm trying to program the AI for a Mastermind game in ruby using Donal Knuth's 5 guess algorithm. The game consists of a codemaker, who uses 8 different colored pegs to create a set of 4, and a codebreaker, who guesses at the code and receives feedback (a red square for a peg which is both the right color and in the right spot, and a white square for a peg which is the right color but in the wrong spot).
I've created a set for all possible codes. My goal is to compare feedback from the guess to feedback from all codes in the set, then delete the ones that don't match. It seems to delete the entire set though.
class ComputerPlayer < Player
def initialize(game)
super(game)
#all_possible_codes = create_codes
#turn = 1
end
def get_code
Array.new(4){rand(1..6)}
end
def get_guess
puts #all_possible_codes.length
if #turn == 0
#turn += 1
cull_set([1, 1, 2, 2])
#all_possible_codes.delete("1122")
return [1, 1, 2, 2]
else
random_sample = #all_possible_codes.to_a.sample.split('').map{|str| str.to_i}
#all_possible_codes.delete(random_sample.join(''))
cull_set(random_sample)
random_sample
end
end
def cull_set(guess)
feedback = #game.feedback_on_guess(guess)
puts feedback
#all_possible_codes.delete_if { |str| #game.feedback_on_guess(str.split.map{|num| num.to_i}) != feedback }
end
def create_codes
set = Set.new
(1..8).each do |i|
(1..8).each do |j|
(1..8).each do |k|
(1..8).each do |l|
set << [i, j, k, l].join('')
end
end
end
end
set
end
end
#this is the feedback_on_guess method used by the above class
def feedback_on_guess(code_guess)
code_duplicate = #code
feedback = []
code_duplicate.map.with_index do |entry, i|
if entry == code_guess[i]
feedback.push('r')
code_guess[i] = -1
-2
else
entry
end
end.each do |entry|
found_index = code_guess.find_index(entry)
if found_index
feedback.push('g')
code_guess[found_index] = -1
end
end
puts feedback
feedback
end

Try
copy = something.dup
because after just
copy = something
copy and something are pointing to the same object. You can confirm this by checking the object_id of the object referenced by the variable. If it is the same, then it is the same object.
When you dup an object, you will cretae a copy. Depending on what you want to dup you might need to implement/override the logic to create a copy. For built in Classes like String, Hash and so on it will work out of the box.
Be aware that nested constructs (eq. Hash containing other Hashes) are not duplicated.
h1 = {"a" => {"b" => 2}}
h2 = h1.dup
puts h1.object_id # 70199597610060
puts h2.object_id # 70199597627020
puts h1["a"].object_id # 70199597610080
puts h2["a"].object_id # 70199597610080

Related

How to find count matching characters at the same indes and at an unmatching index

I have built a version of mastermind that checks a user's input and provides feedback based on how close the user's guess was to the winning sequence. If you're not familiar with the game, you get feedback indicating how many of your characters were guessed correctly at the same index and how many characters guessed are in the sequence, but at the wrong index. If there are duplicates in the guess, then you would not count the extra values unless they correspond to the same number of duplicates in the secret code.
Example: If the sequence is ["G","G","G","Y"] and the user guesses ["G", "Y","G","G"] then you'd want to return 2 for items at the same index and 2 for items at different indexes that are included in the secret sequence.
Another example: If the sequence is ["X","R","Y","T"] and the user guesses ["T","T","Y","Y"] then you'd return 1 for items at the same index 1 for the character guessed that is in the sequence but at the wrong index.
Anyway, to me this is not a simple problem to solve. Here's the code I used to get it to work, but it's not elegant. There must be a better way. I was hoping someone can tell me what I'm missing here?? New to Ruby...
def index_checker(input_array, sequence_array)
count = 0
leftover_input = []
leftover_sequence = []
input.each_with_index do |char, idx|
if char == sequence[idx]
count += 1
else
leftover_input << char
leftover_sequence << sequence[idx]
end
end
diff_index_checker(leftover_input, leftover_sequence, count)
end
def diff_index_checker(input, sequence, count)
count2 = 0
already_counted = []
input.each do |char|
if sequence.include?(char) && !already_counted.include?(char)
count2 += 1
already_counted << char
end
end
[count, count2]
end
Here's a clean Ruby solution, written in idiomatic Ruby object-oriented style:
class Mastermind
def initialize(input_array, sequence_array)
#input_array = input_array
#sequence_array = sequence_array
end
def matches
[index_matches, other_matches]
end
def results
[index_matches.size, other_matches.size]
end
private
attr_reader :input_array, :sequence_array
def index_matches
input_array.select.with_index { |e, i| e == sequence_array[i] }
end
def other_matches
non_exact_input & non_exact_sequence
end
def non_exact_input
array_difference(input_array, index_matches)
end
def non_exact_sequence
array_difference(sequence_array, index_matches)
end
# This method is based on https://stackoverflow.com/a/3852809/5961578
def array_difference(array_1, array_2)
counts = array_2.inject(Hash.new(0)) { |h, v| h[v] += 1; h }
array_1.reject { |e| counts[e] -= 1 unless counts[e].zero? }
end
end
You would use this class as follows:
>> input_array = ["G","G","G","Y"]
>> sequence_array = ["G", "Y","G","G"]
>> guess = Mastermind.new(input_array, sequence_array)
>> guess.results
#> [2, 2]
>> guess.matches
#> [["G", "G"], ["G", "Y"]]
Here's how it works. First everything goes into a class called Mastermind. We create a constructor for the class (which in Ruby is a method called initialize) and we have it accept two arguments: input array (the user guess), and sequence array (the answer).
We set each of these arguments to an instance variable, which is indicated by its beginning with #. Then we use attr_reader to create getter methods for #input_array and #sequence_array, which allows us to get the values by calling input_array and sequence_array from any instance method within the class.
We then define two public methods: matches (which returns an array of exact matches and an array of other matches (the ones that match but at the wrong index), and results (which returns a count of each of these two arrays).
Now, within the private portion of our class, we can define the guts of the logic. Each method has a specific job, and each is named to (hopefully) help a reader understand what it is doing.
index_matches returns a subset of the input_array whose elements match the sequence_array exactly.
other_matches returns a subset of the input_array whose elements do not match the sequence_array exactly, but do match at the wrong index.
other_matches relies on non_exact_input and non_exact_sequence, each of which is computed using the array_difference method, which I copied from another SO answer. (There is no convenient Ruby method that allows us to subtract one array from another without deleting duplicates).
Code
def matches(hidden, guess)
indices_wo_match = hidden.each_index.reject { |i| hidden[i] == guess[i] }
hidden_counts = counting_hash(hidden.values_at *indices_wo_match)
guess_counts = counting_hash(guess.values_at *indices_wo_match)
[hidden.size - indices_wo_match.size, guess_counts.reduce(0) { |tot, (k, cnt)|
tot + [hidden_counts[k], cnt].min }]
end
def counting_hash(arr)
arr.each_with_object(Hash.new(0)) { |s, h| h[s] += 1 }
end
Examples
matches ["G","G","G","Y"], ["G", "Y","G","G"]
#=> [2, 2]
matches ["X","R","Y","T"] , ["T","T","Y","Y"]
#=> [1, 1]
Explanation
The steps are as follows.
hidden = ["G","G","G","Y"]
guess = ["G", "Y","G","G"]
Save the indices i for which hidden[i] != guess[i].
indices_wo_match = hidden.each_index.reject { |i| hidden[i] == guess[i] }
#=> [1, 3]
Note that the number of indices for which the values are equal is as follows.
hidden.size - indices_wo_match.size
#=> 2
Now compute the numbers of remaining elements of guess that pair with one of the remaining values of hidden by having the same value. Begin by counting the numbers of instances of each unique element of hidden and then do the same for guess.
hidden_counts = counting_hash(hidden.values_at *indices_wo_match)
#=> {"G"=>1, "Y"=>1}
guess_counts = counting_hash(guess.values_at *indices_wo_match)
#=> {"Y"=>1, "G"=>1}
To understand how counting_hash works, see Hash::new, especially the explanation of the effect of providing a default value as an argument of new. In brief, if a hash is defined h = Hash.new(3), then if h does not have a key k, h[k] returns the default value, here 3 (the hash is not changed).
Now compute the numbers of matches of elements of guess that were not equal to the value of hidden at the same index and which pair with an element of hidden that have the same value.
val_matches = guess_counts.reduce(0) do |tot, (k, cnt)|
tot + [hidden_counts[k], cnt].min
end
#=> 2
Lastly, return the values of interest.
[hidden.size - indices_wo_match.size, val_matches]
#=> [2, 2]
In the code presented above I have substituted out the variable val_matches.
With Ruby 2.4+ one can use Enumerable#sum to replace
guess_counts.reduce(0) { |tot, (k, cnt)| tot + [hidden_counts[k], cnt].min }
with
guess_counts.sum { |k, cnt| [hidden_counts[k], cnt].min }
def judge(secret, guess)
full = secret.zip(guess).count { |s, g| s == g }
semi = secret.uniq.sum { |s| [secret.count(s), guess.count(s)].min } - full
[full, semi]
end
Demo:
> judge(["G","G","G","Y"], ["G","Y","G","G"])
=> [2, 2]
> judge(["X","R","Y","T"], ["T","T","Y","Y"])
=> [1, 1]
A shorter alternative, though I find it less clear:
full = secret.zip(guess).count(&:uniq!)
I prefer my other answer for its simplicity, but this one would be faster if someone wanted to use this for arrays larger than Mastermind's.
def judge(secret, guess)
full = secret.zip(guess).count { |s, g| s == g }
pool = secret.group_by(&:itself)
[full, guess.count { |g| pool[g]&.pop } - full]
end
Demo:
> judge(["G","G","G","Y"], ["G","Y","G","G"])
=> [2, 2]
> judge(["X","R","Y","T"], ["T","T","Y","Y"])
=> [1, 1]

Merging Ranges using Sets - Error - Stack level too deep (SystemStackError)

I have a number of ranges that I want merge together if they overlap. The way I’m currently doing this is by using Sets.
This is working. However, when I attempt the same code with a larger ranges as follows, I get a `stack level too deep (SystemStackError).
require 'set'
ranges = [Range.new(73, 856), Range.new(82, 1145), Range.new(116, 2914), Range.new(3203, 3241)]
set = Set.new
ranges.each { |r| set << r.to_set }
set.flatten!
sets_subsets = set.divide { |i, j| (i - j).abs == 1 } # this line causes the error
puts sets_subsets
The line that is failing is taken directly from the Ruby Set Documentation.
I would appreciate it if anyone could suggest a fix or an alternative that works for the above example
EDIT
I have put the full code I’m using here:
Basically it is used to add html tags to an amino acid sequence according to some features.
require 'set'
def calculate_formatting_classes(hsps, signalp)
merged_hsps = merge_ranges(hsps)
sp = format_signalp(merged_hsps, signalp)
hsp_class = (merged_hsps - sp[1]) - sp[0]
rank_format_positions(sp, hsp_class)
end
def merge_ranges(ranges)
set = Set.new
ranges.each { |r| set << r.to_set }
set.flatten
end
def format_signalp(merged_hsps, sp)
sp_class = sp - merged_hsps
sp_hsp_class = sp & merged_hsps # overlap regions between sp & merged_hsp
[sp_class, sp_hsp_class]
end
def rank_format_positions(sp, hsp_class)
results = []
results += sets_to_hash(sp[0], 'sp')
results += sets_to_hash(sp[1], 'sphsp')
results += sets_to_hash(hsp_class, 'hsp')
results.sort_by { |s| s[:pos] }
end
def sets_to_hash(set = nil, cl)
return nil if set.nil?
hashes = []
merged_set = set.divide { |i, j| (i - j).abs == 1 }
merged_set.each do |s|
hashes << { pos: s.min.to_i - 1, insert: "<span class=#{cl}>" }
hashes << { pos: s.max.to_i - 0.1, insert: '</span>' } # for ordering
end
hashes
end
working_hsp = [Range.new(7, 136), Range.new(143, 178)]
not_working_hsp = [Range.new(73, 856), Range.new(82, 1145),
Range.new(116, 2914), Range.new(3203, 3241)]
sp = Range.new(1, 20).to_set
# working
results = calculate_formatting_classes(working_hsp, sp)
# Not Working
# results = calculate_formatting_classes(not_working_hsp, sp)
puts results
Here is one way to do this:
ranges = [Range.new(73, 856), Range.new(82, 1145),
Range.new(116, 2914), Range.new(3203, 3241)]
ranges.size.times do
ranges = ranges.sort_by(&:begin)
t = ranges.each_cons(2).to_a
t.each do |r1, r2|
if (r2.cover? r1.begin) || (r2.cover? r1.end) ||
(r1.cover? r2.begin) || (r1.cover? r2.end)
ranges << Range.new([r1.begin, r2.begin].min, [r1.end, r2.end].max)
ranges.delete(r1)
ranges.delete(r2)
t.delete [r1,r2]
end
end
end
p ranges
#=> [73..2914, 3203..3241]
The other answers aren't bad, but I prefer a simple recursive approach:
def merge_ranges(*ranges)
range, *rest = ranges
return if range.nil?
# Find the index of the first range in `rest` that overlaps this one
other_idx = rest.find_index do |other|
range.cover?(other.begin) || other.cover?(range.begin)
end
if other_idx
# An overlapping range was found; remove it from `rest` and merge
# it with this one
other = rest.slice!(other_idx)
merged = ([range.begin, other.begin].min)..([range.end, other.end].max)
# Try again with the merged range and the remaining `rest`
merge_ranges(merged, *rest)
else
# No overlapping range was found; move on
[ range, *merge_ranges(*rest) ]
end
end
Note: This code assumes each range is ascending (e.g. 10..5 will break it).
Usage:
ranges = [ 73..856, 82..1145, 116..2914, 3203..3241 ]
p merge_ranges(*ranges)
# => [73..2914, 3203..3241]
ranges = [ 0..10, 5..20, 30..50, 45..80, 50..90, 100..101, 101..200 ]
p merge_ranges(*ranges)
# => [0..20, 30..90, 100..200]
I believe your resulting set has too many items (2881) to be used with divide, which if I understood correctly, would require 2881^2881 iterations, which is such a big number (8,7927981983090337174360463368808e+9966) that running it would take nearly forever even if you didn't get stack level too deep error.
Without using sets, you can use this code to merge the ranges:
module RangeMerger
def merge(range_b)
if cover?(range_b.first) && cover?(range_b.last)
self
elsif cover?(range_b.first)
self.class.new(first, range_b.last)
elsif cover?(range_b.last)
self.class.new(range_b.first, last)
else
nil # Unmergable
end
end
end
module ArrayRangePusher
def <<(item)
if item.kind_of?(Range)
item.extend RangeMerger
each_with_index do |own_item, idx|
own_item.extend RangeMerger
if new_range = own_item.merge(item)
self[idx] = new_range
return self
end
end
end
super
end
end
ranges = [Range.new(73, 856), Range.new(82, 1145), Range.new(116, 2914), Range.new(3203, 3241)]
new_ranges = Array.new
new_ranges.extend ArrayRangePusher
ranges.each do |range|
new_ranges << range
end
puts ranges.inspect
puts new_ranges.inspect
This will output:
[73..856, 82..1145, 116..2914, 3203..3241]
[73..2914, 3203..3241]
which I believe is the intended output for your original problem. It's a bit ugly, but I'm a bit rusty at the moment.
Edit: I don't think this has anything to do with your original problem before the edits which was about merging ranges.

Values in the while loop do not modify outside values

I have a long code but I tried to copy and adapt my problem in as few lines as possible . I have a method which creates an array( 2D ) with 0 and 1
array1 = newValue(2) - the number 2 represents how many 1 the array has
array2 = newValue(3)
and this loop
(0..9).each do|i|
(0..9).each do|j|
while((array1[i][j] == array2[i][j]) && (array2[i][j] == 1)) do
array1 = newvalue(2)
array2 = newvalue(3)
end
end
end
I'm using the while loop so I won t have a 1 in the same position in both arrays . But what is inside the while loop doesn't modify the values of the array . I also tried using map!/collect! but I think I did something wrong because nothing happened. I hope you can understand what I was trying to do .
Edit:
def newValue(value)
value = value.to_i
array = Array.new(10) { Array.new(10 , 0) }
(a lot of conditions on how to position the items in the array)
return array
end
Here's my take... hopefully it'll help out. It seems that what you noticed was true. The arrays are not getting reset. Probably because inside the each blocks, the scope is lost. This is probably because the are arrays. I took a slightly different approach. Put everything in a class so you can have instance variables that you can control and you know where they are and that they are always the same.
I pulled out the compare_arrays function which just returns the coordinates of the match if there is one. If not it returns nil. Then, youre while loop is simplified in the reprocess method. If you found a match, reprocess until you don't have a match any more. I used a dummy newValue method that just returned another 2d array (as you suggested yours does). This seems to do the trick from what I can tell. Give it a whirl and see what you think. You can access the two arrays after all the processing with processor.array1 as you can see I did at the bottom.
# generate a random 2d array with 0's and val's
def generateRandomArray(val=1)
array = []
(0..9).each do |i|
(0..9).each do |j|
array[i] ||= []
array[i][j] = (rand > 0.1) ? 0 : val
end
end
array
end
array1 = generateRandomArray
array2 = generateRandomArray
def newValue(val)
generateRandomArray(val)
end
class Processor
attr_reader :array1, :array2
def initialize(array1, array2)
#array1 = array1
#array2 = array2
end
def compare_arrays
found = false
for ii in 0..9
break unless for jj in 0..9
if ((#array2[ii][jj] == 1) && (#array1[ii][jj] == 1))
found = true
break
end
end
end
[ii,jj] if found
end
def reprocess
while compare_arrays
puts "Reprocessing"
#array1 = newValue(2)
#array2 = newValue(3)
reprocess
end
end
end
processor = Processor.new(array1, array2)
processor.reprocess
puts processor.array1.inspect

lost in a sea of rspec

I am so close. I am working to create a simple rspec test for this method. I want it to check that the method receives 'board' and returns a value of c3.
the current test looks like this...
describe 'attempt_win' do
it 'should contain all possible ai win moves' do
#player_computer.attempt_win(#board).should include(#ai_winmoves)
end
it 'should return a win move' do
# need to fake grid input here
board = Board.new
board.grid[:a1] = "O"
board.grid[:b2] = "O"
#player_computer.attempt_win(board).should include("c3") #how to say return 'c3' ??
end
end
the method I'm testing looks like this...
def attempt_win(board)
#keys_with_o = board.grid.select{ |k, v| v == "O" }.keys # find Os on the board
#answers_array = [] # initialize answers array
#ai_winmoves.each do |k, v| # go through each win move in the ai_winmoves array above.
ai_keys = v.select{ |k, v| v == "O"}.keys # grab all computer player's Os from the value hash
intersection = ai_keys & #keys_with_o
# get common elements between two arrays..note: keys_with_o = all current O's on game board
if intersection.length >=2 # when two intersections exist it means two O's are on the board
#answers_array << k # add to answers array per iteration
#answers_array.each do |key|
# answer = #anskey[#thing.last].to_sym
puts "which moves can ai win with?"
puts #anskey[key]
answer = #anskey[key].to_sym
puts "attempt win"
puts answer
if board.grid[answer] == " " #if win move space is empty take it
#move = answer
else #check for a block move
# attempt_block # handled at line 162
end
end
end
end # END #ai_winmoves.each do |k,v|
end
When I run rspec spec, my output related to this test looks like this...
attempt_win
should contain all possible ai win moves
which moves can ai win with?
c3
attempt win
c3
should return a win move (FAILED - 1)
So I need to somehow capture the "c3" in my rspec expectation
anybody point me in the right direction?
ok so the test turned out to be this...
describe 'attempt_win' do
it 'should contain all possible ai win moves' do
#player_computer.attempt_win(#board).should include(#ai_winmoves)
end
it 'should return a win move' do
# need to fake grid input here
myboard = Board.new
myboard.grid[:a1] = "O"
myboard.grid[:b2] = "O"
#player_computer.attempt_win(myboard).should eq(:c3)
end
end
The problem was located in how I was returning my answer in the method...Rspec made me write it correctly...
def attempt_win(board)
#keys_with_o = board.grid.select{ |k, v| v == "O" }.keys # find Os on the board
#answers_array = [] # initialize answers array
#ai_winmoves.each do |k, v| # go through each win move in the ai_winmoves array above.
ai_keys = v.select{ |k, v| v == "O"}.keys # grab all computer player's Os from the value hash
intersection = ai_keys & #keys_with_o
# get common elements between two arrays..note: keys_with_o = all current O's on game board
if intersection.length >=2 # when two intersections exist it means two O's are on the board
#answers_array << k # add to answers array per iteration
#answers_array.each do |key|
# puts "which moves can ai win with?"
# puts #anskey[key]
answer = #anskey[key].to_sym
# puts "attempt win"
# puts answer
if board.grid[answer] == " " #if win move space is empty take it
#move = answer
else #check for a block move
# attempt_block # handled at line 162
end
end
end
end # END #ai_winmoves.each do |k,v|
return #move
end

Somehow not assigning a class with Ruby

On runtime, my code often come into an undefined method error for the method mate. As far as I can figure, a Person somehow slips through the cracks sometime along the code's exucution, and manages not to have an allele assigned to it.
Code (disclaimer, not the best formatted):
class Allele
attr_accessor :c1, :c2
def initialize(c1, c2)
#c1 = c1
#c2 = c2
end
#formats it to be readable
def to_s
c1.to_s + c2.to_s
end
#given Allele a
def combine(a)
pick = rand(4)
case pick
when 0
Allele.new(c1,a.c1)
when 1
Allele.new(c1,a.c2)
when 2
Allele.new(c2,a.c1)
when 3
Allele.new(c2,a.c2)
end
end
end
class Person
attr_accessor :allele, :male
def initialize(allele,male)
#allele = allele
#male= male
end
#def self.num_people
###num_people
#end
def to_s
"Allele:" + allele.to_s + " | Male:" + male.to_s
end
def male
#male
end
def allele
#allele
end
def mate(p)
if rand(2) == 0
Person.new(allele.combine(p.allele),true)
else
Person.new(allele.combine(p.allele),false)
end
end
end
male_array = Array.new
female_array = Array.new
male_child_array = Array.new
female_child_array = Array.new
# EVENLY POPULATE THE ARRAY WITH 5 THAT PHENOTYPICALLY MANIFEST TRAIT, 5 THAT DON'T
# AND 5 GIRLS, 5 GUYS
pheno_dist = rand(5)
#make guys with phenotype
pheno_dist.times { male_array << Person.new(Allele.new(1,rand(2)),true) }
#guys w/o
(5-pheno_dist).times { male_array << Person.new(Allele.new(0,0),true) }
#girls w/ pheno
(5-pheno_dist).times { female_array << Person.new(Allele.new(1,rand(2)),false) }
#girls w/o
pheno_dist.times { female_array << Person.new(Allele.new(0,0),false) }
puts male_array
puts female_array
puts "----------------------"
4.times do
#mates male with females, adding children to children arrays. deletes partners as it iterates
male_array.each do
male_id = rand(male_array.length) #random selection function. adjust as needed
female_id = rand(female_array.length)
rand(8).times do
child = male_array[male_id].mate(female_array[female_id])
if child.male
male_child_array << child
else
female_child_array << child
end
end
male_array.delete_at(male_id)
female_array.delete_at(female_id)
end
#makes males male children, females female children, resets child arrays
male_array = male_child_array
female_array = female_child_array
male_child_array = []
female_child_array = []
puts male_array
puts female_array
puts "----------------------"
end
What immediately looks wrong?
As egosys says, you ought not to delete from an array over which you are iterating.
Another problem is in your loop that starts "4.times do". Sometimes the female array is empty, so returns size 0; rand(0) is a random float >= 0 and < 1. Using that as an array index on an empty female_array returns nil, which is then passed to mate.
But there's more than that wrong. You're iterating over male_array using each, but then picking a male at random. That allows some males to mate more than once; others not at all. Similarly, some females get to mate and reproduce more than once in each iteration, others not at all. Is that your intent?
Let's first consider keeping all of the males and females in the same array. This will simplify things. However, since you do need to sometimes find all the males and sometimes all the females, we'll make methods for that:
def males(population)
population.find_all do |person|
person.male?
end
end
def females(population)
population.find_all do |person|
person.female?
end
end
It would be more biologically accurate if males and females should be paired at random, but nobody gets to mate more than once. That's pretty simple:
def random_pairs(males, females)
males.shuffle[0...females.size].zip(females.shuffle)
end
Then reproduction of a population becomes, simply:
def make_children(male, female)
# return an array of children
end
def reproduce(population)
children = []
random_pairs(males(population), females(population)).each do |male, female|
children << make_children(male, female)
end
children
end
Having such functions, then doing 4 cycles of reproduction would be as simple as this:
people = # ... generate your initial list of people of all sexe.
4.times do
people = reproduce(people)
end
Since no function modifies the arguments passed to it, you will have no troubles with side-effects.
More could be done in an OO style, for example making Population a first-class object and moving the functions "males", "females", "random_pairs" and "reproduce" into it. I'll leave that as an exercise for the reader.
Deleting from an array which you are iterating with each has undefined behavior. Usually the advice is to use Array#delete_if, but I'm not sure how you would use it in this case.

Resources