4x4 Grid of Letters - Word Generator - Scramble with Friends - ruby

I'm attempting to create a word generator based on a 4x4 grid of letters (below).
Here are the rules:
Letters cannot be repeated
Words must be formed by adjacent letters
Words can be formed horizontally, vertically or diagonally to the left, right or up-and-down
Currently, I take a 16-character input and loop through every word in the dictionary, determining whether that word can be spelled with the letters on the grid.
#!/usr/bin/ruby
require './scores' # alphabet and associated Scrabble scoring value (ie the wordValue() method)
require './words.rb' # dictionary of English words (ie the WORDS array)
# grab users letters
puts "Provide us the 16 letters of your grid (no spaces please)"
word = gets.chomp.downcase
arr = word.split('')
# store words that can be spelled with user's letters
success = []
# iterate through dictionary of words
WORDS.each do |w|
# create temp arrays
dict_arr = w.split('')
user_arr = arr.dup
test = true
# test whether users letters spell current word in dict
while test
dict_arr.each do |letter|
if (user_arr.include?(letter))
i = user_arr.index(letter)
user_arr.delete_at(i)
else
test = false
break
end
end
# store word in array
if test
success << w
test = false
end
end
end
# create hash for successful words and their corresponding values
SUCCESS = {}
success.each do |w|
score = wordValue(w)
SUCCESS[w] = score
end
# sort hash from lowest to smallest value
SUCCESS = SUCCESS.sort_by {|word, value| value}
# print results to screen
SUCCESS.each {|k,v| puts "#{k}: #{v}"}
However, this approach doesn't take into account the positions of the tiles on the board. How would you suggest I go about finding words that can be created based on their location in the 4x4 grid?
For the board game in the image above, it takes about 1.21 seconds for my VM running Ubuntu to compute the 1185 possible words. I'm using the dictionary of words provided with Ubunut in /usr/share/dict/words

Instead of iterating over words and searching for their presence, walk through each tile on the grid and find all words stemming from that tile.
First, compile your dictionary into a trie. Tries are efficient at performing prefix-matching string comparisons, which will be of use to us shortly.
To find the words within the board, perform the following steps for each of the 16 tiles, starting with an empty string for prefix.
Add the current tile's value to prefix.
Check if our trie contains any words starting with prefix.
If it does, branch the search: for each legal (unvisited) tile that is adjacent to this tile, go back to step 1 (recurse).
If it doesn't match, stop this branch of the search since there are no matching words.

I would create a simple graph representing the whole board. Letters would be vertices. If two letters are near one another on the board I would create an edge between their vertices. It would be very easy to find out whether the input is valid. You simply would have to check whether there is a matching path in the graph.

