Skip to content

Instantly share code, notes, and snippets.

@zobar
Last active August 29, 2015 14:04
Show Gist options
  • Select an option

  • Save zobar/c68c7df7a1207edbe0b9 to your computer and use it in GitHub Desktop.

Select an option

Save zobar/c68c7df7a1207edbe0b9 to your computer and use it in GitHub Desktop.
Ruby simulation of a full adder
.rbenv-gemsets
.yardopts
*/
#!/usr/bin/env ruby -w
require 'curses'
require 'forwardable'
require_relative 'board'
require_relative 'key'
require_relative 'led'
require_relative 'logger'
require_relative 'logical'
require_relative 'toggle'
#
# Simulation of a 4-bit full adder. To toggle input bits, press the
# corresponding key.
#
class Adder
extend Forwardable
#
# Contains the Adder's components and runs the main event loop.
#
attr_reader :board
def_delegators :board, :run
#
# Creates a new Adder.
#
def initialize
Curses.init_screen
cols = Curses.cols
lines = Curses.lines
@board = Board height: lines, width: cols, x: 0, y: 0
board.logger x: 17, y: 0, height: lines, width: cols - 36
key_0_3 = switch 'd'
board.led key_0_3, label: 'D', x: 4, y: 3
key_0_2 = switch 'a'
board.led key_0_2, label: 'A', x: 7, y: 3
key_0_1 = switch 'v'
board.led key_0_1, label: 'V', x: 10, y: 3
key_0_0 = switch 'e'
board.led key_0_0, label: 'E', x: 13, y: 3
key_1_3 = switch 'r'
board.led key_1_3, label: 'R', x: 4, y: 6
key_1_2 = switch 'u'
board.led key_1_2, label: 'U', x: 7, y: 6
key_1_1 = switch 'l'
board.led key_1_1, label: 'L', x: 10, y: 6
key_1_0 = switch 'z'
board.led key_1_0, label: 'Z', x: 13, y: 6
result_0, carry_1 = half_adder key_0_0, key_1_0
board.led result_0, label: '1', x: 13, y: 9
board.led carry_1, label: ' ', x: 10, y: 0
result_1, carry_2 = full_adder carry_1, key_0_1, key_1_1
board.led result_1, label: '2', x: 10, y: 9
board.led carry_2, label: ' ', x: 7, y: 0
result_2, carry_3 = full_adder carry_2, key_0_2, key_1_2
board.led result_2, label: '4', x: 7, y: 9
board.led carry_3, label: ' ', x: 4, y: 0
result_3, result_4 = full_adder carry_3, key_0_3, key_1_3
board.led result_3, label: '8', x: 4, y: 9
board.led result_4, label: '16', x: 0, y: 9
end
private
def full_adder(carry, input_0, input_1)
result_0, carry_0 = half_adder(input_0, input_1)
result, carry_1 = half_adder(carry, result_0)
[result, board.or(carry_0, carry_1)]
end
def half_adder(input_0, input_1)
[board.xor(input_0, input_1), board.and(input_0, input_1)]
end
def log(string)
puts string
end
def switch(character)
board.toggle board.key(character)
end
end
Adder.new.run
require 'curses'
require_relative 'factories'
class Board
include Factories
#
# Visible height of the Board.
#
attr_reader :height
#
# Hash that maps keys to an Array of handlers that should be called when each
# key is pressed.
#
attr_reader :key_press_handlers
#
# Array of handlers that will be called at the end of the event loop.
#
attr_reader :soon_handlers
#
# Array of visual components.
#
attr_reader :visual
#
# Visible width of the Board.
#
attr_reader :width
#
# Curses window that is used to draw this Board and supply key events.
#
attr_reader :window
#
# Horizontal offset of the Board on the screen.
#
attr_reader :x
#
# Vertical offset of the Board on the screen.
#
attr_reader :y
#
# Registers a visual component with the Board. The component should implement
# `start` to create its Curses context, and `stop` to destroy it.
#
def add_visual(component)
visual << component
end
#
# Clears the log area.
#
def clear_log
@logger.clear unless @logger.nil?
end
def initialize(x, y, width, height)
@x, @y, @width, @height = x, y, width, height
@key_press_handlers = Hash.new { |hash, key| hash[key] = [] }
@soon_handlers = []
@visual = []
end
#
# Appends a message to the log area.
#
def log(message)
@logger.log message unless @logger.nil?
end
#
# Registers a key-press handler.
#
def on_key_press(character, &block)
@key_press_handlers[character] << block
end
#
# Initializes Curses and all visual components, runs the event loop, and
# cleans up when signaled by an `Interrupt` (Control-C).
#
def run
start
loop(&method(:event_loop))
rescue Interrupt
ensure
stop
end
#
# Registers a handler to be called at the end of the current event loop. All
# `soon` handlers are cleared after they return, so you will need to
# re-register your handler if it needs to be called on a subsequent iteration
# of the event loop.
#
def soon(&block)
@soon_handlers << block
end
private
def event_loop
Curses.doupdate
key = Curses.getch
clear_log
key_press_handlers[key].each(&:call)
until soon_handlers.empty?
handlers, @soon_handlers = @soon_handlers, []
handlers.each(&:call)
end
end
def start
Curses.cbreak
Curses.curs_set 0
Curses.noecho
@window = Curses.stdscr.subwin height, width, y, x
visual.each(&:start)
log "Press Control-C to quit."
end
def stop
visual.each(&:stop)
window.close unless window.nil?
@window = nil
Curses.close_screen
end
end
def Board(options)
Board.new(*options.values_at(:x, :y, :width, :height))
end
require 'forwardable'
#
# Base class for all logic components.
#
class Component
extend Forwardable
#
# Board that this Component belongs to.
#
attr_reader :board
#
# Handlers that will be called when this Component's value changes.
#
attr_reader :change_handlers
#
# Current value of the component.
#
attr_reader :value
def_delegators :board, :log
#
# Sets the value of the Component. If the value has changed, all registered
# `change` handlers are called.
#
def change(new_value)
if new_value != value
log "#{self} → #{new_value.inspect}"
@value = new_value
change_handlers.each(&:call)
else
log "#{self} (noop)"
end
end
def initialize(board, *args)
@board = board
@change_handlers = []
@value = false
end
#
# Registers a handler to be called when this Component's value changes.
#
def on_change(&handler)
change_handlers << handler
end
protected
def watch(*components, &calculate)
components.each do |component|
component.on_change do
change calculate.call(*components.map(&:value))
end
end
end
end
#
# Methods for creating Components on a Board.
#
module Factories
end
source 'https://rubygems.org'
gem 'curses'
group :development do
gem 'yard'
end
GEM
remote: https://rubygems.org/
specs:
curses (1.0.1)
yard (0.8.7.4)
PLATFORMS
ruby
DEPENDENCIES
curses
yard
require_relative 'strobe'
require_relative 'factories'
#
# Strobes when a specific key has been pressed.
#
class Key < Strobe
attr_reader :character
def initialize(board, character)
super
@character = character
board.on_key_press character, &method(:strobe)
end
def to_s
"Key #{character.inspect}"
end
end
module Factories
#
# Adds a Key to this Board.
#
def key(character)
Key.new self, character
end
end
require 'curses'
require_relative 'component'
require_relative 'factories'
#
# Visual component that displays a label in normal text if its input is off, or
# inverse text if its input is on.
#
class Led < Component
extend Forwardable
#
# Component that this Led uses for input.
#
attr_reader :input
#
# Label that this Led displays on-screen.
#
attr_reader :label
#
# Curses window that corresponds to this Led.
#
attr_reader :window
#
# Horizontal offset of this Led on the screen.
#
attr_reader :x
#
# Vertical offset of this Led on the screen.
#
attr_reader :y
def initialize(board, input, x, y, label)
super
@input, @label = input, label
@x = x + board.x
@y = y + board.y
watch input, &method(:calculate)
end
#
# Creates and initializes this Led's Curses window.
#
def start
@window = board.window.subwin 3, (label.size + 2), y, x
calculate input.value
end
#
# Destroys this Led's Curses window.
#
def stop
window.close unless window.nil?
@window = nil
end
def to_s
"Led #{label.inspect}"
end
private
def calculate(value)
window.attrset(value ? Curses::A_REVERSE : Curses::A_NORMAL)
window.box 0, 0
window.setpos 1, 1
window.addstr label
window.noutrefresh
value
end
end
module Factories
#
# Adds a new Led component to this Board. Available options are `:x`, `:y`,
# and `:label`.
#
def led(input, options)
add_visual Led.new(self, input, *options.values_at(:x, :y, :label))
end
end
require 'curses'
require_relative 'factories'
#
# Area for displaying a Board's event history.
#
class Logger
#
# Board that this Logger belongs to.
#
attr_reader :board
#
# Height of this Logger on-screen.
#
attr_reader :height
#
# Width of this Logger on-screen.
#
attr_reader :width
#
# Curses window that corresponds to this Logger.
#
attr_reader :window
#
# Horizontal offset of this Logger on-screen.
#
attr_reader :x
#
# Vertical offset of this Logger on-screen.
#
attr_reader :y
def initialize(board, x, y, width, height)
@board, @width, @height = board, width, height
@x = x + board.x
@y = y + board.y
end
#
# Clears the contents of this Logger.
#
def clear
window.clear
window.noutrefresh
end
#
# Logs a message.
#
def log(message)
window.addstr "#{message}\n"
window.noutrefresh
end
#
# Creates and initializes this Logger's Curses window.
#
def start
@window = board.window.subwin height, width, y, x
window.scrollok true
end
#
# Destroys this Logger's Curses window.
#
def stop
window.close unless window.nil?
@window = nil
end
end
module Factories
#
# Adds a Logger to this Board. Available options are `:height`, `:width`,
# `:x`, and `:y`.
#
def logger(options)
@logger = Logger.new(self, *options.values_at(:x, :y, :width, :height))
add_visual @logger
end
end
require_relative 'component'
require_relative 'factories'
#
# Implements components whose outputs are defined by applying pure functions to
# their inputs.
#
class Logical < Component
#
# Name of the operation performed by this Logical component.
#
attr_reader :name
def initialize(board, name, inputs, &calculate)
super
@name = name
watch(*inputs, &calculate)
end
def to_s
"Logical #{name.inspect}"
end
end
module Factories
#
# Adds an `and` gate to this Board.
#
def and(input_0, input_1)
logical('and', input_0, input_1) do |value_0, value_1|
value_0 && value_1
end
end
#
# Adds an `or` gate to this Board.
#
def or(input_0, input_1)
logical('or', input_0, input_1) do |value_0, value_1|
value_0 || value_1
end
end
#
# Adds an `xor` gate to this Board.
#
def xor(input_0, input_1)
logical('xor', input_0, input_1) do |value_0, value_1|
value_0 != value_1
end
end
private
def logical(name, *inputs, &calculate)
Logical.new self, name, inputs, &calculate
end
end
require_relative 'component'
#
# Abstract class for components that strobe their outputs (ie, signal true then
# false on the same iteration of the event loop).
#
class Strobe < Component
#
# Strobes this component's output.
#
def strobe
change true
board.soon { change false }
end
end
require_relative 'component'
require_relative 'factories'
#
# Component whose value toggles between `true` and `false` when its input
# changes from `false` to `true`.
#
class Toggle < Component
def initialize(board, input)
super
watch(input) { |input_value| input_value != value }
end
def to_s
'Toggle'
end
end
module Factories
#
# Adds a Toggle to this Board.
#
def toggle(input)
Toggle.new self, input
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment