Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/custom event loop #14

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ PATH
gli (~> 2.20, >= 2.20.1)
ice_nine (~> 0.11.2)
midilib (~> 2.0, >= 2.0.5)
ruby2d (~> 0.10.0)
ruby2d (~> 0.11.0)

GEM
remote: https://rubygems.org/
Expand Down Expand Up @@ -48,7 +48,7 @@ GEM
rubocop-ast (1.11.0)
parser (>= 3.0.1.1)
ruby-progressbar (1.11.0)
ruby2d (0.10.0)
ruby2d (0.11.0)
simplecov (0.21.2)
docile (~> 1.1)
simplecov-html (~> 0.11)
Expand Down
2 changes: 1 addition & 1 deletion fet.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
spec.add_dependency "gli", "~> 2.20", ">= 2.20.1"
spec.add_dependency "ice_nine", "~> 0.11.2"
spec.add_dependency "midilib", "~> 2.0", ">= 2.0.5"
spec.add_dependency "ruby2d", "~> 0.10.0"
spec.add_dependency "ruby2d", "~> 0.11.0"

# RDoc configuration
spec.extra_rdoc_files = ["README.rdoc", "fet.rdoc"]
Expand Down
50 changes: 50 additions & 0 deletions lib/fet/processing_queue.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# frozen_string_literal: true

module Fet
# This class defines a queue that processes items one at a time and lets you know if there are still items processing/to be processed
class ProcessingQueue
attr_reader :processing_item

def initialize
self.queue = Queue.new
self.mutex = Mutex.new
self.condition_variable = ConditionVariable.new
self.processing_item = nil
end

def push(item)
queue.push(item)
condition_variable.broadcast
end

def set_processing_item
mutex.synchronize do
condition_variable.wait(mutex) until processing_item.nil? && !queue.empty?
self.processing_item = queue.pop
end
end

def reset_processing_item
mutex.synchronize do
self.processing_item = nil
condition_variable.broadcast
end
end

def wait_until_all_items_processed
mutex.synchronize do
condition_variable.wait(mutex) while items_processing_unsafe?
end
end

private

attr_accessor :queue, :mutex, :condition_variable
attr_writer :processing_item

# WARNING: should be checked inside the mutex
def items_processing_unsafe?
return !queue.empty? || !processing_item.nil?
end
end
end
1 change: 1 addition & 0 deletions lib/fet/ui/game.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def initialize(tempo:, degrees:, key_type:, next_on_correct:)
self.score = Score.new(self)
self.level = Level.new(self)
self.timer = Timer.new(self)
initialize_synchronization_primitives
setup_window
end

Expand Down
42 changes: 9 additions & 33 deletions lib/fet/ui/game_loop_handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,56 +19,32 @@ def handle_event_loop(event)
score.handle_event_loop(event)
level.handle_event_loop(event)
timer.handle_event_loop(event)

handle_custom_events
end

def set_note_selected_event_flag
self.note_selected_event_flag = true
def note_selected_event
push_custom_event(CustomEvent.new(CustomEvent::EVENT_TYPE_NOTE_SELECTED))
end

def set_level_started_event_flag
self.level_started_event_flag = true
def level_started_event
push_custom_event(CustomEvent.new(CustomEvent::EVENT_TYPE_LEVEL_STARTED))
end

def set_level_complete_event_flag
self.level_complete_event_flag = true
def level_complete_event
push_custom_event(CustomEvent.new(CustomEvent::EVENT_TYPE_LEVEL_COMPLETE))
end

private

attr_accessor :note_selected_event_flag, :level_started_event_flag, :level_complete_event_flag
def push_custom_event(custom_event)
custom_event_queue.push(custom_event)
end

def handle_keyboard_event(event)
return unless event.is_a?(Ruby2D::Window::KeyEvent)
return unless event.type == :down

stop if event.key == "q"
end

def handle_custom_events
handle_note_selected_event
handle_level_started_event
handle_level_complete_event
end

def handle_note_selected_event
handle_event = note_selected_event_flag
self.note_selected_event_flag = false
handle_event_loop(CustomEvent.new(CustomEvent::EVENT_TYPE_NOTE_SELECTED)) if handle_event
end

