fet 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +2 -0
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +9 -3
- data/Gemfile +2 -0
- data/Gemfile.lock +7 -1
- data/README.md +43 -6
- data/assets/fonts/PTSans/OFL.txt +94 -0
- data/assets/fonts/PTSans/PTSans-Bold.ttf +0 -0
- data/assets/fonts/PTSans/PTSans-BoldItalic.ttf +0 -0
- data/assets/fonts/PTSans/PTSans-Italic.ttf +0 -0
- data/assets/fonts/PTSans/PTSans-Regular.ttf +0 -0
- data/assets/readme/demo.gif +0 -0
- data/bin/fet +32 -5
- data/fet.gemspec +1 -0
- data/fet.rdoc +38 -0
- data/lib/fet/cli/play/listening.rb +19 -0
- data/lib/fet/degree.rb +4 -0
- data/lib/fet/degrees.rb +19 -2
- data/lib/fet/exceptions.rb +4 -0
- data/lib/fet/generator/listening.rb +21 -24
- data/lib/fet/midi_file_generator.rb +48 -0
- data/lib/fet/midilib_interface.rb +2 -26
- data/lib/fet/score.rb +52 -0
- data/lib/fet/ui/color_scheme.rb +13 -0
- data/lib/fet/ui/custom_event.rb +45 -0
- data/lib/fet/ui/game.rb +87 -0
- data/lib/fet/ui/game_loop_handler.rb +74 -0
- data/lib/fet/ui/game_setup_helper.rb +49 -0
- data/lib/fet/ui/key.rb +44 -0
- data/lib/fet/ui/level.rb +108 -0
- data/lib/fet/ui/level_loop_handler.rb +57 -0
- data/lib/fet/ui/note_box.rb +117 -0
- data/lib/fet/ui/note_box_loop_handler.rb +107 -0
- data/lib/fet/ui/note_boxes.rb +81 -0
- data/lib/fet/ui/score.rb +85 -0
- data/lib/fet/ui/timer.rb +81 -0
- data/lib/fet/version.rb +1 -1
- data/lib/fet.rb +3 -0
- metadata +38 -3
- data/LICENSE.txt +0 -21
    
        data/lib/fet/exceptions.rb
    CHANGED
    
    | @@ -13,6 +13,10 @@ module Fet | |
| 13 13 |  | 
| 14 14 | 
             
              class InvalidDegreeName < Error; end
         | 
| 15 15 |  | 
| 16 | 
            +
              class InvalidCustomEventType < Error; end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              class ImplementationError < Error; end
         | 
| 19 | 
            +
             | 
| 16 20 | 
             
              # TODO: this can be removed if the circle of fifths is generated dynamically
         | 
| 17 21 | 
             
              class UnsupportedRootName < Error; end
         | 
| 18 22 | 
             
            end
         | 
| @@ -29,13 +29,13 @@ module Fet | |
| 29 29 | 
             
                  def generate_all_single_degree_exercises
         | 
| 30 30 | 
             
                    Fet::MAJOR_ROOT_MIDI_VALUES.each do |root|
         | 
| 31 31 | 
             
                      note_range.each do |note|
         | 
| 32 | 
            -
                         | 
| 32 | 
            +
                        select_notes([note], root[0], root[1], 1, "major")
         | 
| 33 33 | 
             
                      end
         | 
| 34 34 | 
             
                    end
         | 
| 35 35 |  | 
| 36 36 | 
             
                    Fet::MINOR_ROOT_MIDI_VALUES.each do |root|
         | 
| 37 37 | 
             
                      note_range.each do |note|
         | 
| 38 | 
            -
                         | 
| 38 | 
            +
                        select_notes([note], root[0], root[1], 1, "minor")
         | 
| 39 39 | 
             
                      end
         | 
| 40 40 | 
             
                    end
         | 
| 41 41 | 
             
                  end
         | 
| @@ -43,53 +43,50 @@ module Fet | |
| 43 43 | 
             
                  def generate_number_of_exercises
         | 
| 44 44 | 
             
                    number_of_exercises.times do
         | 
| 45 45 | 
             
                      # Create major key exercises
         | 
| 46 | 
            -
                       | 
| 47 | 
            -
                      until  | 
| 46 | 
            +
                      root_name, root_midi_value = Fet::MAJOR_ROOT_MIDI_VALUES.to_a.sample
         | 
| 47 | 
            +
                      until select_notes(note_range, root_name, root_midi_value, number_of_degrees, "major"); end
         | 