My original answer was not what you wanted. I was creating a list of all of the
"words" in the grid, instead of searching for the words you had already identified
from the dictionary. Now I have written a function which searches the grid for a
particular word. It works recursively.
So, now the algorithm is:
1) Get the 16 letters from the user
2) Search dictionary for all words with those letters
3) Call is_word_on_board with each of those words to see if you have a match
#!/usr/bin/ruby
# This script searches a board for a word
#
# A board is represented by a string of letters, for instance, the string
# "abcdefghijklmnop" represents the board:
#
# a b c d
# e f g h
# i j k l
# m n o p
#
# The array ADJACENT lists the cell numbers that are adjacent to another
# cell. For instance ADJACENT[3] is [2, 6, 7]. If the cells are numbered
#
# 0 1 2 3
# 4 5 6 7
# 8 9 10 11
# 12 13 14 15
ADJACENT = [
[1, 4, 5],
[0, 2, 4, 5, 6],
[1, 3, 5, 6, 7],
[2, 6, 7],
[0, 1, 5, 8, 9],
[0, 1, 2, 4, 6, 8, 9, 10],
[1, 2, 3, 5, 7, 9, 10, 11],
[2, 3, 6, 10, 11],
[4, 5, 9, 12, 13],
[4, 5, 6, 8, 10, 12, 13, 14],
[5, 6, 7, 9, 11, 13, 14, 15],
[6, 7, 10, 14, 15],
[8, 9, 13],
[8, 9, 10, 12, 14],
[9, 10, 11, 13, 15],
[10, 11, 14]
]
# function: is_word_on_board
#
# parameters:
# word - word you're searching for
# board - string of letters representing the board, left to right, top to bottom
# prefix - partial word found so far
# cell - position of last letter chosen on the board
#
# returns true if word was found, false otherwise
#
# Note: You only need to provide the word and the board. The other two parameters
# have default values, and are used by the recursive calls.
# set this to true to log the recursive calls
DEBUG = false
def is_word_on_board(word, board, prefix = "", cell = -1)
if DEBUG
puts "word = #{word}"
puts "board = #{board}"
puts "prefix = #{prefix}"
puts "cell = #{cell}"
puts
end
# If we're just beginning, start word at any cell containing
# the starting letter of the word
if prefix.length == 0
0.upto(15) do |i|
if word[0] == board[i]
board_copy = board.dup
newprefix = board[i,1]
# put "*" in place of letter so we don't reuse it
board_copy[i] = ?*
# recurse, and return true if the word is found
if is_word_on_board(word, board_copy, newprefix, i)
return true
end
end
end
# we got here without finding a match, so return false
return false
elsif prefix.length == word.length
# we have the whole word!
return true
else
# search adjacent cells for the next letter in the word
ADJACENT[cell].each do |c|
# if the letter in this adjacent cell matches the next
# letter of the word, add it to the prefix and recurse
if board[c] == word[prefix.length]
newprefix = prefix + board[c, 1]
board_copy = board.dup
# put "*" in place of letter so we don't reuse it
board_copy[c] = ?*
# recurse, and return true if the word is found
if is_word_on_board(word, board_copy, newprefix, c)
return true
end
end
end
# bummer, no word found, so return false
return false
end
end
puts "Test board:"
puts
puts " r u t t"
puts " y b s i"
puts " e a r o"
puts " g h o l"
puts
board = "ruttybsiearoghol"
for word in ["ruby", "bears", "honey", "beast", "rusty", "burb", "bras", "ruttisbyearolohg", "i"]
if is_word_on_board(word, board)
puts word + " is on the board"
else
puts word + " is NOT on the board"
end
end
Running this script give the following results:
Test board:
r u t t
y b s i
e a r o
g h o l
ruby is on the board
bears is on the board
honey is NOT on the board
beast is on the board
rusty is NOT on the board
burb is NOT on the board
bras is on the board
ruttisbyearolohg is on the board
i is on the board

I happen to have a Boggle solver I wrote a while ago. It follows Cheeken's outline. It's invoked a bit differently (you supply the word list file and a text file with a 4x4 grid as arguments), but I figured it was worth sharing. Also note that it treats "Q" as "QU", so there's some extra logic in there for that.
require 'set'
def build_dict(dict, key, value)
if key.length == 0
dict[:a] = value
else
if key[0] == "q"
first = key[0..1]
rest = key[2, key.length - 1]
else
first = key[0]
rest = key[1, key.length - 1]
end
dict[first] = {} unless dict.has_key? first
build_dict(dict[first], rest, value)
end
end
dict = {}
#parse the file into a dictionary
File.open(ARGV[0]).each_line do |line|
real_line = line.strip
build_dict(dict, real_line, real_line)
end
#parse the board
board = {}
j = 0
File.open(ARGV[1]).each_line do |line|
line.chars.each_with_index do |l, i|
board[[j, i]] = l
end
j += 1
end
#(0..3).each{|r| puts (0..3).map{|c| board[[r, c]]}.join}
#how can i get from one place to another?
def get_neighbors(slot, sofar)
r, c = slot
directions =
[
[r+1, c],
[r+1, c+1],
[r+1, c-1],
[r, c+1],
[r, c-1],
[r-1, c],
[r-1, c+1],
[r-1, c-1]
]
directions.select{|a| a.all?{|d| d >= 0 && d <= 3} && !sofar.include?(a)}
end
#actual work
def solve(board, slot, word_dict, sofar)
results = Set.new
letter = board[slot]
letter = "qu" if letter == "q"
stuff = word_dict[letter]
return results if stuff.nil?
if stuff.has_key? :a
results << stuff[:a] if stuff[:a].length > 2
end
unless stuff.keys.select{|key| key != :a}.empty?
get_neighbors(slot, sofar).each do |dir|
results += solve(board, dir, stuff, sofar.clone << slot)
end
end
results
end
#do it!
results = Set.new
all_slots = (0..3).to_a.product((0..3).to_a)
all_slots.each do |slot|
results += solve(board, slot, dict, slot)
end
puts results.sort

Related

I'm trying to do a stock picker method on Ruby but i have some issue in my code

I'm trying to do a stock picker method that takes in an array of stock prices, one for each hypothetical day. It should return a pair of days representing the best day to buy and the best day to sell. Days start at 0.
def stock_picker stocks
pair = []
if stocks.size < 2
return "Please enter an array with a valid number of stocks"
else
buy_day = 0
sell_day = 0
profit = 0
stocks.each_with_index do |buy, index|
i = index
while (i < stocks[index..-1].size)
if ((buy - stocks[i]) > profit)
profit = buy - stocks[i]
buy_day = stocks.index(buy)
sell_day = i
end
i+= 1
end
end
pair = [buy_day,sell_day]
return pair.inspect
end
end
stock_picker([17,3,6,9,15,8,6,1,10])
It should return [1,4] instead of [0,7]
Another option is to slice the Array while iterating over it for finding the best profit:
res = ary.each_with_index.with_object([]) do |(buy_val, i), res|
highest_val = ary[i..].max
highest_idx = ary[i..].each_with_index.max[1] + i
res << [highest_val - buy_val, i, highest_idx]
end.max_by(&:first)
#=> [12, 1, 4]
Where 12 is the profit, 1 is the buy index and 4 is the sell index.
To understand how it works, run this extended version, it worth more than any written explanation:
res = []
ary.each_with_index do |buy_val, i|
p buy_val
p ary[i..]
p highest_val = ary[i..].max
p highest_idx = ary[i..].each_with_index.max[1] + i
res << [highest_val - buy_val, i, highest_idx]
p '----'
end
res #=> [[0, 0, 0], [12, 1, 4], [9, 2, 4], [6, 3, 4], [0, 4, 4], [2, 5, 8], [4, 6, 8], [9, 7, 8], [0, 8, 8]]
From the Ruby standard library I used Enumerable#each_with_index, Enumerable#each_with_object, Enumerable#max and Enumerable#max_by.
For getting the index of the max I kindly stole from Chuck (https://stackoverflow.com/a/2149874), thanks and +1. I didn't look for any better option.
As per a comment from Cary Swoveland in the linked post:
[..] a.index(a.max) will return the index of the first and
a.each_with_index.max[1] will return the index of the last [..]
So, maybe you want to use the first option to keep the time between buy and sell shorter.
Use Array#combination:
stocks.
each_with_index.
to_a.
combination(2).
select { |(_, idx1), (_, idx2)| idx2 > idx1 }.
reduce([-1, [-1, -1]]) do |(val, acc), ((v1, idx1), (v2, idx2))|
val < v2 - v1 ? [v2 - v1, [idx1, idx2]] : [val, acc]
end
#⇒ [ 12, [1, 4] ]
You can loop through the stock_prices array selecting for days with greatest positive difference. Your while condition needs to be changed.
#steps
#sets value of biggest_profit to 0(biggest_loss if looking for loss)
#sets most_profitable_days to [nil,nil]
#loops through array
#takes buy day
#loops through remainder of array
#if current day-first day>biggest_profit (first_day-current_day for loss)
#make >= for shortest holding period
#reassign biggest_profit
#most_profitable_days.first=buy_day, most_profitable_days.last=sell_day
#sell_day & buy_day are values of indices
#tests
#must accept only array
#must return array
#must return correct array
def stock_picker(arr)
#checks to make sure array inputs only are given
raise 'Only arrays allowed' unless arr.instance_of?(Array)
#sets value of biggest_profit to 0(biggest_loss if looking for loss)
biggest_profit=0
#sets most_profitable_days to [nil,nil]
most_profitable_days=[nil,nil]
#loops through array
arr.each_with_index do |starting_price, buy_day|
#takes buy day
arr.each_with_index do |final_price,sell_day|
#loops through remainder of array
next if sell_day<=buy_day
#if current day-first day>biggest_profit (first_day-current_day for loss)
#make '>=' for shortest holding period
if final_price-starting_price>=biggest_profit
#reassign biggest_profit
biggest_profit=final_price-starting_price
#most_profitable_days.first=buy_day,
most_profitable_days[0]=buy_day#+1 #to make it more user friendly
#most_profitable_days.last=sell_day
most_profitable_days[-1]=sell_day#+1 #to make it more user friendly
end
end
end
#return most_profitable_days
most_profitable_days
end
p stock_picker([3,2,5,4,12,3]) #[1,4]

How to find all combinations of subarrays that add to a specific value

I need to find the first combination in a given array that adds up to a specific value. The combination needs to be the lowest indexed combination as a whole.
I have most of the problem worked out:
def pairs(array_ints, sum)
array_ints.combination(2).detect {|x, y| x + y == sum}
end
This method doesn't give the combination with the lowest indexed pair. For example:
def pairs([10, 5, 2, 3, 7, 5], 10)
array_ints.combination(2).detect {|x, y| x + y == sum}
end
#output [5, 5]
#desired output [3, 7] because they appear earlier as a pair in the array.
How do I output all pairs that equal a particular sum and select the lowest indexed pair?
Taking into account your constraint from the comments: They do not have to be adjacent. The index number I'm concerned about is the second numbers in the pair. It needs to be the lowest.
def pairs(array_ints, sum)
array_ints.combination(2).inject({}) do |acc, pair|
first, second = pair
#
# Find the last occurrence of the second element. Note the use of
# 'rindex' to search from the end of the array. The same element
# may occur more than once ...
#
index = array_ints.rindex(second)
if first + second == sum
if !acc[:index] || acc[:index] > index
# Store the first match, or replace it if the stored
# index is higher than the current index
acc[:result] = pair
acc[:index] = index
end
end
acc
end.fetch(:result, [])
end
describe "pairs" do
let(:array) { [10, 5, 2, 3, 7, 5] }
describe "when there are multiple combinations that add up to the sum" do
it "finds the pair having the lowest index of the second element in the pair" do
expect(pairs(array, 10)).to eq([3,7])
expect(pairs(array, 8)).to eq([5,3])
end
end
describe "when there is no combination matching the sum" do
it "returns an empty array" do
expect(pairs(array, 1)).to eq([])
end
end
end
array_ints = [10, 5, 2, 3, 7, 5, 8, 2] and sum = 10
def pairs(array_ints, sum)
arr = []
array_ints.each_cons(2){|x,y| arr.push(x,y) if x+y==sum }
print arr.first(2)
end
# output [3, 7]

Check if numbers summed in array match input parameter

I'm following Ruby practice problem from a website and I'm completely stuck on figuring out a solution to this problem. Basically, given a function has_sum?(val, arr), return true if any combination of numbers in the array (second parameter) can be added together to equal the first parameter, otherwise return false. So:
has_sum?(5, [1, 2, 3, 4]) # true
has_sum?(5, [1, 2, 6]) # false
I'm completely stuck and not quite sure how to accomplish this... Here's what I have so far.
def has_sum?(val, arr)
arr.each_with_index do |idx, v|
# ??? no idea what to do here except add the current num to the next in the list
end
end
Any help would be greatly appreciated - thanks!
An array can produce a sum when there is a subset of any length that adds up to that sum:
def has_sum?(val, arr)
(arr.size + 1).times
.flat_map { |i| arr.combination(i).to_a }
.any? { |s| s.inject(:+) == val }
end
has_sum?(5, [5])
# => true
has_sum?(5, [1, 2, 3])
# => true
has_sum?(5, [1, 1, 1, 1, 1, 1])
# => true
has_sum?(5, [1, 2, 7])
# => false
This is not very efficient as it generates all the possibilities before testing. This should terminate sooner:
def has_sum?(val, arr)
(arr.size + 1).times.any? { |i|
arr.combination(i).any? { |s| s.inject(:+) == val }
}
end
Even more efficiently, a recursive implementation, with the idea that a sum of an empty array is zero (and has_sum(nonzero, []) should return false); for a larger array, we pop off its head, and see if the sum of the rest of the array is okay if we count, or don't count, the head element. Here, we don't do the useless summing of the whole array over and over again:
def has_sum?(val, arr)
if arr.empty?
val.zero?
else
first, *rest = arr
has_sum?(val, rest) || has_sum?(val - first, rest)
end
end
This solution employs dynamic programming. I assume that zeroes have been removed from the array. If all numbers in the array are positive, we can also remove elements that are larger than the target sum.
Code
def sum_to_target(arr, target)
h = arr.each_index.with_object({}) do |i,h|
v = arr[i]
h.keys.each do |n|
unless h.key?(n+v) # || (n+v > target)
h[n+v] = v
return reconstruct(h, target) if n+v == target
end
end
h[v] = v unless h.key?(v)
return reconstruct(h, target) if v == target
end
nil
end
def reconstruct(h, target)
a = []
loop do
i = h[target]
a.unshift i
target -= i
return a if target == 0
end
a
end
Additional efficiency improvements are possible if arr contains only postive values.1
Examples
#1
sum_to_target [2,4,7,2], 8
#=> [2, 4, 2]
#2
arr = [64, 18, 64, 6, 39, 51, 87, 62, 78, 62, 49, 86, 35, 57, 40, 15, 74, 10, 8, 7]
a = sum_to_target(arr, 461)
#=> [64, 18, 39, 51, 87, 62, 78, 62]
Let's check that.
a.reduce(:+)
#=> 461
#3
a = sum_to_target([-64, 18, 64, -6, 39, 51, -87, 62, -78, 62, 49, 86, 35, 57,
40, 15, -74, 10, -8, -7], 190)
#=> [18, 64, -6, 39, 51, -87, 62, 49]
a.reduce(:+)
#=> 190
#4
arr = 1_000.times.map { rand 1..5_000 }
#=> [3471, 1891, 4257, 2265, 832, 1060, 3961, 875, 614, 2308, 2240, 3286,
# ...
# 521, 1316, 1986, 4099, 1398, 3803, 4498, 4607, 2262, 3941, 4367]
arr is an array of 1,000 elements, each a random number between 1 and 5,000.
answer = arr.sample(500)
#=> [3469, 2957, 1542, 950, 4765, 3126, 3602, 755, 4132, 4281, 2374,
# ...
# 427, 4238, 4397, 2717, 912, 1690, 3626, 169, 3607, 4084, 3161]
answer is an array of 500 elements from arr, sampled without replacement.
target = answer.reduce(:+)
#=> 1_226_020
target is the sum of the elements of answer. We will now search arr for a collection of elements that sum to 1,226,020 (answer being one such collection).
require 'time'
t = Time.now
#=> 2016-12-12 23:00:51 -0800
a = sum_to_target(arr, target)
#=> [3471, 1891, 4257, 2265, 832, 1060, 3961, 875, 614, 2308, 2240, 3286,
# ...
# 3616, 4150, 3222, 3896, 631, 2806, 1932, 3244, 2430, 1443, 1452]
Notice that a != answer (which is not surprising).
a.reduce(:+)
#=> 1226020
(Time.now-t).to_i
#=> 60 seconds
For this last example, methods that use Array#combination would have to wade though as many as
(1..arr.size).reduce(0) { |t,i| t + arr.combination(i).size }.to_f
#~> 1.07+301
combinations.
Explanation
Let
arr = [2,4,7,2]
target = 8
Suppose we temporarily redefine reconstruct to return the hash passed to it.
def reconstruct(h, target)
h
end
We then obtain the following:
h = sum_to_target(arr, target)
#=> {2=>2, 6=>4, 4=>4, 9=>7, 13=>7, 11=>7, 7=>7, 8=>2}
h is defined as follows.
Given an array of non-zero integers arr and a number n, if n is a key of h there exists an array a containing elements from arr, in the same order, such that the elements of a sum to n and the last element of a equals h[n].
which, admittedly, is a mouthful.
We now use the reconstruct (as defined in the "Code" section) to construct an array answer that will contain elements from arr (without repeating elements) that sum to target.
reconstruct(h, target) #=> [2, 4, 2]
Initially, reconstruct initializes the array answer, which it will build and return:
answer = []
h will always contain a key equal to target (8). As h[8] #=> 2 we conclude that the last element of answer equals 2, so we execute
answer.unshift(2) #=> [2]
The problem is now to find an array of elements from arr that sum to 8 - 2 #=> 6. As h[6] #=> 4, we conclude that the element in answer that precedes the 2 we just added is 4:
answer.unshift(4) #=> [4, 2]
We now need 8-2-4 #=> 2 more to total target. As h[2] #=> 2 we execute
answer.unshift(2) #=> [2, 4, 2]
Since 8-2-4-2 #=> 0 we are finished and therefore return answer.
Notice that 4 precedes the last 2 in arr and the first 2 precedes the 4 in arr. The way h is constructed ensures the elements of answer will always be ordered in this way.
Now consider how h is constructed. First,
h = {}
As arr[0] #=> 2, we conclude that, using only the first element of arr, all we can conclude is:
h[2] = 2
h #=> {2=>2}
h has no key equal to target (8), so we continue. Now consider arr[1] #=> 4. Using only the first two elements of arr we can conclude the following:
h[2+4] = 4
h #=> {2=>2, 6=>4}
and since h has no key 4,
h[4] = 4
h #=> {2=>2, 6=>4, 4=>4}
h still has no key equal to target (8), so we press on and examine arr[2] #=> 7. Using only the first three elements of arr we conclude the following:
h[2+7] = 7
h[6+7] = 7
h[4+7] = 7
h #=> {2=>2, 6=>4, 4=>4, 9=>7, 13=>7, 11=>7}
and since h has no key 7:
h[7] = 7
h #=> {2=>2, 6=>4, 4=>4, 9=>7, 13=>7, 11=>7, 7=>7}
We added four elements to h, but since arr contains only positive numbers, those with keys 9, 13 and 11 are of no interest.
Since h still does not have a key equal to target (8), we examine the next element in arr: arr[3] #=> 2. Using only the first four elements of arr we conclude the following:
h[4+2] = 2
h[6+2] = 2
Here we stop, since 6+2 == target #=> true.
h #=> {2=>2, 6=>2, 4=>4, 9=>7, 13=>7, 11=>7, 7=>7, 8=>2}
Notice that we did not compute h[2+2] = 2 since h already has a key 4. Further, had arr contained additional elements we still would have terminated the construction of the hash at this point.
Had we modified the code to take advantage of the fact that arr contains only positive values, the final hash would have been:
h #=> {2=>2, 6=>2, 4=>4, 7=>7, 8=>2}
If this is still not clear, it might be helpful to run the code for this example with included puts statements (e.g., puts "i=#{i}, h=#{h}, v=#{v}" after the line v = arr[i] in sum_to_target, and so on).
1 The line unless h.key?(n+v) can be changed to unless h.key?(n+v) || (n+v > target) if it is known that the array contains no negative elements. (Doing so reduced the solution time for example #4 by 4 seconds.) One could also compute #all_positive = arr.all?(&:positive?) and then make that line conditional on #all_positive.
I would do nested loops.
for x = 0 to length of array
for y = x + 1 to length of array
if number at x + number at y = sum return true
return false
Basically it will check the sum of each number with each of the numbers after it.
EDIT: this will only sum 2 numbers at a time. If you want to be able to sum any amount of numbers this wont work.

Using `inject`, `unless`, and `next` to determine the minimum value

I have this code:
def test(vertices, distances)
until vertices.empty?
nearest_vertex = vertices.inject do |a, b|
p "a = #{a}: b = #{b}"
p "distances[a] = #{distances[a]}, distances[b] = #{distances[b]}"
next b unless distances[a] #next b if distances[a] == true
next a unless distances[b] #next a if distances[b] == true
next a if distances[a] < distances[b]
p "b = #{b}"
b
end
p "nearest_vertex = #{nearest_vertex}"
vertices.delete nearest_vertex
end
end
vertices = [1, 2, 3, 4, 5, 6]
distances = {1 => 0, 2 => 3, 3 => 2, 4 => 18, 5 => nil, 6 => 7}
test(vertices, distances)
Which outputs:
"a = 1: b = 2"
"distances[a] = 0, distances[b] = 3"
"a = 1: b = 3"
"distances[a] = 0, distances[b] = 2"
...
"a = 1: b = 6"
"distances[a] = 0, distances[b] = 7"
"nearest_vertex = 1"
Here, b = 6 isn't printed. Is this because next issues a stop iteration command?
"a = 2: b = 3"
"distances[a] = 3, distances[b] = 2"
"b = 3"
Why doesn't the iteration continue to a=2: b=4 here?
"a = 3: b = 4"
"distances[a] = 2, distances[b] = 18"
"a = 3: b = 5"
"distances[a] = 2, distances[b] = "
"a = 3: b = 6"
"distances[a] = 2, distances[b] = 7"
"nearest_vertex = 3"
...
Once a is set to 3, everything works as I thought it should. How does the program know that nearest_vertex is three?
I don't understand the interaction between inject and next in determining how and when to declare a vertex to be the nearest_vertex. How are the distances compared when there is no comparison operator?
Let me explain Enumerable#inject in pure Ruby. Note that the following code is NOT the original implementation of Enumerable#inject. For clarity, I will explain it in class Array, and focus on the most basic usage ary.inject(&block):
class Array
def inject(&block)
return nil if self.empty?
enumerator = self.each
accumulator = enumerator.next
loop do
begin
accumulator = block.call(accumulator, enumerator.next)
rescue StopIteration
break
end
end
accumulator
end
end
You can see that in the loop, the accumulator of previous iteration and the current element of the array is passed to the block's params, and the block's return value is reassigned to the accumulator.
Then what is next x in a block?
You can think of a block as an anonymous function, and the keyword next is its return. It terminates the current block call and makes the block return x (nil if the return value is not explicitly specified).
By the way, break x in a block terminates the method call which takes the block, and makes the method return x. For example:
[1, 2, 3, 4].inject do |acc, n|
break n if n == 2
acc + n
end
=> 2
The Array#inject is terminated by the break when n is 2, and that n is returned.
return in a block terminates the method call which calls the method that takes the block. For example:
def foo
[1, 2, 3].inject do |acc, n|
return n
end
puts 'You will never see this this sentence.'
end
foo
=> 2
And there is no sentence printed, because foo is terminated by return.
How inject works
The block passed to inject receives two arguments in each iteration. The first argument (prev_nearest_key here) is an "accumulator" whose value is whatever value was returned by the previous iteration. (For the first iteration it will be the argument given to inject or, inits absence, the first element of the collection—vertices[0] here.) The second argument (key) is the current element of the collection. When iteration is complete, the final value of the accumulator is returned.
When you call next val in a block passed to an iterator, val is immediately returned from the block and the next iteration begins. To demonstrate, here's how it looks with map:
["h", "e", "l", "l", "o"].map do |letter|
next letter.upcase if "aeoiu".include?(letter)
letter
end
# => ["h", "E", "l", "l", "O"]
Above, when letter is a vowel, letter.upcase is returned from the block and the next line is never evaluated. When letter isn't a vowel, it's returned from the block unchanged.
Here's a similar example with inject:
["h", "e", "l", "l", "o"].inject do |accum, letter|
next accum + letter.upcase if "aeoiu".include?(letter)
accum + letter
end
# => "hEllO"
What's different here? When letter is a vowel, accum + letter.upcase is returned from the block (effectively appending letter.upcase to accum), and the next line is never evaluated. When letter isn't a vowel, accum + letter is returned from the block. In both cases, the value returned from the block becomes accum in the next iteration.
How your code works
Here's your code, but with more intelligible variable names.
nearest_vertex = vertices.inject do |prev_nearest_vertex, current_vertex|
next current_vertex unless distances[prev_nearest_vertex]
next prev_nearest_vertex unless distances[current_vertex]
next prev_nearest_vertex if distances[prev_nearest_vertex] < distances[current_vertex]
current_vertex
end
I've renamed a, the accumulator, to prev_nearest_vertex, since it's the value returned by the previous iteration, and b to current_vertex.
The first two lines inside the block are just checking to see if distances[prev_nearest_vertex] and distances[current_vertex] are nil. They could (and, perhaps, should) be written like this instead:
next current_vertex if distances[prev_nearest_vertex].nil?
next prev_nearest_vertex if distances[current_vertex].nil?
The first line basically says, "If the previous nearest vertex's distance is nil, then it's not the nearest, so set prev_nearest_vertex to current_vertex in the next iteration." The second line says "If the current vertex's distance is nil, then it's not the nearest, so keep prev_nearest_vertex the same in the next iteration.
And here are the third and fourth lines:
next prev_nearest_vertex if distances[prev_nearest_vertex] < distances[current_vertex]
current_vertex
These could be rewritten like this:
if distances[prev_nearest_vertex] < distances[current_vertex]
prev_nearest_vertex
else
current_vertex
end
It just says, "Set prev_nearest_vertex in the next iteration to prev_nearest_vertex if its distance is less; otherwise set it to current_vertex.
This is pretty good code, but you should probably...
Do this instead
Ruby's Enumerable module has a lot of really useful methods, including one called min_by. It evaluates the given block for each element in an Enumerable and returns the element for which the lowest value was returned. To demonstrate, consider this map:
words = ["lorem", "ipsum", "dolor", "sit", "amet"]
words.map {|word| word.size }
# => [5, 5, 5, 3, 4]
This just turns an array of words into an array of their sizes. Now suppose we want to get the word that's the shortest. This is easy with min_by:
words = ["lorem", "ipsum", "dolor", "sit", "amet"]
words.min_by {|word| word.size }
# => "sit"
Instead of returning the words' sizes, this calculates their sizes and then returns the word whose size is the smallest.
This is directly applicable to your code. Again, consider a map operation:
vertices = [1, 2, 3, 4, 5, 6]
distances = { 1 => 0, 2 => 3, 3 => 2, 4 => 18, 5 => nil, 6 => 7 }
vertices.map do |vertex|
distances[vertex] || Float::INFINITY
end
# => [0, 3, 2, 18, Infinity, 7]
This produces an array of distances corresponding to the elements in vertices, but nil distances are replaced with Float::INFINITY. This is useful because n < Float::INFINITY is true for every number n. As before, we can now replace map with min_by to get the vertex corresponding to the smallest distance:
def test(vertices, distances)
vertices.min_by {|vertex| distances[vertex] || Float::INFINITY }
end
test(vertices, distances)
# => 1

Datatype for accessing data in multidimensional arrays

I have an array as follows:
#spaces = []
def make_board
7.times do |x|
x += 1
6.times do |y|
y += 1
#spaces << [x," "]
end
end
end
...and a series of desired combinations:
def winners
#V row 1
[ [[0][1], [1][1], [2][1], [3][1]],
[[1][1], [2][1], [3][1], [4][1]],
[[2][1], [3][1], [4][1], [5][1]],
#V row 2
[[6][1], [7][1], [8][1], [9][1]],
[[7][1], [8][1], [9][1], [10][1]],
[[8][1], [9][1], [10][1], [11][1]],
#V row 3
[[12][1], [13][1], [14][1], [15][1]],
[[13][1], [14][1], [15][1], [16][1]],
[[14][1], [15][1], [16][1], [17][1]] ]
end
Throughout the rest of the program, the " " in the #spaces array can be changed to either "X" or "O". I'm trying to write a method that detects when any of the combinations are not " " and all have the same value.
def #win? = false
def success
winners.each do |x|
fours = [ #spaces[x[0]], #spaces[x[1]], #spaces[x[2]], #spaces[x[3]] ]
if fours.each{|x| x[1] != " "}
if #spaces[x[0]] == #spaces[x[1]] && #spaces[x[1]] == #spaces[x[2]] && #spaces[x[2]] == #spaces[x[3]]
#win = true
end
end
end
end
It doesn't like the data type being passed from #winners array the #success method to search the #spaces array. It reads it as nil and wants an integer, but adding .to_i results in everything coming across as 0. Varying and assorted combinations of adding brackets, " ", and whatever else in response to the error messages has just resulted in more nil and 0. What data type is necessary to make the method work? Alternatively, in the likely case I'm going about it in a particularly stupid manner and just don't realize it yet Could anyone suggest an alternative means of accomplishing the same goal?
The 6X7 grid is a "connect four" board. Each subarray[0] in the #sample array is the column, and is used in other parts of the program. Each subarray[1] is the "X", "O", or " " (unused). The program is a game of "connect four" for learning purposes
I think your definition of winners is a little off. In particular
[0][1] # => nil
Because you're essentially creating an array [0] then immediately asking for element in position 1, which is nil (the array only has an element in position 0). Since all the arrays are single element, winners looks like this
[ [nil, nil, nil, nil],
[nil, nil, nil, nil],
[nil, nil, nil, nil],
# etc
I think what you want is this
[ [0, 1, 2, 3],
[1, 2, 3, 4],
[2, 3, 4, 5],
That will make this line
fours = [ #spaces[x[0]], #spaces[x[1]], #spaces[x[2]], #spaces[x[3]] ]
So, for the first array in winners
fours = [ #spaces[0], #spaces[1], #spaces[2], #spaces[3] ]
Which may look like
# the first 6 elements of spaces have x = 1, so all 1's here
fours = [ [1, " "], [1, " "], [1, " "], [1, " "] ]
...depending on the state of the game. After that, your code gets a little off. It looks like you're trying to check that all four of those fours has the same value in the space AND that value isn't blank. Using the structure you have:
if fours.all? { |space| space[1] != " " }
if fours[0][1] == fours[1][1] &&
fours[1][1] == fours[2][1] &&
fours[2][1] == fours[3][1]
#win = true
end
end
That should work, or at least be close, if I'm understanding your structure correctly.
ALL of that being said, I would change the structure here pretty significantly, if you can. To begin, I'd represent the board as an array of arrays, where you can access any location with just x, y coordinates, like so
board = 7.times.map { Array.new(6, " ") }
board[0][0] # => " "
You can either use 0- or 1- based coordinates, but if you're using 1 you'll have to subtract 1 every time you access the array.
Filling a square is easy
board[x][y] = "O"
You can enumerate all your winners as coordinate pairs (note the use of a constant, since this never changes):
WINNERS = [
[[0,0], [1,0], [2,0], [3,0]], # horizontal win
[[0,0], [1,1], [2,2], [3,3]], # diagonal win
# etc
]
And then check through those
WINNERS.each do |win_row|
if win_row.all? { |x, y| board[x][y] == "X" } || win_row.all? { |x, y| board[x][y] == "O" }
#win = true
break
end
end
You could algorithmically determine the winners as well, if you don't want to construct and store the winners array.
This is the first problem...
[0] is an array with only one element.
[0][1] is saying "get the second element of the array" (index 1) but there is no second element, so that returns nil.
I think instead of [0][1] you really wanted to do just 0, etc.
def winners
#V row 1
[
[0, 1, 2, 3],
[1, 2, 3, 4],
[2, 3, 4, 5],
#V row 2
[6, 7, 8, 9],
[7, 8, 9, 10],
[8, 9, 10, 11],
#V row 3
[12, 13, 14, 15],
[13, 14, 15, 16],
[14, 15, 16, 17]
]
end
Next problem is this line...
if fours.each{|x| x[1] != " "}
... doesn't do what you think it does. An each block returns the iterated array, not true or false. What you want is...
if fours.select{|x| x[1] == " "}.empty?
Which will return true if there are no cells with " " in the second element.

Resources