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
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "color_scheme"
|
4
|
+
|
5
|
+
module Fet
|
6
|
+
module Ui
|
7
|
+
# Handles setting up the game before starting
|
8
|
+
module GameSetupHelper
|
9
|
+
private
|
10
|
+
|
11
|
+
def setup_window
|
12
|
+
setup_window_title
|
13
|
+
setup_window_background
|
14
|
+
setup_window_event_loop
|
15
|
+
setup_window_update_loop
|
16
|
+
end
|
17
|
+
|
18
|
+
def setup_window_title
|
19
|
+
Ruby2D::Window.set(title: "FET")
|
20
|
+
end
|
21
|
+
|
22
|
+
def setup_window_background
|
23
|
+
Ruby2D::Window.set(background: Fet::Ui::ColorScheme::BLACK)
|
24
|
+
end
|
25
|
+
|
26
|
+
# NOTE: don't test coverage for these methods because this is more of a test of the Ruby2D library
|
27
|
+
# :nocov:
|
28
|
+
def setup_window_event_loop
|
29
|
+
Ruby2D::Window.on(:key_down) do |event|
|
30
|
+
handle_event_loop(event)
|
31
|
+
end
|
32
|
+
|
33
|
+
Ruby2D::Window.on(:mouse_down) do |event|
|
34
|
+
handle_event_loop(event)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
# :nocov:
|
38
|
+
|
39
|
+
# NOTE: don't test coverage for these methods because this is more of a test of the Ruby2D library
|
40
|
+
# :nocov:
|
41
|
+
def setup_window_update_loop
|
42
|
+
Ruby2D::Window.update do
|
43
|
+
handle_update_loop
|
44
|
+
end
|
45
|
+
end
|
46
|
+
# :nocov:
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/fet/ui/key.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fet
|
4
|
+
module Ui
|
5
|
+
# Shows the current level's key in the UI
|
6
|
+
class Key
|
7
|
+
attr_accessor :level
|
8
|
+
|
9
|
+
def initialize(level)
|
10
|
+
self.level = level
|
11
|
+
end
|
12
|
+
|
13
|
+
def start
|
14
|
+
self.text ||= generate_text
|
15
|
+
text.text = text_value
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_accessor :text
|
21
|
+
|
22
|
+
TEXT_SIZE = 36
|
23
|
+
X_OFFSET = 60
|
24
|
+
Y_OFFSET = 90
|
25
|
+
private_constant :TEXT_SIZE, :X_OFFSET, :Y_OFFSET
|
26
|
+
|
27
|
+
def generate_text
|
28
|
+
return Ruby2D::Text.new(
|
29
|
+
text_value,
|
30
|
+
x: X_OFFSET, y: Y_OFFSET,
|
31
|
+
font: File.join(Fet.root, "assets/fonts/PTSans/PTSans-Regular.ttf"),
|
32
|
+
size: TEXT_SIZE,
|
33
|
+
color: ColorScheme::WHITE,
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
def text_value
|
38
|
+
key_name = level.degrees.root_name
|
39
|
+
key_type = level.game.key_type == "major" ? "Major" : "Minor"
|
40
|
+
return "#{key_name} #{key_type}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/fet/ui/level.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "custom_event"
|
4
|
+
require_relative "key"
|
5
|
+
require_relative "level_loop_handler"
|
6
|
+
require_relative "note_boxes"
|
7
|
+
|
8
|
+
module Fet
|
9
|
+
module Ui
|
10
|
+
# Holds state for the current level of the game
|
11
|
+
class Level
|
12
|
+
attr_accessor :game, :question_number, :note_boxes, :key, :degrees
|
13
|
+
|
14
|
+
include LevelLoopHandler
|
15
|
+
|
16
|
+
def initialize(game)
|
17
|
+
self.game = game
|
18
|
+
self.question_number = 0
|
19
|
+
self.note_boxes = NoteBoxes.new(self)
|
20
|
+
self.key = Key.new(self)
|
21
|
+
end
|
22
|
+
|
23
|
+
def start
|
24
|
+
start_self
|
25
|
+
note_boxes.start
|
26
|
+
key.start
|
27
|
+
|
28
|
+
# TODO: This is not ideal because we're piggybacking on the normal events at the moment,
|
29
|
+
# but there's no event here to piggyback on, so the handler has to be called manually.
|
30
|
+
game.set_level_started_event_flag
|
31
|
+
game.handle_event_loop(nil)
|
32
|
+
end
|
33
|
+
|
34
|
+
def degree_indices
|
35
|
+
return midi_values.map { |midi_value| degrees.degree_index_of_midi_value(midi_value) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def over?
|
39
|
+
return degree_indices.all? do |degree_index|
|
40
|
+
note_boxes.note_boxes.any? { |note_box| note_box.selected && note_box.degree_index == degree_index }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def answered_correctly?
|
45
|
+
over? && note_boxes.note_boxes.none? { |note_box| note_box.selected && !note_box.correct? }
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
attr_accessor :midi_values, :full_question_music, :chord_progression_music, :notes_music
|
51
|
+
|
52
|
+
def start_self
|
53
|
+
self.question_number += 1
|
54
|
+
self.degrees = generate_degrees
|
55
|
+
self.midi_values = degrees.select_degrees_from_midi_values(game.note_range, game.number_of_degrees)
|
56
|
+
|
57
|
+
update_music_objects
|
58
|
+
play_full_question
|
59
|
+
end
|
60
|
+
|
61
|
+
def play_full_question
|
62
|
+
full_question_music.play
|
63
|
+
end
|
64
|
+
|
65
|
+
def generate_degrees
|
66
|
+
root_midi_values = game.key_type == "major" ? Fet::MAJOR_ROOT_MIDI_VALUES : Fet::MINOR_ROOT_MIDI_VALUES
|
67
|
+
root_name, root_midi_value = root_midi_values.to_a.sample
|
68
|
+
root_octave_value = Fet::MidiNote.new(root_midi_value).octave_number
|
69
|
+
return Degrees.new(root_name: root_name, octave_value: root_octave_value)
|
70
|
+
end
|
71
|
+
|
72
|
+
def update_music_objects
|
73
|
+
self.full_question_music = generate_full_question_music
|
74
|
+
self.chord_progression_music = generate_chord_progression_music
|
75
|
+
self.notes_music = generate_notes_music
|
76
|
+
end
|
77
|
+
|
78
|
+
def generate_full_question_music
|
79
|
+
filename = "tmp/chord_progression_and_question.mid"
|
80
|
+
create_midilib_object("Chord Progression + Question", filename).create_full_question
|
81
|
+
return Ruby2D::Music.new(filename)
|
82
|
+
end
|
83
|
+
|
84
|
+
def generate_chord_progression_music
|
85
|
+
filename = "tmp/chord_progression.mid"
|
86
|
+
create_midilib_object("Chord Progression", filename).create_chord_progression_of_question
|
87
|
+
return Ruby2D::Music.new(filename)
|
88
|
+
end
|
89
|
+
|
90
|
+
def generate_notes_music
|
91
|
+
filename = "tmp/question.mid"
|
92
|
+
create_midilib_object("Question", filename).create_notes_only
|
93
|
+
return Ruby2D::Music.new(filename)
|
94
|
+
end
|
95
|
+
|
96
|
+
def create_midilib_object(info, filename)
|
97
|
+
progression = Fet::ChordProgression.new(offset: degrees.root_midi_value, template_type: game.key_type).with_offset
|
98
|
+
Fet::MidilibInterface.new(
|
99
|
+
tempo: game.tempo,
|
100
|
+
progression: progression,
|
101
|
+
notes: midi_values,
|
102
|
+
info: info,
|
103
|
+
filename: filename,
|
104
|
+
)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "custom_event"
|
4
|
+
require_relative "key"
|
5
|
+
require_relative "note_boxes"
|
6
|
+
|
7
|
+
module Fet
|
8
|
+
module Ui
|
9
|
+
# Handles events for the Level class
|
10
|
+
module LevelLoopHandler
|
11
|
+
def handle_event_loop(event)
|
12
|
+
handle_keyboard_event(event)
|
13
|
+
handle_level_complete_event(event)
|
14
|
+
note_boxes.handle_event_loop(event)
|
15
|
+
end
|
16
|
+
|
17
|
+
def handle_update_loop; end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def handle_keyboard_event(event)
|
22
|
+
return unless event.is_a?(Ruby2D::Window::KeyEvent)
|
23
|
+
return unless event.type == :down
|
24
|
+
|
25
|
+
handle_c_key if event.key == "c"
|
26
|
+
handle_n_key if event.key == "n"
|
27
|
+
handle_l_key if event.key == "l"
|
28
|
+
handle_return_key if event.key == "return"
|
29
|
+
end
|
30
|
+
|
31
|
+
def handle_c_key
|
32
|
+
chord_progression_music.play
|
33
|
+
end
|
34
|
+
|
35
|
+
def handle_n_key
|
36
|
+
notes_music.play
|
37
|
+
end
|
38
|
+
|
39
|
+
def handle_l_key
|
40
|
+
full_question_music.loop = true
|
41
|
+
full_question_music.play
|
42
|
+
end
|
43
|
+
|
44
|
+
def handle_return_key
|
45
|
+
return unless over?
|
46
|
+
|
47
|
+
start
|
48
|
+
end
|
49
|
+
|
50
|
+
def handle_level_complete_event(event)
|
51
|
+
return unless event.is_a?(CustomEvent) && event.type == CustomEvent::EVENT_TYPE_LEVEL_COMPLETE
|
52
|
+
|
53
|
+
start if answered_correctly? && game.next_on_correct
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "color_scheme"
|
4
|
+
require_relative "note_boxes"
|
5
|
+
require_relative "note_box_loop_handler"
|
6
|
+
|
7
|
+
module Fet
|
8
|
+
module Ui
|
9
|
+
# Handles drawing + events for notes
|
10
|
+
class NoteBox
|
11
|
+
include NoteBoxLoopHandler
|
12
|
+
|
13
|
+
attr_accessor :note_boxes, :degree_name, :selected
|
14
|
+
|
15
|
+
def initialize(note_boxes:, degree_name:)
|
16
|
+
self.note_boxes = note_boxes
|
17
|
+
self.degree_name = degree_name
|
18
|
+
end
|
19
|
+
|
20
|
+
def start
|
21
|
+
self.keyboard_accidental = ""
|
22
|
+
self.square ||= generate_square
|
23
|
+
self.text ||= generate_text
|
24
|
+
self.selected = false
|
25
|
+
self.user_selected = false
|
26
|
+
self.note_music = generate_note_music
|
27
|
+
update_colors
|
28
|
+
end
|
29
|
+
|
30
|
+
def correct?
|
31
|
+
return note_boxes.level.degree_indices.include?(degree_index)
|
32
|
+
end
|
33
|
+
|
34
|
+
def degree_index
|
35
|
+
degree_instance.degree_index
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_accessor :square, :text, :note_music
|
41
|
+
|
42
|
+
NOTE_BOX_SIZE = 70
|
43
|
+
TEXT_SIZE = 36
|
44
|
+
TEXT_X_FOR_NATURAL_OFFSET = 26
|
45
|
+
TEXT_X_FOR_ACCIDENTAL_OFFSET = 16
|
46
|
+
TEXT_Y_OFFSET = 13
|
47
|
+
private_constant :NOTE_BOX_SIZE, :TEXT_SIZE, :TEXT_X_FOR_NATURAL_OFFSET, :TEXT_X_FOR_ACCIDENTAL_OFFSET, :TEXT_Y_OFFSET
|
48
|
+
|
49
|
+
def generate_square
|
50
|
+
return Ruby2D::Square.new(
|
51
|
+
x: NoteBoxes::NOTE_BOX_OFFSETS[degree_name][0],
|
52
|
+
y: NoteBoxes::NOTE_BOX_OFFSETS[degree_name][1],
|
53
|
+
size: NOTE_BOX_SIZE,
|
54
|
+
color: color,
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
def generate_text
|
59
|
+
return Ruby2D::Text.new(
|
60
|
+
degree_name,
|
61
|
+
x: square.x + text_x_offset, y: square.y + text_y_offset,
|
62
|
+
font: File.join(Fet.root, "assets/fonts/PTSans/PTSans-Regular.ttf"),
|
63
|
+
size: TEXT_SIZE,
|
64
|
+
color: text_color,
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
def text_x_offset
|
69
|
+
return degree_instance.degree_accidental ? TEXT_X_FOR_ACCIDENTAL_OFFSET : TEXT_X_FOR_NATURAL_OFFSET
|
70
|
+
end
|
71
|
+
|
72
|
+
def text_y_offset
|
73
|
+
return TEXT_Y_OFFSET
|
74
|
+
end
|
75
|
+
|
76
|
+
def degree_instance
|
77
|
+
return Fet::Degree.new(degree_name)
|
78
|
+
end
|
79
|
+
|
80
|
+
def color
|
81
|
+
return correct? ? ColorScheme::GREEN : ColorScheme::RED if selected
|
82
|
+
|
83
|
+
return degree_instance.degree_accidental ? ColorScheme::GREY : ColorScheme::WHITE
|
84
|
+
end
|
85
|
+
|
86
|
+
def text_color
|
87
|
+
case color
|
88
|
+
when ColorScheme::GREY, ColorScheme::GREEN, ColorScheme::RED
|
89
|
+
return ColorScheme::WHITE
|
90
|
+
when ColorScheme::WHITE
|
91
|
+
return ColorScheme::BLACK
|
92
|
+
else
|
93
|
+
raise ImplementationError
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def level_over?
|
98
|
+
return note_boxes.level.over?
|
99
|
+
end
|
100
|
+
|
101
|
+
def generate_note_music
|
102
|
+
degrees = note_boxes.level.degrees
|
103
|
+
|
104
|
+
filename = "tmp/#{degree_name}.mid"
|
105
|
+
Fet::MidilibInterface.new(
|
106
|
+
tempo: note_boxes.level.game.tempo,
|
107
|
+
progression: nil,
|
108
|
+
notes: [degrees.root_midi_value + degree_instance.degree_index],
|
109
|
+
info: degree_name,
|
110
|
+
filename: filename,
|
111
|
+
).create_notes_only
|
112
|
+
|
113
|
+
return Ruby2D::Music.new(filename)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "color_scheme"
|
4
|
+
require_relative "note_boxes"
|
5
|
+
|
6
|
+
module Fet
|
7
|
+
module Ui
|
8
|
+
# Handles events for NoteBox
|
9
|
+
module NoteBoxLoopHandler
|
10
|
+
attr_accessor :user_selected
|
11
|
+
|
12
|
+
def handle_event_loop(event)
|
13
|
+
handle_click_event(event)
|
14
|
+
handle_keyboard_event(event)
|
15
|
+
end
|
16
|
+
|
17
|
+
def handle_update_loop; end
|
18
|
+
|
19
|
+
def manually_select
|
20
|
+
handle_selected(user_selected: false)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_accessor :keyboard_accidental
|
26
|
+
|
27
|
+
def handle_click_event(event)
|
28
|
+
return unless event.is_a?(Ruby2D::Window::MouseEvent)
|
29
|
+
return unless event.type == :down
|
30
|
+
return unless event.button == :left
|
31
|
+
|
32
|
+
handle_selected if square.contains?(event.x, event.y)
|
33
|
+
end
|
34
|
+
|
35
|
+
def handle_keyboard_event(event)
|
36
|
+
return unless event.is_a?(Ruby2D::Window::KeyEvent)
|
37
|
+
return unless event.type == :down
|
38
|
+
|
39
|
+
input = determine_keyboard_input(event)
|
40
|
+
handle_selected if degree_instance.degree_index == input_degree_instance(input)&.degree_index
|
41
|
+
end
|
42
|
+
|
43
|
+
KEYBOARD_TO_ACCIDENTAL_MAP = {
|
44
|
+
"=" => "#",
|
45
|
+
"0" => "",
|
46
|
+
"-" => "b",
|
47
|
+
}.deep_freeze
|
48
|
+
private_constant :KEYBOARD_TO_ACCIDENTAL_MAP
|
49
|
+
|
50
|
+
def determine_keyboard_input(event)
|
51
|
+
update_keyboard_input_accidental(event)
|
52
|
+
handle_keyboard_input_degree(event)
|
53
|
+
end
|
54
|
+
|
55
|
+
def update_keyboard_input_accidental(event)
|
56
|
+
case event.key
|
57
|
+
when *KEYBOARD_TO_ACCIDENTAL_MAP.keys
|
58
|
+
self.keyboard_accidental = KEYBOARD_TO_ACCIDENTAL_MAP[event.key]
|
59
|
+
when *(1..7).map(&:to_s)
|
60
|
+
# noop
|
61
|
+
else
|
62
|
+
self.keyboard_accidental = ""
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def handle_keyboard_input_degree(event)
|
67
|
+
case event.key
|
68
|
+
when *(1..7).map(&:to_s)
|
69
|
+
input = "#{keyboard_accidental}#{event.key}"
|
70
|
+
self.keyboard_accidental = ""
|
71
|
+
return input
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def input_degree_instance(input)
|
76
|
+
return nil if input.nil?
|
77
|
+
|
78
|
+
return Fet::Degree.new(input)
|
79
|
+
rescue Fet::InvalidDegreeName
|
80
|
+
# TODO: alterenatively, make degrees like b1 or #1 valid degree names, though that would need a bit of a refactor
|
81
|
+
return nil
|
82
|
+
end
|
83
|
+
|
84
|
+
def handle_selected(user_selected: true)
|
85
|
+
self.keyboard_accidental = ""
|
86
|
+
|
87
|
+
if level_over?
|
88
|
+
play_note
|
89
|
+
else
|
90
|
+
self.user_selected = user_selected
|
91
|
+
self.selected = true
|
92
|
+
update_colors
|
93
|
+
note_boxes.level.game.set_note_selected_event_flag
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def play_note
|
98
|
+
note_music.play
|
99
|
+
end
|
100
|
+
|
101
|
+
def update_colors
|
102
|
+
square.color = color
|
103
|
+
text.color = text_color
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|