| 48 48 |  | 
| 49 49 | 
             
                      # Create minor key exercises
         | 
| 50 | 
            -
                       | 
| 51 | 
            -
                      until  | 
| 50 | 
            +
                      root_name, root_midi_value = Fet::MINOR_ROOT_MIDI_VALUES.to_a.sample
         | 
| 51 | 
            +
                      until select_notes(note_range, root_name, root_midi_value, number_of_degrees, "minor"); end
         | 
| 52 52 | 
             
                    end
         | 
| 53 53 | 
             
                  end
         | 
| 54 54 |  | 
| 55 | 
            -
                  def  | 
| 56 | 
            -
                     | 
| 57 | 
            -
             | 
| 58 | 
            -
                     | 
| 59 | 
            -
                    chosen_notes  | 
| 60 | 
            -
             | 
| 61 | 
            -
                    all_notes_without_note_degree = all_notes.reject { |note| Fet::MidiNote.new(note).degree(root[1]) == Fet::MidiNote.new(selected_note).degree(root[1]) }
         | 
| 62 | 
            -
                    select_notes_recursive(all_notes_without_note_degree, chosen_notes, root, number_degrees - 1, key_type)
         | 
| 55 | 
            +
                  def select_notes(all_notes, root_name, root_midi_value, number_degrees, key_type)
         | 
| 56 | 
            +
                    root_octave_value = Fet::MidiNote.new(root_midi_value).octave_number
         | 
| 57 | 
            +
                    degrees_instance = Fet::Degrees.new(root_name: root_name, octave_value: root_octave_value)
         | 
| 58 | 
            +
                    chosen_notes = degrees_instance.select_degrees_from_midi_values(all_notes, number_degrees)
         | 
| 59 | 
            +
                    return create_midi_file(chosen_notes, root_name, root_midi_value, key_type)
         | 
| 63 60 | 
             
                  end
         | 
