Having written a more ruby like version of the processing map method, I realized it might be better to have constrain also use a range input:-
def constrain(amt, range)
return amt if range.include? amt
return range.first if amt < range.first
return range.end
end
Here is a use case example, with excluded top value version of range
CELL_SIZE = 5
ALIVE = true
DEAD = false
ALIVE_START = 150
INTERVAL = 100
attr_reader :pause, :cells, :row, :column, :last_time, :alive, :cells_buffer
def setup
size(960, 640)
@pause = false
@row = width / CELL_SIZE
@column = height / CELL_SIZE
@cells = Array.new(row) {Array.new(column) {(rand(1000) > ALIVE_START)? DEAD : ALIVE}}
@last_time = 0
@alive = color(100, 255, 100)
stroke(48, 100)
noSmooth
end
def draw
background(0)
row.times do |x|
column.times do |y|
if (cells[x][y])
fill(alive)
rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE)
end
end
end
if (millis - last_time > INTERVAL)
if (!pause)
tick!
@last_time = millis
end
end
if (pause && mouse_pressed?)
over_x = (map(mouse_x, 0, width, 0, row)).to_i
over_x = constrain(over_x, (0 ... row))
over_y = (map(mouse_y, 0, height, 0, column)).to_i
over_y = constrain(over_y, (0 ... column))
if (cells_buffer[over_x][over_y])
cells[over_x][over_y] = DEAD
fill(0)
else
cells[over_x][over_y] = ALIVE
fill(alive)
end
elsif (pause && !mouse_pressed?)
@cells_buffer = clone2d(cells)
end
end
def tick!
@cells_buffer = clone2d(cells)
row.times do |x|
column.times do |y|
neighbours = 0
(x - 1 .. x + 1).each do |xx|
(y - 1 .. y + 1).each do |yy|
if [(xx>=0), (xx<row), (yy>=0), (yy<column)].all? {|in_bounds| in_bounds == true}
if ![(xx == x), (yy == y)].all? {|is_self| is_self == true}
if (cells_buffer[xx][yy])
neighbours += 1
end
end
end
end
end
cells[x][y] = (cells_buffer[x][y])? ((2 .. 3) === neighbours) : (neighbours == 3)
end
end
end
def key_pressed
case key
when 'r', 'R'
@cells = Array.new(row) {Array.new(column) {(rand(1000) > ALIVE_START)? DEAD : ALIVE}}
when ' '
@pause = !pause
when 'c', 'C'
@cells = Array.new(row) {Array.new(column) {DEAD}}
end
end
def clone2d array
result = []
array.each do |val|
result << val.clone
end
return result
end
Here using included top value version:-
load_library :vecmath
module SeekingNeural
class Perceptron
def initialize(n, c)
@weights = Array.new(n) { rand(0 .. 1.0) }
@c = c
end
def train(forces, error)
trained = @weights.zip(forces.map { |f| f.to_a }
.map { |a, b| (a * error.x + b * error.y) * @c })
.map { |w, c| constrain(w + c, (0.0 .. 1.0)) }
@weights = trained
end
def feedforward(forces)
forces.zip(@weights).map { |a, b| a * b }.inject(Vec2D.new, :+)
end
end
class Vehicle
MAX_SPEED = 4
MAX_FORCE = 0.1
attr_reader :brain, :sz, :location, :targets, :desired
def initialize(n, x, y)
@brain = Perceptron.new(n, 0.001)
@acceleration = Vec2D.new
@velocity = Vec2D.new
@location = Vec2D.new(x, y)
@sz = 6.0
end
def update(width, height)
@velocity += @acceleration
@velocity.set_mag(MAX_SPEED) { @velocity.mag > MAX_SPEED }
@location += @velocity
@acceleration *= 0
@location.x = constrain(location.x, (0 .. width))
@location.y = constrain(location.y, (0 .. height))
end
def apply_force(force)
@acceleration += force
end
def steer(targets, desired)
forces = targets.map { |target| seek(target) }
result = brain.feedforward(forces)
apply_force(result)
error = desired - location
brain.train(forces, error)
end
def seek(target)
desired = target - location
desired.normalize!
desired *= MAX_SPEED
steer = desired - @velocity
steer.set_mag(MAX_FORCE) { steer.mag > MAX_FORCE }
steer
end
def display
theta = @velocity.heading + Math::PI / 2
fill(175)
stroke(0)
stroke_weight(1)
push_matrix
translate(location.x, location.y)
rotate(theta)
begin_shape
vertex(0, -sz)
vertex(-sz * 0.5, sz)
vertex(sz * 0.5, sz)
end_shape(CLOSE)
pop_matrix
end
end
end
include SeekingNeural
attr_reader :targets, :desired, :v
def setup
size(640, 360)
@desired = Vec2D.new(width / 2, height / 2)
make_targets
@v = Vehicle.new(targets.size, rand(width), rand(height))
end
def make_targets
@targets = Array.new(8) { Vec2D.new(rand(width), rand(height)) }
end
def draw
background(255)
stroke(0)
stroke_weight(2)
fill(0, 100)
ellipse(desired.x, desired.y, 36, 36)
targets.each do |target|
no_fill
stroke(0)
stroke_weight(2)
ellipse(target.x, target.y, 16, 16)
line(target.x, target.y - 16, target.x, target.y + 16)
line(target.x - 16, target.y, target.x + 16, target.y)
end
v.steer(targets, desired)
v.update(width, height)
v.display
end
def mouse_pressed
make_targets
end