def handle_level_started_event
handle_event = level_started_event_flag
self.level_started_event_flag = false
handle_event_loop(CustomEvent.new(CustomEvent::EVENT_TYPE_LEVEL_STARTED)) if handle_event
end

def handle_level_complete_event
handle_event = level_complete_event_flag
self.level_complete_event_flag = false
handle_event_loop(CustomEvent.new(CustomEvent::EVENT_TYPE_LEVEL_COMPLETE)) if handle_event
end
end
end
end
24 changes: 21 additions & 3 deletions lib/fet/ui/game_setup_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@ module Ui
module GameSetupHelper
private

attr_accessor :event_loop_mutex, :custom_event_queue

def initialize_synchronization_primitives
self.event_loop_mutex = Mutex.new
self.custom_event_queue = ProcessingQueue.new
end

def setup_window
setup_window_title
setup_window_background
setup_custom_event_loop
setup_window_event_loop
setup_window_update_loop
end
Expand All @@ -23,15 +31,25 @@ def setup_window_background
Ruby2D::Window.set(background: Fet::Ui::ColorScheme::BLACK)
end

def setup_custom_event_loop
Thread.abort_on_exception = true
Thread.new do
while custom_event_queue.set_processing_item
event_loop_mutex.synchronize { handle_event_loop(custom_event_queue.processing_item) }
custom_event_queue.reset_processing_item
end
end
end

# NOTE: don't test coverage for these methods because this is more of a test of the Ruby2D library
# :nocov:
def setup_window_event_loop
Ruby2D::Window.on(:key_down) do |event|
handle_event_loop(event)
event_loop_mutex.synchronize { handle_event_loop(event) }
end

Ruby2D::Window.on(:mouse_down) do |event|
handle_event_loop(event)
event_loop_mutex.synchronize { handle_event_loop(event) }
end
end
# :nocov:
Expand All @@ -40,7 +58,7 @@ def setup_window_event_loop
# :nocov:
def setup_window_update_loop
Ruby2D::Window.update do
handle_update_loop
event_loop_mutex.synchronize { handle_update_loop }
end
end
# :nocov:
Expand Down
5 changes: 1 addition & 4 deletions lib/fet/ui/level.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ def start
note_boxes.start
key.start

# TODO: This is not ideal because we're piggybacking on the normal events at the moment,
# but there's no event here to piggyback on, so the handler has to be called manually.
game.set_level_started_event_flag
game.handle_event_loop(nil)
game.level_started_event
end

def degree_indices
Expand Down
2 changes: 1 addition & 1 deletion lib/fet/ui/note_box_loop_handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def handle_selected(user_selected: true)
self.user_selected = user_selected
self.selected = true
update_colors
note_boxes.level.game.set_note_selected_event_flag
note_boxes.level.game.note_selected_event
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/fet/ui/note_boxes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def handle_note_selected_event(event)
return unless event.is_a?(CustomEvent) && event.type == CustomEvent::EVENT_TYPE_NOTE_SELECTED

if all_correct_selected?
level.game.set_level_complete_event_flag
level.game.level_complete_event
elsif any_wrong_selected?
correct_remaining.first.manually_select
end
Expand Down
7 changes: 7 additions & 0 deletions test/fet/ui/game_test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,12 @@ def click_note_box(game, note_box, click_button = :left)
)
game.handle_event_loop(click_event)
end
wait_for_custom_events(game)
end

def keyboard_select_note_box(game, note_box)
degree_name_to_button_presses(note_box.degree_name).each { |key| press_key(game, key) }
wait_for_custom_events(game)
end

def press_key(game, key)
Expand All @@ -101,11 +103,16 @@ def press_key(game, key)
def game_instance_test(game)
with_game_stubs do
game.start
wait_for_custom_events(game)
yield
game.stop
end
end

def wait_for_custom_events(game)
game.send(:custom_event_queue).wait_until_all_items_processed
end

def with_game_stubs
stub_ruby2d_objects { stub_our_objects { yield } }
end
Expand Down