patternrubyMinor
Generalized 2D cellular automata simulator
Viewed 0 times
generalizedcellularautomatasimulator
Problem
This is a simple Game of Life implementation, with one neat gimmick: You can specify the rules that the simulator uses to decide whether cells should live or die. Congrats, God!
I'd especially like tips on:
```
class Hash
def boundaries
return [[0, 0], [0, 0]] if self.empty?
x_bounds = self.keys.inject([self.keys[0][0]] * 2) { |(min, max), (x, _)| [min, x, max].minmax }
y_bounds = self.keys.inject([self.keys[0][1]] * 2) { |(min, max), (_, y)| [min, y, max].minmax }
[[x_bounds[0], y_bounds[0]], [x_bounds[1], y_bounds[1]]]
end
def hmap(&block)
Hash[map { |k, v| block.call(k, v) }]
end
end
class Array
def neighbors
range = (-1..1).to_a
range.product(range)
.reject { |item| item == [0,0] }
.map do |loc|
loc.zip(self).map { |arr| arr.inject(:+) }
end
end
def count
self.each_with_object(Hash.new 0) do |item, count|
count[item] += 1
end
end
end
def iterate(board, rules)
board if board.empty?
board.keys.map(&:neighbors).flatten(1).count
.hmap { |loc, count| [loc, [count, board[loc]]] }
.hmap do |loc, (count, alive)|
if rules[alive ? :s : :b].include?(count)
alive = true
else
alive = false
end
[loc, alive]
end.reject { |_, alive| !alive }
end
def draw_board(board)
puts 'No living cells!' if board.empty?
top_left, b
I'd especially like tips on:
- Idiomaticity: As always, I abuse language features, and I'd like help doing that.
- Efficiency: After a certain point, it takes a while to print out the next generation -- whether this is from figuring it out or printing it, I'm not sure. Any tips to speed up both phases are appreciated.
- Edge cases: I tested a few common patterns (spaceships, bombs, etc.) in various rules, but I don't know enough about cellular automata to make sure it works in every scenario. I can't imagine why it wouldn't, though.
- Prettiness: The output is kinda clunky. I'm open to improvement in the format of that as well.
game_of_life.rb```
class Hash
def boundaries
return [[0, 0], [0, 0]] if self.empty?
x_bounds = self.keys.inject([self.keys[0][0]] * 2) { |(min, max), (x, _)| [min, x, max].minmax }
y_bounds = self.keys.inject([self.keys[0][1]] * 2) { |(min, max), (_, y)| [min, y, max].minmax }
[[x_bounds[0], y_bounds[0]], [x_bounds[1], y_bounds[1]]]
end
def hmap(&block)
Hash[map { |k, v| block.call(k, v) }]
end
end
class Array
def neighbors
range = (-1..1).to_a
range.product(range)
.reject { |item| item == [0,0] }
.map do |loc|
loc.zip(self).map { |arr| arr.inject(:+) }
end
end
def count
self.each_with_object(Hash.new 0) do |item, count|
count[item] += 1
end
end
end
def iterate(board, rules)
board if board.empty?
board.keys.map(&:neighbors).flatten(1).count
.hmap { |loc, count| [loc, [count, board[loc]]] }
.hmap do |loc, (count, alive)|
if rules[alive ? :s : :b].include?(count)
alive = true
else
alive = false
end
[loc, alive]
end.reject { |_, alive| !alive }
end
def draw_board(board)
puts 'No living cells!' if board.empty?
top_left, b
Solution
Wow, this sure is some beautiful code, posted by someone who's probably just as beautiful, despite being a fat fu.
I'm gonna kick off this review by saying that
Now that I'm out of the corner, I feel confident in asking: how in the ever-loving RNGesus does
In
Wait, no, let's shorten that even more by not using a dumb extra variable where it's not needed:
Now, that doesn't make much sense, so let's rename a few variables and change some symbols (which is gonna end up requiring some refactoring but that's okay):
'course, that's gonna change a few things, like I said. First and foremost: You can't use that slick,
Now, changing this means that the command-line syntax changes, too -- now, it's
Both
Back to ragging on
I don't think I'll ever understand your reasons for making this
In
Bam! ASCIIArt quote for the week done.
I'm gonna kick off this review by saying that
pretty is named pretty (hah, get it?) badly. I mean, come on -- what the heck does it do? Does it return a formatted string? If that's the case, how's it formatting? I recommend a name like format_array_as_sentence, or arr_as_sentence if you want it shorter. Heck, maybe even as_sentence, if you're feeling particularly ornery. Still, you were tired and sick of being so close but not done, so that's forgivable.[[x_bounds[0], y_bounds[0]], [x_bounds[1], y_bounds[1]]] can be written better as x_bounds.zip(y_bounds), but since you didn't even know that function existed back when you wrote this, I'll let it slide. Wait, you did! Shame on you! Go sit in the corner.Now that I'm out of the corner, I feel confident in asking: how in the ever-loving RNGesus does
Array#neighbors work? I mean, it does, and it works beautifully, but how? Seriously, that's some top-level magic going on right there. Add a few comments with an example to step through what's happening, so when you have to debug because they removed product or renamed it you don't need to sit there going "whaaaaaaat" for two hours like you did last time you had to maintain code you stole from the internetIn
iterate why do you do two hmaps? Why not just one that looks like this:.hmap do |loc, count|
if rules[board[loc] ? :s : :b].include?(count)
alive = true
else
alive = false
end
[loc, alive]
endWait, no, let's shorten that even more by not using a dumb extra variable where it's not needed:
.hmap { |loc, count| [loc, rules[board[loc] ? :s : :b].include?(count)] }Now, that doesn't make much sense, so let's rename a few variables and change some symbols (which is gonna end up requiring some refactoring but that's okay):
.hmap { |loc, neighbor_count| [loc, alive_from[board[loc] ? :alive : :dead].include?(neighbor_count)] }'course, that's gonna change a few things, like I said. First and foremost: You can't use that slick,
shifty hack to get the symbols in place. You gotta do it the hard way. Well, it's not hard per se but shut up I'm doing your work for you so I get to use the words I want.alive_from = Hash[%i[alive dead].zip(ARGV.shift.split('/').map(&:chars).map{|a|a.map &:to_i})]Now, changing this means that the command-line syntax changes, too -- now, it's
X/Y, instead of BX/SY. Much easier to remember.Both
iterate and draw_board are badly-placed, since everything else is object-oriented but those two bits are functional-style. Change one or the other, or make me cry by changing both.print (board[[x, y]] ? 'X' : ' ') makes me just a little sad, since it means that you didn't realize that you should put the parens next to the method name because you can do that instead of relying on tricky spacing and expression calls.Back to ragging on
iterate: The check (board if board.empty?) at the looks like it should be useless, but damn if it doesn't speed up the empty rounds. Cute.self.each_with_object(Hash.new 0) do |item, count|
count[item] += 1
endI don't think I'll ever understand your reasons for making this
do/end instead of {/}, but change it anyway. In
draw_board, you should put a box around it, so that it's clear precisely where the boundaries are. Here's the updated code:def draw_board(board)
puts 'No living cells!' if board.empty?
top_left, bot_right = board.boundaries
puts "+#{'-'*(bot_right[1]-top_left[1])}+"
top_left[1].upto(bot_right[1]) do |y|
print '|'
top_left[0].upto(bot_right[0]) do |x|
print (board[[x, y]] ? 'X' : ' ')
end
puts '|'
end
puts "+#{'-'*(bot_right[1]-top_left[1])}+"
endBam! ASCIIArt quote for the week done.
Code Snippets
.hmap do |loc, count|
if rules[board[loc] ? :s : :b].include?(count)
alive = true
else
alive = false
end
[loc, alive]
end.hmap { |loc, count| [loc, rules[board[loc] ? :s : :b].include?(count)] }.hmap { |loc, neighbor_count| [loc, alive_from[board[loc] ? :alive : :dead].include?(neighbor_count)] }alive_from = Hash[%i[alive dead].zip(ARGV.shift.split('/').map(&:chars).map{|a|a.map &:to_i})]self.each_with_object(Hash.new 0) do |item, count|
count[item] += 1
endContext
StackExchange Code Review Q#100501, answer score: 2
Revisions (0)
No revisions yet.