| 64 61 |  | 
| 65 | 
            -
                  def create_midi_file(chosen_notes,  | 
| 62 | 
            +
                  def create_midi_file(chosen_notes, root_name, root_midi_value, key_type)
         | 
| 66 63 | 
             
                    # Sort so that the file name corresponds to degree of lowest to highest
         | 
| 67 64 | 
             
                    chosen_notes = chosen_notes.sort
         | 
| 68 65 |  | 
| 69 | 
            -
                    filename = full_filename(key_type,  | 
| 66 | 
            +
                    filename = full_filename(key_type, root_name, root_midi_value, chosen_notes)
         | 
| 70 67 | 
             
                    return false if File.exist?(filename)
         | 
| 71 68 |  | 
| 72 | 
            -
                    progression = Fet::ChordProgression.new(offset:  | 
| 69 | 
            +
                    progression = Fet::ChordProgression.new(offset: root_midi_value, template_type: key_type).with_offset
         | 
| 73 70 | 
             
                    Fet::MidilibInterface.new(
         | 
| 74 | 
            -
                      tempo: tempo, progression: progression, notes: chosen_notes, info: generate_midi_info(key_type,  | 
| 71 | 
            +
                      tempo: tempo, progression: progression, notes: chosen_notes, info: generate_midi_info(key_type, root_name, root_midi_value, chosen_notes), filename: filename,
         | 
| 75 72 | 
             
                    ).create_listening_midi_file
         | 
| 76 73 | 
             
                    return true
         | 
| 77 74 | 
             
                  end
         | 
| 78 75 |  | 
| 79 | 
            -
                  def generate_midi_info(key_type,  | 
| 76 | 
            +
                  def generate_midi_info(key_type, root_name, root_midi_value, chosen_notes)
         | 
| 80 77 | 
             
                    return [
         | 
| 81 | 
            -
                      "Key: [#{ | 
| 82 | 
            -
                      "Degrees: [#{chosen_notes.map { |i| Fet::MusicTheory::DEGREES[(i -  | 
| 78 | 
            +
                      "Key: [#{root_name} #{key_type}]",
         | 
| 79 | 
            +
                      "Degrees: [#{chosen_notes.map { |i| Fet::MusicTheory::DEGREES[(i - root_midi_value) % 12] }}]",
         | 
| 83 80 | 
             
                      "Notes: [#{chosen_notes}]",
         | 
| 84 81 | 
             
                    ].join(" ")
         | 
| 85 82 | 
             
                  end
         | 
| 86 83 |  | 
| 87 | 
            -
                  def full_filename(key_type,  | 
| 84 | 
            +
                  def full_filename(key_type, root_name, root_midi_value, chosen_notes)
         | 
| 88 85 | 
             
                    result = File.join(*[directory_prefix, "listening", key_type].reject(&:empty?))
         | 
| 89 | 
            -
                    filename =  | 
| 86 | 
            +
                    filename = root_name # note, e.g. Db
         | 
| 90 87 | 
             
                    filename += key_type == "major" ? "M" : "m" # type of note, M or m
         | 
| 91 88 | 
             
                    filename += "_" # delimiter
         | 
| 92 | 
            -
                    filename += chosen_notes.map { |i| note_filename_part( | 
| 89 | 
            +
                    filename += chosen_notes.map { |i| note_filename_part(root_name, root_midi_value, i) }.join("_") # chosen notes description, e.g. b7(Cb4)
         | 
| 93 90 | 
             
                    filename += ".mid" # extension
         | 
| 94 91 | 
             
                    return File.join(result, filename)
         | 
| 95 92 | 
             
                  end
         | 
| @@ -0,0 +1,48 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Fet
         | 
| 4 | 
            +
              # Use MidilibInterface to create concrete MIDI files
         | 
| 5 | 
            +
              module MidiFileGenerator
         | 
| 6 | 
            +
                def create_full_question
         | 
| 7 | 
            +
                  set_progression_on_track
         | 
| 8 | 
            +
                  add_rest(2 * quarter_note_length)
         | 
| 9 | 
            +
                  play_notes_as_chord(notes, quarter_note_length)
         | 
| 10 | 
            +
                  add_rest(3 * quarter_note_length)
         | 
| 11 | 
            +
                  write_sequence_to_file
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def create_chord_progression_of_question
         | 
| 15 | 
            +
                  set_progression_on_track
         | 
| 16 | 
            +
                  add_rest(1 * quarter_note_length)
         | 
| 17 | 
            +
                  write_sequence_to_file
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def create_notes_only
         | 
| 21 | 
            +
                  play_notes_as_chord(notes, quarter_note_length)
         | 
| 22 | 
            +
                  add_rest(1 * quarter_note_length)
         | 
| 23 | 
            +
                  write_sequence_to_file
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def create_listening_midi_file
         | 
| 27 | 
            +
                  set_progression_on_track
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  add_rest(2 * quarter_note_length)
         | 
| 30 | 
            +
                  play_notes_as_chord(notes, quarter_note_length)
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  add_rest(6 * quarter_note_length)
         | 
| 33 | 
            +
                  play_notes_sequentially(notes, quarter_note_length)
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  write_sequence_to_file
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                def create_singing_midi_file(sleep_duration)
         | 
| 39 | 
            +
                  set_progression_on_track
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  add_seconds_of_rest(sleep_duration)
         | 
| 42 | 
            +
                  play_notes_sequentially(notes, quarter_note_length)
         | 
| 43 | 
            +
                  add_rest(3 * quarter_note_length)
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  write_sequence_to_file
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
              end
         | 
| 48 | 
            +
            end
         | 
| @@ -6,6 +6,8 @@ require "midilib/consts" | |
| 6 6 | 
             
            module Fet
         | 
| 7 7 | 
             
              # Interface with the midilib library to generate MIDI files
         | 
| 8 8 | 
             
              class MidilibInterface
         | 
| 9 | 
            +
                include MidiFileGenerator
         | 
| 10 | 
            +
             | 
| 9 11 | 
             
                def initialize(tempo:, progression:, notes:, info:, filename:)
         | 
| 10 12 | 
             
                  self.tempo = tempo
         | 
| 11 13 | 
             
                  self.progression = progression
         | 
| @@ -16,31 +18,6 @@ module Fet | |
| 16 18 | 
             
                  self.track = generate_instrument_track
         | 
| 17 19 | 
             
                end
         | 
| 18 20 |  | 
| 19 | 
            -
                def create_listening_midi_file
         | 
| 20 | 
            -
                  # Play the chord progression
         | 
| 21 | 
            -
                  set_progression_on_track
         | 
| 22 | 
            -
             | 
| 23 | 
            -
                  add_rest(2 * quarter_note_length)
         | 
| 24 | 
            -
                  play_notes_as_chord(notes, quarter_note_length)
         | 
| 25 | 
            -
             | 
| 26 | 
            -
                  add_rest(6 * quarter_note_length)
         | 
| 27 | 
            -
                  play_notes_sequentially(notes, quarter_note_length)
         | 
| 28 | 
            -
             | 
| 29 | 
            -
                  write_sequence_to_file
         | 
| 30 | 
            -
                end
         | 
| 31 | 
            -
             | 
| 32 | 
            -
                def create_singing_midi_file(sleep_duration)
         | 
| 33 | 
            -
                  # Play the chord progression
         | 
| 34 | 
            -
                  set_progression_on_track
         | 
| 35 | 
            -
             | 
| 36 | 
            -
                  # Play the note after waiting for a specified amount of time
         | 
| 37 | 
            -
                  add_seconds_of_rest(sleep_duration) do
         | 
| 38 | 
            -
                    play_notes_sequentially(notes, quarter_note_length)
         | 
| 39 | 
            -
                  end
         | 
| 40 | 
            -
             | 
| 41 | 
            -
                  write_sequence_to_file
         | 
| 42 | 
            -
                end
         | 
| 43 | 
            -
             | 
| 44 21 | 
             
                private
         | 
| 45 22 |  | 
| 46 23 | 
             
                attr_accessor :tempo, :progression, :notes, :info, :filename, :sequence, :track
         | 
| @@ -56,7 +33,6 @@ module Fet | |
| 56 33 | 
             
                  with_temporary_tempo_change(60) do
         | 
| 57 34 | 
             
                    # Sleep for the requested duration
         | 
| 58 35 | 
             
                    add_rest(seconds * quarter_note_length)
         | 
| 59 | 
            -
                    yield
         | 
| 60 36 | 
             
                  end
         | 
| 61 37 | 
             
                end
         | 
| 62 38 |  | 
    
        data/lib/fet/score.rb
    ADDED
    
    | @@ -0,0 +1,52 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "json"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Fet
         | 
| 6 | 
            +
              # Holds the correct/incorrect answers to questions
         | 
| 7 | 
            +
              class Score
         | 
| 8 | 
            +
                def initialize
         | 
| 9 | 
            +
                  self.score = initialize_score
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def answer_correctly(*degree_indices)
         | 
| 13 | 
            +
                  degree_indices.each { |degree_index| score[degree_index][:correct] += 1 }
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def answer_incorrectly(*degree_indices)
         | 
| 17 | 
            +
                  degree_indices.each { |degree_index| score[degree_index][:incorrect] += 1 }
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def answered_correctly(degree_index = nil)
         | 
| 21 | 
            +
                  return score.reduce(0) do |result, (score_degree_index, score_hash)|
         | 
| 22 | 
            +
                    degree_index.nil? || score_degree_index == degree_index ? result + score_hash[:correct] : result
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def answered_incorrectly(degree_index = nil)
         | 
| 27 | 
            +
                  return score.reduce(0) do |result, (score_degree_index, score_hash)|
         | 
| 28 | 
            +
                    degree_index.nil? || score_degree_index == degree_index ? result + score_hash[:incorrect] : result
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def questions_asked(degree_index = nil)
         | 
| 33 | 
            +
                  return answered_correctly(degree_index) + answered_incorrectly(degree_index)
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def as_json(_options = {})
         | 
| 37 | 
            +
                  score
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def to_json(*options)
         | 
| 41 | 
            +
                  as_json(*options).to_json(*options)
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                private
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                attr_accessor :score
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                def initialize_score
         | 
| 49 | 
            +
                  Fet::Degree::DEGREE_NAMES.map.with_index { |_, degree_index| [degree_index, { correct: 0, incorrect: 0 }] }.to_h
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
              end
         | 
| 52 | 
            +
            end
         | 
| @@ -0,0 +1,13 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Fet
         | 
| 4 | 
            +
              module Ui
         | 
| 5 | 
            +
                module ColorScheme
         | 
| 6 | 
            +
                  BLACK = "#151727".deep_freeze
         | 
| 7 | 
            +
                  GREY = "#393D56".deep_freeze
         | 
| 8 | 
            +
                  WHITE = "#FAF9F0".deep_freeze
         | 
| 9 | 
            +
                  RED = "#931621".deep_freeze
         | 
| 10 | 
            +
                  GREEN = "#2F754E".deep_freeze
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
            end
         | 
| @@ -0,0 +1,45 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Fet
         | 
| 4 | 
            +
              module Ui
         | 
| 5 | 
            +
                # Custom events for the event loop
         | 
| 6 | 
            +
                class CustomEvent
         | 
| 7 | 
            +
                  attr_accessor :type
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  EVENT_TYPE_NOTE_SELECTED = :note_selected
         | 
| 10 | 
            +
                  EVENT_TYPE_LEVEL_STARTED = :level_started
         | 
| 11 | 
            +
                  EVENT_TYPE_LEVEL_COMPLETE = :level_complete
         | 
| 12 | 
            +
                  EVENT_TYPES = [
         | 
| 13 | 
            +
                    EVENT_TYPE_NOTE_SELECTED,
         | 
| 14 | 
            +
                    EVENT_TYPE_LEVEL_STARTED,
         | 
| 15 | 
            +
                    EVENT_TYPE_LEVEL_COMPLETE,
         | 
| 16 | 
            +
                  ].deep_freeze
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def initialize(type)
         | 
| 19 | 
            +
                    self.type = type
         | 
| 20 | 
            +
                    validate_type!
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  private
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  def validate_type!
         | 
| 26 | 
            +
                    raise InvalidCustomEventType unless EVENT_TYPES.include?(type)
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
              end
         | 
| 30 | 
            +
            end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            # NOTE: alternatively I started monkey patching the Ruby2D::Window class, though it's probably not the best idea
         | 
| 33 | 
            +
            # but it WOULD let us call events outside of the standard Ruby2D events.
         | 
| 34 | 
            +
            # module Ruby2D
         | 
| 35 | 
            +
            #   class Window
         | 
| 36 | 
            +
            #     CustomEvent = Struct.new(:type)
         | 
| 37 | 
            +
            #     @events[:custom] = {}
         | 
| 38 | 
            +
            #
         | 
| 39 | 
            +
            #     def custom_callback(type)
         | 
| 40 | 
            +
            #       @events[:custom].each do |_id, e|
         | 
| 41 | 
            +
            #         e.call(customEvent.new(type))
         | 
| 42 | 
            +
            #       end
         | 
| 43 | 
            +
            #     end
         | 
| 44 | 
            +
            #   end
         | 
| 45 | 
            +
            # end
         | 
    
        data/lib/fet/ui/game.rb
    ADDED
    
    | @@ -0,0 +1,87 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "ruby2d"
         | 
| 4 | 
            +
            require_relative "game_loop_handler"
         | 
| 5 | 
            +
            require_relative "game_setup_helper"
         | 
| 6 | 
            +
            require_relative "level"
         | 
| 7 | 
            +
            require_relative "score"
         | 
| 8 | 
            +
            require_relative "timer"
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            module Fet
         | 
| 11 | 
            +
              module Ui
         | 
| 12 | 
            +
                # Holds the state of the current game
         | 
| 13 | 
            +
                class Game
         | 
| 14 | 
            +
                  include GameSetupHelper
         | 
| 15 | 
            +
                  include GameLoopHandler
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  SCORES_FILENAME = "#{ENV["HOME"]}/.config/fet/scores".deep_freeze
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  attr_accessor :level, :score, :timer, :note_range,
         | 
| 20 | 
            +
                                :tempo, :number_of_degrees, :key_type, :next_on_correct
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  # NOTE: this is explicitly changed in tests, so no need to check for coverage
         | 
| 23 | 
            +
                  # :nocov:
         | 
| 24 | 
            +
                  def self.scores_filename
         | 
| 25 | 
            +
                    return SCORES_FILENAME
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
                  # :nocov:
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  def initialize(tempo:, degrees:, key_type:, next_on_correct:)
         | 
| 30 | 
            +
                    self.note_range = Fet::REDUCED_BY_OCTAVE_PIANO_RANGE
         | 
| 31 | 
            +
                    self.tempo = tempo
         | 
| 32 | 
            +
                    self.key_type = key_type
         | 
| 33 | 
            +
                    self.number_of_degrees = degrees
         | 
| 34 | 
            +
                    self.next_on_correct = next_on_correct
         | 
| 35 | 
            +
                    self.score = Score.new(self)
         | 
| 36 | 
            +
                    self.level = Level.new(self)
         | 
| 37 | 
            +
                    self.timer = Timer.new(self)
         | 
| 38 | 
            +
                    setup_window
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  def start
         | 
| 42 | 
            +
                    score.start
         | 
| 43 | 
            +
                    level.start
         | 
| 44 | 
            +
                    timer.start
         | 
| 45 | 
            +
                    show_window
         | 
| 46 | 
            +
                    write_score_to_file
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def stop
         | 
| 50 | 
            +
                    close_window
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  private
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  def show_window
         | 
| 56 | 
            +
                    Ruby2D::Window.show
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  def close_window
         | 
| 60 | 
            +
                    Ruby2D::Window.close
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  def write_score_to_file
         | 
| 64 | 
            +
                    new_score_entries = historic_score_entries
         | 
| 65 | 
            +
                    new_score_entries << current_score_entry
         | 
| 66 | 
            +
                    directory_name = File.dirname(self.class.scores_filename)
         | 
| 67 | 
            +
                    FileUtils.mkdir_p(directory_name)
         | 
| 68 | 
            +
                    File.open(self.class.scores_filename, "w") { |file| file.write(new_score_entries.to_json) }
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  def historic_score_entries
         | 
| 72 | 
            +
                    result = File.read(self.class.scores_filename)
         | 
| 73 | 
            +
                    return JSON.parse(result)
         | 
| 74 | 
            +
                  rescue Errno::ENOENT
         | 
| 75 | 
            +
                    return []
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  def current_score_entry
         | 
| 79 | 
            +
                    return {
         | 
| 80 | 
            +
                      "started_at" => timer.started_at.to_s,
         | 
| 81 | 
            +
                      "seconds_elapsed" => timer.seconds_elapsed,
         | 
| 82 | 
            +
                      "score" => score.score,
         | 
| 83 | 
            +
                    }
         | 
| 84 | 
            +
                  end
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
              end
         | 
| 87 | 
            +
            end
         | 
| @@ -0,0 +1,74 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative "custom_event"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Fet
         | 
| 6 | 
            +
              module Ui
         | 
| 7 | 
            +
                # Handles various events and updates for the Game object
         | 
| 8 | 
            +
                module GameLoopHandler
         | 
| 9 | 
            +
                  def handle_update_loop
         | 
| 10 | 
            +
                    score.handle_update_loop
         | 
| 11 | 
            +
                    level.handle_update_loop
         | 
| 12 | 
            +
                    timer.handle_update_loop
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def handle_event_loop(event)
         | 
| 16 | 
            +
                    handle_keyboard_event(event)
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    # NOTE: score must handle event before level because level event could recreate the whole level
         | 
| 19 | 
            +
                    score.handle_event_loop(event)
         | 
| 20 | 
            +
                    level.handle_event_loop(event)
         | 
| 21 | 
            +
                    timer.handle_event_loop(event)
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    handle_custom_events
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  def set_note_selected_event_flag
         | 
| 27 | 
            +
                    self.note_selected_event_flag = true
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  def set_level_started_event_flag
         | 
| 31 | 
            +
                    self.level_started_event_flag = true
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  def set_level_complete_event_flag
         | 
| 35 | 
            +
                    self.level_complete_event_flag = true
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  private
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  attr_accessor :note_selected_event_flag, :level_started_event_flag, :level_complete_event_flag
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  def handle_keyboard_event(event)
         | 
| 43 | 
            +
                    return unless event.is_a?(Ruby2D::Window::KeyEvent)
         | 
| 44 | 
            +
                    return unless event.type == :down
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                    stop if event.key == "q"
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def handle_custom_events
         | 
| 50 | 
            +
                    handle_note_selected_event
         | 
| 51 | 
            +
                    handle_level_started_event
         | 
| 52 | 
            +
                    handle_level_complete_event
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  def handle_note_selected_event
         | 
| 56 | 
            +
                    handle_event = note_selected_event_flag
         | 
| 57 | 
            +
                    self.note_selected_event_flag = false
         | 
| 58 | 
            +
                    handle_event_loop(CustomEvent.new(CustomEvent::EVENT_TYPE_NOTE_SELECTED)) if handle_event
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  def handle_level_started_event
         | 
| 62 | 
            +
                    handle_event = level_started_event_flag
         | 
| 63 | 
            +
                    self.level_started_event_flag = false
         | 
| 64 | 
            +
                    handle_event_loop(CustomEvent.new(CustomEvent::EVENT_TYPE_LEVEL_STARTED)) if handle_event
         | 
| 65 | 
            +
                  end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                  def handle_level_complete_event
         | 
| 68 | 
            +
                    handle_event = level_complete_event_flag
         | 
| 69 | 
            +
                    self.level_complete_event_flag = false
         | 
| 70 | 
            +
                    handle_event_loop(CustomEvent.new(CustomEvent::EVENT_TYPE_LEVEL_COMPLETE)) if handle_event
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
              end
         | 
| 74 | 
            +
            end
         |