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.
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:, directory_prefix: "")
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, :directory_prefix
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:, directory_prefix: "")
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, :directory_prefix
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:, directory_prefix: "")
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, :directory_prefix
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
- self.score = initialize_score
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
- SCORES_FILENAME = "#{ENV["HOME"]}/.config/fet/scores".deep_freeze
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
- 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:)
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.score = Score.new(self)
36
- self.level = Level.new(self)
37
- self.timer = Timer.new(self)
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
- write_score_to_file
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 show_window
56
- Ruby2D::Window.show
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 close_window
60
- Ruby2D::Window.close
54
+ def validate!
55
+ validate_degrees!
61
56
  end
62
57
 
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) }
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 historic_score_entries
72
- result = File.read(self.class.scores_filename)
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 current_score_entry
79
- return {
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 = "tmp/chord_progression_and_question.mid"
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 = "tmp/chord_progression.mid"
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 = "tmp/question.mid"
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
 
@@ -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: filename,
107
+ filename: midi_filename,
111
108
  ).create_notes_only
112
109
 
113
- return Ruby2D::Music.new(filename)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fet
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.1"
5
5
  end
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.0
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-06 00:00:00.000000000 Z
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
- - ".ruby_version"
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