fet 0.3.0 → 0.3.1
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/.rubocop.yml +3 -0
- data/{.ruby_version → .ruby-version} +0 -0
- data/CHANGELOG.md +9 -1
- data/Gemfile.lock +4 -1
- data/README.md +15 -2
- data/bin/fet +5 -88
- data/fet.gemspec +1 -0
- data/fet.rdoc +57 -2
- data/lib/fet/cli/generate/listening.rb +0 -1
- data/lib/fet/cli/generate/listening_command.rb +72 -0
- data/lib/fet/cli/generate/singing.rb +0 -1
- data/lib/fet/cli/generate/singing_command.rb +54 -0
- data/lib/fet/cli/generate/single_note_listening.rb +0 -1
- data/lib/fet/cli/generate/single_note_listening_command.rb +46 -0
- data/lib/fet/cli/play/listening.rb +1 -0
- data/lib/fet/cli/play/listening_command.rb +76 -0
- data/lib/fet/cli/score/summary.rb +20 -0
- data/lib/fet/cli/score/summary_command.rb +74 -0
- data/lib/fet/degrees.rb +12 -2
- data/lib/fet/generator/listening.rb +11 -5
- data/lib/fet/generator/singing.rb +10 -4
- data/lib/fet/generator/single_note_listening.rb +10 -4
- data/lib/fet/score.rb +33 -2
- data/lib/fet/score_summary.rb +133 -0
- data/lib/fet/score_summary_writer.rb +51 -0
- data/lib/fet/ui/game.rb +23 -37
- data/lib/fet/ui/level.rb +4 -4
- data/lib/fet/ui/level_loop_handler.rb +4 -0
- data/lib/fet/ui/note_box.rb +7 -6
- data/lib/fet/ui/score.rb +4 -0
- data/lib/fet/version.rb +1 -1
- metadata +31 -3
data/lib/fet/degrees.rb
CHANGED
@@ -31,8 +31,8 @@ module Fet
|
|
31
31
|
return degree_to_note_name[Degree.new(degree_name).degree_name]
|
32
32
|
end
|
33
33
|
|
34
|
-
def select_degrees_from_midi_values(midi_value_range, number_of_degrees)
|
35
|
-
return select_degrees_from_midi_values_recursive(midi_value_range, [], number_of_degrees)
|
34
|
+
def select_degrees_from_midi_values(midi_value_range, number_of_degrees, degrees_filter)
|
35
|
+
return select_degrees_from_midi_values_recursive(filtered_midi_values(midi_value_range, degrees_filter), [], number_of_degrees)
|
36
36
|
end
|
37
37
|
|
38
38
|
private
|
@@ -42,6 +42,16 @@ module Fet
|
|
42
42
|
|
43
43
|
attr_accessor :degree_to_note_name
|
44
44
|
|
45
|
+
def filtered_midi_values(midi_value_range, degrees_filter)
|
46
|
+
return midi_value_range if degrees_filter.empty?
|
47
|
+
|
48
|
+
filter_degree_indices = degrees_filter.map { |degree_name| Degree.new(degree_name).degree_index }
|
49
|
+
return midi_value_range.select do |midi_value|
|
50
|
+
degree_index = degree_index_of_midi_value(midi_value)
|
51
|
+
filter_degree_indices.include?(degree_index)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
45
55
|
def note_name_of_degree_internal(degree_name)
|
46
56
|
degree = Degree.new(degree_name)
|
47
57
|
|
@@ -9,22 +9,28 @@ module Fet
|
|
9
9
|
# (i.e. if you chose the b2 degree, you exclude the rest of the b2 degrees too)
|
10
10
|
# 2 degrees => 21592 (left it for a while and it seemed to stop at this value) - comparable to 64C2 * 12 = 24192
|
11
11
|
# 3 degrees => 252398 (before I stopped it, there was more being generated)
|
12
|
-
def initialize(exercises:, tempo:, degrees:, all_single_degree
|
12
|
+
def initialize(exercises:, tempo:, degrees:, all_single_degree:)
|
13
13
|
self.number_of_exercises = exercises
|
14
14
|
self.all_single_degree = all_single_degree
|
15
15
|
self.tempo = tempo
|
16
16
|
self.number_of_degrees = degrees
|
17
17
|
self.note_range = Fet::REDUCED_BY_OCTAVE_PIANO_RANGE
|
18
|
-
self.directory_prefix = directory_prefix
|
19
18
|
end
|
20
19
|
|
20
|
+
# NOTE: this is explicitly changed in tests, so no need to check for coverage
|
21
|
+
# :nocov:
|
22
|
+
def self.directory_prefix
|
23
|
+
return ""
|
24
|
+
end
|
25
|
+
# :nocov:
|
26
|
+
|
21
27
|
def generate
|
22
28
|
all_single_degree ? generate_all_single_degree_exercises : generate_number_of_exercises
|
23
29
|
end
|
24
30
|
|
25
31
|
private
|
26
32
|
|
27
|
-
attr_accessor :number_of_exercises, :tempo, :number_of_degrees, :note_range, :all_single_degree
|
33
|
+
attr_accessor :number_of_exercises, :tempo, :number_of_degrees, :note_range, :all_single_degree
|
28
34
|
|
29
35
|
def generate_all_single_degree_exercises
|
30
36
|
Fet::MAJOR_ROOT_MIDI_VALUES.each do |root|
|
@@ -55,7 +61,7 @@ module Fet
|
|
55
61
|
def select_notes(all_notes, root_name, root_midi_value, number_degrees, key_type)
|
56
62
|
root_octave_value = Fet::MidiNote.new(root_midi_value).octave_number
|
57
63
|
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)
|
64
|
+
chosen_notes = degrees_instance.select_degrees_from_midi_values(all_notes, number_degrees, [])
|
59
65
|
return create_midi_file(chosen_notes, root_name, root_midi_value, key_type)
|
60
66
|
end
|
61
67
|
|
@@ -82,7 +88,7 @@ module Fet
|
|
82
88
|
end
|
83
89
|
|
84
90
|
def full_filename(key_type, root_name, root_midi_value, chosen_notes)
|
85
|
-
result = File.join(*[directory_prefix, "listening", key_type].reject(&:empty?))
|
91
|
+
result = File.join(*[self.class.directory_prefix, "listening", key_type].reject(&:empty?))
|
86
92
|
filename = root_name # note, e.g. Db
|
87
93
|
filename += key_type == "major" ? "M" : "m" # type of note, M or m
|
88
94
|
filename += "_" # delimiter
|
@@ -4,13 +4,19 @@ module Fet
|
|
4
4
|
module Generator
|
5
5
|
# Class that generates MIDI files for the singing exercises
|
6
6
|
class Singing
|
7
|
-
def initialize(tempo:, pause
|
7
|
+
def initialize(tempo:, pause:)
|
8
8
|
self.tempo = tempo
|
9
9
|
self.pause = pause
|
10
10
|
self.midi_range = HIGH_SINGING_OCTAVE_RANGE
|
11
|
-
self.directory_prefix = directory_prefix
|
12
11
|
end
|
13
12
|
|
13
|
+
# NOTE: this is explicitly changed in tests, so no need to check for coverage
|
14
|
+
# :nocov:
|
15
|
+
def self.directory_prefix
|
16
|
+
return ""
|
17
|
+
end
|
18
|
+
# :nocov:
|
19
|
+
|
14
20
|
def generate
|
15
21
|
generate_major
|
16
22
|
generate_minor
|
@@ -18,7 +24,7 @@ module Fet
|
|
18
24
|
|
19
25
|
private
|
20
26
|
|
21
|
-
attr_accessor :tempo, :pause, :midi_range
|
27
|
+
attr_accessor :tempo, :pause, :midi_range
|
22
28
|
|
23
29
|
def generate_major
|
24
30
|
MusicTheory::MAJOR_KEYS.each do |root_note_name|
|
@@ -59,7 +65,7 @@ module Fet
|
|
59
65
|
end
|
60
66
|
|
61
67
|
def full_filename(key_type, root_note_name, root_midi_value, note_midi_value)
|
62
|
-
result = File.join(*[directory_prefix, "singing", key_type].reject(&:empty?))
|
68
|
+
result = File.join(*[self.class.directory_prefix, "singing", key_type].reject(&:empty?))
|
63
69
|
filename = root_note_name # note, e.g. Db
|
64
70
|
filename += key_type == "major" ? "M" : "m" # type of note, M or m
|
65
71
|
filename += "_" # delimiter
|
@@ -4,14 +4,20 @@ module Fet
|
|
4
4
|
module Generator
|
5
5
|
# Class that generates MIDI files for the single note listening exercises
|
6
6
|
class SingleNoteListening
|
7
|
-
def initialize(tempo
|
7
|
+
def initialize(tempo:)
|
8
8
|
self.tempo = tempo
|
9
9
|
self.note = Fet::Note.new("C")
|
10
10
|
self.octave_value = 4
|
11
11
|
self.midi_value = MidiNote.from_note(note.full_note, octave_value).midi_value
|
12
|
-
self.directory_prefix = directory_prefix
|
13
12
|
end
|
14
13
|
|
14
|
+
# NOTE: this is explicitly changed in tests, so no need to check for coverage
|
15
|
+
# :nocov:
|
16
|
+
def self.directory_prefix
|
17
|
+
return ""
|
18
|
+
end
|
19
|
+
# :nocov:
|
20
|
+
|
15
21
|
def generate
|
16
22
|
MusicTheory::MAJOR_KEYS.each do |root_note_name|
|
17
23
|
root_midi_value = MAJOR_ROOT_MIDI_VALUES[root_note_name]
|
@@ -26,7 +32,7 @@ module Fet
|
|
26
32
|
|
27
33
|
private
|
28
34
|
|
29
|
-
attr_accessor :tempo, :note, :octave_value, :midi_value
|
35
|
+
attr_accessor :tempo, :note, :octave_value, :midi_value
|
30
36
|
|
31
37
|
def create_midi_file(key_type, root_note_name, root_midi_value)
|
32
38
|
progression = Fet::ChordProgression.new(offset: root_midi_value, template_type: key_type).with_offset
|
@@ -47,7 +53,7 @@ module Fet
|
|
47
53
|
end
|
48
54
|
|
49
55
|
def full_filename(key_type, root_note_name, root_midi_value)
|
50
|
-
result = File.join(*[directory_prefix, "listening_single_note", key_type].reject(&:empty?))
|
56
|
+
result = File.join(*[self.class.directory_prefix, "listening_single_note", key_type].reject(&:empty?))
|
51
57
|
filename = root_note_name # note, e.g. Db
|
52
58
|
filename += key_type == "major" ? "M" : "m" # type of note, M or m
|
53
59
|
filename += "_" # delimiter
|
data/lib/fet/score.rb
CHANGED
@@ -5,8 +5,16 @@ require "json"
|
|
5
5
|
module Fet
|
6
6
|
# Holds the correct/incorrect answers to questions
|
7
7
|
class Score
|
8
|
-
def initialize
|
9
|
-
|
8
|
+
def initialize(score: nil)
|
9
|
+
score_hash = score || initialize_score
|
10
|
+
score_hash = score_hash.map do |k, v|
|
11
|
+
[k.to_i, v.transform_keys(&:to_sym)]
|
12
|
+
end.to_h
|
13
|
+
self.score = score_hash
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.merge(*scores)
|
17
|
+
scores.each_with_object(Fet::Score.new) { |x, res| res.merge(x) }
|
10
18
|
end
|
11
19
|
|
12
20
|
def answer_correctly(*degree_indices)
|
@@ -41,6 +49,23 @@ module Fet
|
|
41
49
|
as_json(*options).to_json(*options)
|
42
50
|
end
|
43
51
|
|
52
|
+
def merge(other)
|
53
|
+
score.each do |k, v|
|
54
|
+
v[:correct] += other.answered_correctly(k)
|
55
|
+
v[:incorrect] += other.answered_incorrectly(k)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def percentages
|
60
|
+
score.map do |k, _|
|
61
|
+
next([k, percentage(answered_correctly(k), questions_asked(k)).to_i])
|
62
|
+
end.to_h
|
63
|
+
end
|
64
|
+
|
65
|
+
def total_percentage
|
66
|
+
return percentage(answered_correctly, questions_asked).to_i
|
67
|
+
end
|
68
|
+
|
44
69
|
private
|
45
70
|
|
46
71
|
attr_accessor :score
|
@@ -48,5 +73,11 @@ module Fet
|
|
48
73
|
def initialize_score
|
49
74
|
Fet::Degree::DEGREE_NAMES.map.with_index { |_, degree_index| [degree_index, { correct: 0, incorrect: 0 }] }.to_h
|
50
75
|
end
|
76
|
+
|
77
|
+
def percentage(correct, total)
|
78
|
+
return 0.0 if total.zero?
|
79
|
+
|
80
|
+
return correct.fdiv(total) * 100
|
81
|
+
end
|
51
82
|
end
|
52
83
|
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
require "json"
|
5
|
+
require "terminal-table"
|
6
|
+
require_relative "score_summary_writer"
|
7
|
+
|
8
|
+
module Fet
|
9
|
+
# Responsible for writing + showing the score summary to the user
|
10
|
+
class ScoreSummary
|
11
|
+
extend ScoreSummaryWriter
|
12
|
+
|
13
|
+
NO_SCORES_MESSAGE = "No scores available yet!".deep_freeze
|
14
|
+
|
15
|
+
def initialize(minimum_session_length: 0, number_of_degrees: nil, key_type: nil, begin_offset: 0, end_offset: 0)
|
16
|
+
self.minimum_session_length = minimum_session_length
|
17
|
+
self.number_of_degrees = number_of_degrees
|
18
|
+
self.key_type = key_type
|
19
|
+
self.begin_offset = begin_offset
|
20
|
+
self.end_offset = end_offset
|
21
|
+
end
|
22
|
+
|
23
|
+
def summary
|
24
|
+
if file_exists?
|
25
|
+
percentage_summary
|
26
|
+
else
|
27
|
+
puts NO_SCORES_MESSAGE
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_accessor :minimum_session_length, :number_of_degrees, :key_type, :begin_offset, :end_offset
|
34
|
+
|
35
|
+
def filename
|
36
|
+
return self.class.scores_filename
|
37
|
+
end
|
38
|
+
|
39
|
+
def file_exists?
|
40
|
+
return File.exist?(filename)
|
41
|
+
end
|
42
|
+
|
43
|
+
def file_contents
|
44
|
+
return File.read(filename)
|
45
|
+
end
|
46
|
+
|
47
|
+
def games_array
|
48
|
+
return JSON.parse(file_contents)
|
49
|
+
end
|
50
|
+
|
51
|
+
def games_array_with_constraints
|
52
|
+
result = games_array
|
53
|
+
result = filter_by_seconds_elapsed(result)
|
54
|
+
result = filter_by_number_of_degrees(result)
|
55
|
+
result = filter_by_key_type(result)
|
56
|
+
result = filter_by_offsets(result)
|
57
|
+
return result
|
58
|
+
end
|
59
|
+
|
60
|
+
def filter_by_seconds_elapsed(games_array)
|
61
|
+
return games_array.select { |game_details| game_details["seconds_elapsed"] >= minimum_session_length }
|
62
|
+
end
|
63
|
+
|
64
|
+
def filter_by_number_of_degrees(games_array)
|
65
|
+
return games_array if number_of_degrees.nil?
|
66
|
+
|
67
|
+
return games_array.select { |game_details| game_details["number_of_degrees"] == number_of_degrees }
|
68
|
+
end
|
69
|
+
|
70
|
+
def filter_by_key_type(games_array)
|
71
|
+
return games_array if key_type.nil?
|
72
|
+
|
73
|
+
return games_array.select { |game_details| game_details["key_type"] == key_type }
|
74
|
+
end
|
75
|
+
|
76
|
+
def filter_by_offsets(games_array)
|
77
|
+
begin_index = 0
|
78
|
+
end_index = games_array.size
|
79
|
+
return games_array[(begin_index + begin_offset)...(end_index + end_offset)] || []
|
80
|
+
end
|
81
|
+
|
82
|
+
def score_instances
|
83
|
+
return games_array_with_constraints.map { |game_details| Fet::Score.new(score: game_details["score"]) }
|
84
|
+
end
|
85
|
+
|
86
|
+
def merged_score_instance
|
87
|
+
return Fet::Score.merge(*score_instances)
|
88
|
+
end
|
89
|
+
|
90
|
+
def generate_table(score_instance)
|
91
|
+
Terminal::Table.new do |t|
|
92
|
+
generate_table_header(t)
|
93
|
+
t.add_separator
|
94
|
+
generate_table_main(t, score_instance)
|
95
|
+
t.add_separator
|
96
|
+
generate_table_footer(t, score_instance)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def generate_table_header(table)
|
101
|
+
table.add_row(["Degree", "Answered Correctly", "Total Answered", "Percentage"])
|
102
|
+
end
|
103
|
+
|
104
|
+
def generate_table_main(table, score_instance)
|
105
|
+
score_instance.percentages.each do |degree_index, percentage|
|
106
|
+
table.add_row(
|
107
|
+
[
|
108
|
+
Fet::Degree::DEGREE_NAMES[degree_index].last,
|
109
|
+
score_instance.answered_correctly(degree_index),
|
110
|
+
score_instance.questions_asked(degree_index),
|
111
|
+
"#{percentage}%",
|
112
|
+
],
|
113
|
+
)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def generate_table_footer(table, score_instance)
|
118
|
+
table.add_row(
|
119
|
+
[
|
120
|
+
"All",
|
121
|
+
score_instance.answered_correctly,
|
122
|
+
score_instance.questions_asked,
|
123
|
+
"#{score_instance.total_percentage}%",
|
124
|
+
],
|
125
|
+
)
|
126
|
+
end
|
127
|
+
|
128
|
+
def percentage_summary
|
129
|
+
table = generate_table(merged_score_instance)
|
130
|
+
puts table
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
require "json"
|
5
|
+
require "terminal-table"
|
6
|
+
|
7
|
+
module Fet
|
8
|
+
# Methods for writing game score to file
|
9
|
+
module ScoreSummaryWriter
|
10
|
+
SCORES_FILENAME = "#{ENV["HOME"]}/.config/fet/scores".deep_freeze
|
11
|
+
|
12
|
+
# NOTE: this is explicitly changed in tests, so no need to check for coverage
|
13
|
+
# :nocov:
|
14
|
+
def scores_filename
|
15
|
+
return SCORES_FILENAME
|
16
|
+
end
|
17
|
+
# :nocov:
|
18
|
+
|
19
|
+
def add_entry(game)
|
20
|
+
write_score_to_file(game)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def write_score_to_file(game)
|
26
|
+
new_score_entries = historic_score_entries
|
27
|
+
new_score_entries << current_score_entry(game)
|
28
|
+
directory_name = File.dirname(scores_filename)
|
29
|
+
FileUtils.mkdir_p(directory_name)
|
30
|
+
File.open(scores_filename, "w") { |file| file.write(new_score_entries.to_json) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def historic_score_entries
|
34
|
+
result = File.read(scores_filename)
|
35
|
+
return JSON.parse(result)
|
36
|
+
rescue Errno::ENOENT
|
37
|
+
return []
|
38
|
+
end
|
39
|
+
|
40
|
+
def current_score_entry(game)
|
41
|
+
return {
|
42
|
+
"started_at" => game.timer.started_at.to_s,
|
43
|
+
"seconds_elapsed" => game.timer.seconds_elapsed,
|
44
|
+
"number_of_degrees" => game.number_of_degrees,
|
45
|
+
"tempo" => game.tempo,
|
46
|
+
"key_type" => game.key_type,
|
47
|
+
"score" => game.score.score,
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/fet/ui/game.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "ruby2d"
|
4
|
+
require "tmpdir"
|
4
5
|
require_relative "game_loop_handler"
|
5
6
|
require_relative "game_setup_helper"
|
6
7
|
require_relative "level"
|
@@ -14,27 +15,19 @@ module Fet
|
|
14
15
|
include GameSetupHelper
|
15
16
|
include GameLoopHandler
|
16
17
|
|
17
|
-
|
18
|
+
attr_accessor :level, :score, :timer, :note_range, :tmp_directory,
|
19
|
+
:tempo, :number_of_degrees, :key_type, :next_on_correct, :limit_degrees
|
18
20
|
|
19
|
-
|
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:)
|
21
|
+
def initialize(tempo:, degrees:, key_type:, next_on_correct:, limit_degrees: [])
|
30
22
|
self.note_range = Fet::REDUCED_BY_OCTAVE_PIANO_RANGE
|
31
23
|
self.tempo = tempo
|
32
24
|
self.key_type = key_type
|
33
25
|
self.number_of_degrees = degrees
|
34
26
|
self.next_on_correct = next_on_correct
|
35
|
-
self.
|
36
|
-
self.
|
37
|
-
|
27
|
+
self.limit_degrees = limit_degrees
|
28
|
+
self.tmp_directory = Dir.mktmpdir
|
29
|
+
initialize_ui_objects
|
30
|
+
validate!
|
38
31
|
setup_window
|
39
32
|
end
|
40
33
|
|
@@ -43,7 +36,7 @@ module Fet
|
|
43
36
|
level.start
|
44
37
|
timer.start
|
45
38
|
show_window
|
46
|
-
|
39
|
+
Fet::ScoreSummary.add_entry(self)
|
47
40
|
end
|
48
41
|
|
49
42
|
def stop
|
@@ -52,35 +45,28 @@ module Fet
|
|
52
45
|
|
53
46
|
private
|
54
47
|
|
55
|
-
def
|
56
|
-
|
48
|
+
def initialize_ui_objects
|
49
|
+
self.score = Score.new(self)
|
50
|
+
self.level = Level.new(self)
|
51
|
+
self.timer = Timer.new(self)
|
57
52
|
end
|
58
53
|
|
59
|
-
def
|
60
|
-
|
54
|
+
def validate!
|
55
|
+
validate_degrees!
|
61
56
|
end
|
62
57
|
|
63
|
-
def
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
FileUtils.mkdir_p(directory_name)
|
68
|
-
File.open(self.class.scores_filename, "w") { |file| file.write(new_score_entries.to_json) }
|
58
|
+
def validate_degrees!
|
59
|
+
return unless !limit_degrees.empty? && number_of_degrees > limit_degrees.uniq.size
|
60
|
+
|
61
|
+
raise ArgumentError.new("Can not select #{number_of_degrees} degrees from set of #{limit_degrees}")
|
69
62
|
end
|
70
63
|
|
71
|
-
def
|
72
|
-
|
73
|
-
return JSON.parse(result)
|
74
|
-
rescue Errno::ENOENT
|
75
|
-
return []
|
64
|
+
def show_window
|
65
|
+
Ruby2D::Window.show
|
76
66
|
end
|
77
67
|
|
78
|
-
def
|
79
|
-
|
80
|
-
"started_at" => timer.started_at.to_s,
|
81
|
-
"seconds_elapsed" => timer.seconds_elapsed,
|
82
|
-
"score" => score.score,
|
83
|
-
}
|
68
|
+
def close_window
|
69
|
+
Ruby2D::Window.close
|
84
70
|
end
|
85
71
|
end
|
86
72
|
end
|
data/lib/fet/ui/level.rb
CHANGED
@@ -52,7 +52,7 @@ module Fet
|
|
52
52
|
def start_self
|
53
53
|
self.question_number += 1
|
54
54
|
self.degrees = generate_degrees
|
55
|
-
self.midi_values = degrees.select_degrees_from_midi_values(game.note_range, game.number_of_degrees)
|
55
|
+
self.midi_values = degrees.select_degrees_from_midi_values(game.note_range, game.number_of_degrees, game.limit_degrees)
|
56
56
|
|
57
57
|
update_music_objects
|
58
58
|
play_full_question
|
@@ -76,19 +76,19 @@ module Fet
|
|
76
76
|
end
|
77
77
|
|
78
78
|
def generate_full_question_music
|
79
|
-
filename = "
|
79
|
+
filename = File.join(game.tmp_directory, "chord_progression_and_question.mid")
|
80
80
|
create_midilib_object("Chord Progression + Question", filename).create_full_question
|
81
81
|
return Ruby2D::Music.new(filename)
|
82
82
|
end
|
83
83
|
|
84
84
|
def generate_chord_progression_music
|
85
|
-
filename = "
|
85
|
+
filename = File.join(game.tmp_directory, "chord_progression.mid")
|
86
86
|
create_midilib_object("Chord Progression", filename).create_chord_progression_of_question
|
87
87
|
return Ruby2D::Music.new(filename)
|
88
88
|
end
|
89
89
|
|
90
90
|
def generate_notes_music
|
91
|
-
filename = "
|
91
|
+
filename = File.join(game.tmp_directory, "question.mid")
|
92
92
|
create_midilib_object("Question", filename).create_notes_only
|
93
93
|
return Ruby2D::Music.new(filename)
|
94
94
|
end
|
@@ -47,6 +47,10 @@ module Fet
|
|
47
47
|
start
|
48
48
|
end
|
49
49
|
|
50
|
+
# NOET: It seems that until the event loop finishes, the window updates don't happen.
|
51
|
+
# Specifically here, selecting the correct answer will set color on a note box, but that change
|
52
|
+
# does not take effect until we're out of the event loop, but by then we've already started a
|
53
|
+
# new level, so the color change never shows.
|
50
54
|
def handle_level_complete_event(event)
|
51
55
|
return unless event.is_a?(CustomEvent) && event.type == CustomEvent::EVENT_TYPE_LEVEL_COMPLETE
|
52
56
|
|
data/lib/fet/ui/note_box.rb
CHANGED
@@ -99,18 +99,19 @@ module Fet
|
|
99
99
|
end
|
100
100
|
|
101
101
|
def generate_note_music
|
102
|
-
degrees = note_boxes.level.degrees
|
103
|
-
|
104
|
-
filename = "tmp/#{degree_name}.mid"
|
105
102
|
Fet::MidilibInterface.new(
|
106
103
|
tempo: note_boxes.level.game.tempo,
|
107
104
|
progression: nil,
|
108
|
-
notes: [degrees.root_midi_value + degree_instance.degree_index],
|
105
|
+
notes: [note_boxes.level.degrees.root_midi_value + degree_instance.degree_index],
|
109
106
|
info: degree_name,
|
110
|
-
filename:
|
107
|
+
filename: midi_filename,
|
111
108
|
).create_notes_only
|
112
109
|
|
113
|
-
return Ruby2D::Music.new(
|
110
|
+
return Ruby2D::Music.new(midi_filename)
|
111
|
+
end
|
112
|
+
|
113
|
+
def midi_filename
|
114
|
+
return File.join(note_boxes.level.game.tmp_directory, "#{degree_name}.mid")
|
114
115
|
end
|
115
116
|
end
|
116
117
|
end
|
data/lib/fet/ui/score.rb
CHANGED
@@ -35,6 +35,7 @@ module Fet
|
|
35
35
|
TEXT_SIZE = 36
|
36
36
|
X_OFFSET = 508
|
37
37
|
Y_OFFSET = 90
|
38
|
+
X_CORRECTION = 20
|
38
39
|
private_constant :TEXT_SIZE, :X_OFFSET, :Y_OFFSET
|
39
40
|
|
40
41
|
def text_value
|
@@ -79,6 +80,9 @@ module Fet
|
|
79
80
|
|
80
81
|
def update_text
|
81
82
|
text.text = text_value
|
83
|
+
# NOTE: keep "/" character in the same position as score increases
|
84
|
+
characters = text_value.split("/")[0].size
|
85
|
+
text.x = X_OFFSET - (characters - 1) * X_CORRECTION
|
82
86
|
end
|
83
87
|
end
|
84
88
|
end
|
data/lib/fet/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dimitrios Lisenko
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-09-
|
11
|
+
date: 2021-09-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: gli
|
@@ -78,6 +78,26 @@ dependencies:
|
|
78
78
|
- - "~>"
|
79
79
|
- !ruby/object:Gem::Version
|
80
80
|
version: 0.10.0
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: terminal-table
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - "~>"
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '3.0'
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 3.0.1
|
91
|
+
type: :runtime
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '3.0'
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: 3.0.1
|
81
101
|
description: Teaches your ear to recognize notes based on their function in a key.
|
82
102
|
email:
|
83
103
|
- dimitrioslisenko@gmail.com
|
@@ -91,7 +111,7 @@ files:
|
|
91
111
|
- ".github/workflows/main.yml"
|
92
112
|
- ".gitignore"
|
93
113
|
- ".rubocop.yml"
|
94
|
-
- ".
|
114
|
+
- ".ruby-version"
|
95
115
|
- CHANGELOG.md
|
96
116
|
- Gemfile
|
97
117
|
- Gemfile.lock
|
@@ -116,9 +136,15 @@ files:
|
|
116
136
|
- lib/fet.rb
|
117
137
|
- lib/fet/chord_progression.rb
|
118
138
|
- lib/fet/cli/generate/listening.rb
|
139
|
+
- lib/fet/cli/generate/listening_command.rb
|
119
140
|
- lib/fet/cli/generate/singing.rb
|
141
|
+
- lib/fet/cli/generate/singing_command.rb
|
120
142
|
- lib/fet/cli/generate/single_note_listening.rb
|
143
|
+
- lib/fet/cli/generate/single_note_listening_command.rb
|
121
144
|
- lib/fet/cli/play/listening.rb
|
145
|
+
- lib/fet/cli/play/listening_command.rb
|
146
|
+
- lib/fet/cli/score/summary.rb
|
147
|
+
- lib/fet/cli/score/summary_command.rb
|
122
148
|
- lib/fet/degree.rb
|
123
149
|
- lib/fet/degrees.rb
|
124
150
|
- lib/fet/exceptions.rb
|
@@ -134,6 +160,8 @@ files:
|
|
134
160
|
- lib/fet/note.rb
|
135
161
|
- lib/fet/note_validations.rb
|
136
162
|
- lib/fet/score.rb
|
163
|
+
- lib/fet/score_summary.rb
|
164
|
+
- lib/fet/score_summary_writer.rb
|
137
165
|
- lib/fet/ui/color_scheme.rb
|
138
166
|
- lib/fet/ui/custom_event.rb
|
139
167
|
- lib/fet/ui/game.rb
|