diff --git a/.circleci/config.yml b/.circleci/config.yml index 75e45da..fa43b0c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,13 +1,69 @@ version: 2.1 -jobs: - build: + +executors: + default: + working_directory: ~/repo + description: The official CircleCI Ruby Docker image docker: - - image: ruby:2.4.2 + - image: circleci/ruby:2.7.2 + +caches: + - &bundle_cache_full v2-repo-{{ checksum "Gemfile.lock" }} + - &bundle_cache v2-repo- + +commands: + defaults: steps: - checkout + - restore_cache: + keys: + - *bundle_cache_full + - *bundle_cache + - run: bundle install --path vendor/bundle + - save_cache: + key: *bundle_cache_full + paths: + - vendor/bundle + run_linters: + description: command to start linters + steps: + - run: + name: rubocop + command: bundle exec rubocop + - run: + name: fasterer + command: bundle exec fasterer + run_specs: + steps: - run: - name: Run the default task + name: run specs command: | - gem install bundler -v 2.2.4 - bundle install - bundle exec rake + mkdir /tmp/test-results + TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)" + bundle exec rspec --format progress \ + --out /tmp/test-results/rspec.xml \ + $TEST_FILES + - store_artifacts: + path: ~/repo/coverage + destination: coverage + +jobs: + lintering: + executor: default + steps: + - defaults + - run_linters + run_specs: + executor: default + steps: + - defaults + - run_specs + +workflows: + version: 2.1 + build: + jobs: + - lintering + - run_specs: + requires: + - lintering diff --git a/.fasterer.yml b/.fasterer.yml new file mode 100644 index 0000000..9641667 --- /dev/null +++ b/.fasterer.yml @@ -0,0 +1,22 @@ +speedups: + rescue_vs_respond_to: true + module_eval: true + shuffle_first_vs_sample: true + for_loop_vs_each: true + each_with_index_vs_while: false + map_flatten_vs_flat_map: true + reverse_each_vs_reverse_each: true + select_first_vs_detect: true + sort_vs_sort_by: true + fetch_with_argument_vs_block: true + keys_each_vs_each_key: true + hash_merge_bang_vs_hash_brackets: true + block_vs_symbol_to_proc: true + proc_call_vs_yield: true + gsub_vs_tr: true + select_last_vs_reverse_detect: true + getter_vs_attr_reader: true + setter_vs_attr_writer: true + +exclude_paths: + - 'vendor/**/*.rb' diff --git a/.rubocop.yml b/.rubocop.yml index 00a72e3..5b60fbd 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,10 +1,22 @@ -Style/StringLiterals: - Enabled: true - EnforcedStyle: double_quotes +require: + - rubocop-performance + - rubocop-rspec -Style/StringLiteralsInInterpolation: - Enabled: true - EnforcedStyle: double_quotes +AllCops: + NewCops: enable + Exclude: + - "codebreaker.gemspec" + - "vendor/bundle/**/*" + +Metrics/BlockLength: + Exclude: + - "spec/codebreaker/**/*" + +Style/FrozenStringLiteralComment: + Enabled: false + +Style/Documentation: + Enabled: false Layout/LineLength: Max: 120 diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..2eb2fe9 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-2.7.2 diff --git a/Gemfile b/Gemfile index 241197e..3fac2ed 100644 --- a/Gemfile +++ b/Gemfile @@ -1,12 +1,4 @@ -# frozen_string_literal: true - -source "https://rubygems.org" +source 'https://rubygems.org' # Specify your gem's dependencies in codebreaker.gemspec gemspec - -gem "rake", "~> 13.0" - -gem "rspec", "~> 3.0" - -gem "rubocop", "~> 0.80" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..07dfb8e --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,149 @@ +PATH + remote: . + specs: + codebreaker (0.1.0) + +GEM + remote: https://rubygems.org/ + specs: + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) + ast (2.4.2) + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) + byebug (11.1.3) + coderay (1.1.3) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) + colorize (0.8.1) + concurrent-ruby (1.1.8) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) + diff-lcs (1.4.4) + docile (1.3.5) + equalizer (0.0.11) + erubis (2.7.0) + fasterer (0.9.0) + colorize (~> 0.7) + ruby_parser (>= 3.14.1) + flay (2.12.1) + erubis (~> 2.7.0) + path_expander (~> 1.0) + ruby_parser (~> 3.0) + sexp_processor (~> 4.0) + flog (4.6.4) + path_expander (~> 1.0) + ruby_parser (~> 3.1, > 3.1.0) + sexp_processor (~> 4.8) + i18n (1.8.10) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + kwalify (0.7.2) + launchy (2.5.0) + addressable (~> 2.7) + method_source (1.0.0) + parallel (1.20.1) + parser (3.0.1.0) + ast (~> 2.4.1) + path_expander (1.1.0) + pry (0.13.1) + coderay (~> 1.1) + method_source (~> 1.0) + pry-byebug (3.9.0) + byebug (~> 11.0) + pry (~> 0.13.0) + psych (3.3.1) + public_suffix (4.0.6) + rainbow (3.0.0) + rake (13.0.3) + reek (6.0.3) + kwalify (~> 0.7.0) + parser (~> 3.0.0) + psych (~> 3.1) + rainbow (>= 2.0, < 4.0) + regexp_parser (2.1.1) + rexml (3.2.5) + rspec (3.10.0) + rspec-core (~> 3.10.0) + rspec-expectations (~> 3.10.0) + rspec-mocks (~> 3.10.0) + rspec-core (3.10.1) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-support (3.10.2) + rubocop (1.12.1) + parallel (~> 1.10) + parser (>= 3.0.0.0) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml + rubocop-ast (>= 1.2.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 3.0) + rubocop-ast (1.4.1) + parser (>= 2.7.1.5) + rubocop-performance (1.10.2) + rubocop (>= 0.90.0, < 2.0) + rubocop-ast (>= 0.4.0) + rubocop-rake (0.5.1) + rubocop + rubocop-rspec (2.2.0) + rubocop (~> 1.0) + rubocop-ast (>= 1.1.0) + ruby-progressbar (1.11.0) + ruby_parser (3.15.1) + sexp_processor (~> 4.9) + rubycritic (4.6.1) + flay (~> 2.8) + flog (~> 4.4) + launchy (>= 2.0.0) + parser (>= 2.6.0) + rainbow (~> 3.0) + reek (~> 6.0, < 7.0) + ruby_parser (~> 3.8) + simplecov (>= 0.17.0) + tty-which (~> 0.4.0) + virtus (~> 1.0) + sexp_processor (4.15.2) + simplecov (0.21.2) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.12.3) + simplecov_json_formatter (0.1.2) + thread_safe (0.3.6) + tty-which (0.4.2) + unicode-display_width (2.0.0) + virtus (1.0.5) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0, >= 0.0.3) + equalizer (~> 0.0, >= 0.0.9) + +PLATFORMS + ruby + x86_64-darwin-20 + +DEPENDENCIES + codebreaker! + fasterer (~> 0.9.0) + i18n (~> 1.8.10) + pry-byebug (~> 3.9.0) + rake (~> 13.0.3) + rspec (~> 3.10.0) + rubocop (~> 1.12.1) + rubocop-performance (~> 1.10.2) + rubocop-rake (~> 0.5.1) + rubocop-rspec (~> 2.2.0) + rubycritic (~> 4.6.1) + simplecov (~> 0.21.2) + +BUNDLED WITH + 2.2.3 diff --git a/Rakefile b/Rakefile index cca7175..85b3874 100644 --- a/Rakefile +++ b/Rakefile @@ -1,11 +1,9 @@ -# frozen_string_literal: true - -require "bundler/gem_tasks" -require "rspec/core/rake_task" +require 'bundler/gem_tasks' +require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) -require "rubocop/rake_task" +require 'rubocop/rake_task' RuboCop::RakeTask.new diff --git a/bin/console b/bin/console index a3c76e4..7654e6c 100755 --- a/bin/console +++ b/bin/console @@ -1,8 +1,7 @@ #!/usr/bin/env ruby -# frozen_string_literal: true -require "bundler/setup" -require "codebreaker" +require 'bundler/setup' +require 'codebreaker' # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. @@ -11,5 +10,5 @@ require "codebreaker" # require "pry" # Pry.start -require "irb" +require 'irb' IRB.start(__FILE__) diff --git a/codebreaker.gemspec b/codebreaker.gemspec index f1050ea..3144d01 100644 --- a/codebreaker.gemspec +++ b/codebreaker.gemspec @@ -1,5 +1,3 @@ -# frozen_string_literal: true - require_relative "lib/codebreaker/version" Gem::Specification.new do |spec| @@ -8,30 +6,31 @@ Gem::Specification.new do |spec| spec.authors = ["mechetel"] spec.email = ["dima.homa5@gmail.com"] - spec.summary = "TODO: Write a short summary, because RubyGems requires one." - spec.description = "TODO: Write a longer description or delete this line." - spec.homepage = "TODO: Put your gem's website or public repo URL here." + spec.summary = 'Codebreaker game' + spec.description = 'Second rubygarage task' + spec.homepage = 'https://github.com/mechetel/codebreaker' spec.license = "MIT" - spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") - - spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" + spec.required_ruby_version = Gem::Requirement.new(">= 2.7.2") spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here." - spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." + spec.metadata["source_code_uri"] = "https://github.com/mechetel/codebreaker" - # Specify which files should be added to the gem when it is released. - # The `git ls-files -z` loads the files in the RubyGem that have been added into git. spec.files = Dir.chdir(File.expand_path(__dir__)) do `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) } end - spec.bindir = "exe" + spec.bindir = 'exe' spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } - spec.require_paths = ["lib"] - - # Uncomment to register a new dependency of your gem - # spec.add_dependency "example-gem", "~> 1.0" - - # For more information and examples about making a new gem, checkout our - # guide at: https://bundler.io/guides/creating_gem.html + spec.require_paths = ['lib'] + + spec.add_development_dependency 'pry-byebug', '~> 3.9.0' + spec.add_development_dependency 'i18n', '~> 1.8.10' + spec.add_development_dependency 'rake', '~> 13.0.3' + spec.add_development_dependency 'fasterer', '~> 0.9.0' + spec.add_development_dependency 'rspec', '~> 3.10.0' + spec.add_development_dependency 'rubocop', '~> 1.12.1' + spec.add_development_dependency 'rubocop-performance', '~> 1.10.2' + spec.add_development_dependency 'rubocop-rspec', '~> 2.2.0' + spec.add_development_dependency 'rubocop-rake', '~> 0.5.1' + spec.add_development_dependency 'rubycritic', '~> 4.6.1' + spec.add_development_dependency 'simplecov', '~> 0.21.2' end diff --git a/lib/codebreaker.rb b/lib/codebreaker.rb index dd39b2d..1e764d4 100644 --- a/lib/codebreaker.rb +++ b/lib/codebreaker.rb @@ -1,8 +1,19 @@ -# frozen_string_literal: true +require_relative 'codebreaker/version' -require_relative "codebreaker/version" +require 'yaml' +require 'pry' +require 'i18n' -module Codebreaker - class Error < StandardError; end - # Your code goes here... -end +I18n.load_path << Dir["#{File.expand_path('locales')}/*.yml"] +I18n.default_locale = :en + +require_relative 'codebreaker/errors/validation_error' + +require_relative 'codebreaker/modules/validator' + +require_relative 'codebreaker/services/statistics_service' + +require_relative 'codebreaker/entities/difficulty' +require_relative 'codebreaker/entities/game' +require_relative 'codebreaker/entities/guess_checker' +require_relative 'codebreaker/entities/user' diff --git a/lib/codebreaker/entities/difficulty.rb b/lib/codebreaker/entities/difficulty.rb new file mode 100644 index 0000000..948ba78 --- /dev/null +++ b/lib/codebreaker/entities/difficulty.rb @@ -0,0 +1,31 @@ +module Codebreaker + class Difficulty + include Validator + + DIFFICULTIES = { + easy: { attempts: 15, hints: 2 }, + medium: { attempts: 10, hints: 1 }, + hell: { attempts: 5, hints: 1 } + }.freeze + + attr_reader :level, :errors + + def initialize(level) + @level = level.to_sym + @errors = [] + end + + def attempts + DIFFICULTIES[@level][:attempts] + end + + def hints + DIFFICULTIES[@level][:hints] + end + + def valid? + validate_difficulty(@level, @errors) + @errors.empty? + end + end +end diff --git a/lib/codebreaker/entities/game.rb b/lib/codebreaker/entities/game.rb new file mode 100644 index 0000000..1d774b8 --- /dev/null +++ b/lib/codebreaker/entities/game.rb @@ -0,0 +1,53 @@ +module Codebreaker + class Game + MIN_CODE_NUM = 1 + MAX_CODE_NUM = 6 + DIGITS_NUM = 4 + + attr_reader :user, + :difficulty, + :secret_code, + :date, + :hints_list, + :attempts, + :hints + + def initialize(user, difficulty) + @user = user + @difficulty = difficulty + @attempts = @difficulty.attempts + @hints = @difficulty.hints + @secret_code = generate_secret_code + @hints_list = @secret_code.shuffle + @date = Time.now.getlocal + end + + def use_hint + @hints -= 1 + @hints_list.pop + end + + def check_attempt(guess) + @attempts -= 1 + GuessChecker.new(@secret_code.clone, guess).check + end + + def lose? + @attempts.zero? + end + + def win?(user_code) + user_code == @secret_code.join + end + + def no_hints? + @hints.zero? + end + + private + + def generate_secret_code + Array.new(DIGITS_NUM) { rand(MIN_CODE_NUM..MAX_CODE_NUM) } + end + end +end diff --git a/lib/codebreaker/entities/guess_checker.rb b/lib/codebreaker/entities/guess_checker.rb new file mode 100644 index 0000000..66ea9ab --- /dev/null +++ b/lib/codebreaker/entities/guess_checker.rb @@ -0,0 +1,45 @@ +module Codebreaker + class GuessChecker + extend Validator + + RIGHT_ANSWER_SYMBOL = '+'.freeze + WRONG_ANSWER_SYMBOL = '-'.freeze + + def initialize(code, input) + @secret_code = code + @user_input = input.chars.map(&:to_i) + end + + def check + pluses + minuses + end + + def self.validate(guess) + validate_guess_count(guess) + validate_guess_for_not_integer(guess) + validate_guess_range(guess) + end + + private + + def pluses + answer = '' + @user_input.map.with_index do |number, index| + pluses_helper(answer, index) if @secret_code[index] == number + end + answer + end + + def minuses + @user_input.compact! + @secret_code.compact! + near_matchers = @secret_code & @user_input + Array.new(near_matchers.size) { '-' }.join + end + + def pluses_helper(answer, index) + answer << RIGHT_ANSWER_SYMBOL + @secret_code[index], @user_input[index] = nil + end + end +end diff --git a/lib/codebreaker/entities/user.rb b/lib/codebreaker/entities/user.rb new file mode 100644 index 0000000..76dcd9d --- /dev/null +++ b/lib/codebreaker/entities/user.rb @@ -0,0 +1,19 @@ +module Codebreaker + class User + include Validator + + attr_reader :name, :errors + + def initialize(name) + @name = name + @errors = [] + end + + def valid? + validate_name_class(@name, @errors) + validate_name_min_length(@name, @errors) if @errors.empty? + validate_name_max_length(@name, @errors) if @errors.empty? + @errors.empty? + end + end +end diff --git a/lib/codebreaker/errors/validation_error.rb b/lib/codebreaker/errors/validation_error.rb new file mode 100644 index 0000000..89882fc --- /dev/null +++ b/lib/codebreaker/errors/validation_error.rb @@ -0,0 +1,7 @@ +module Codebreaker + class ValidationError < StandardError + def initialize(msg = 'Validation error') + super(msg) + end + end +end diff --git a/lib/codebreaker/modules/validator.rb b/lib/codebreaker/modules/validator.rb new file mode 100644 index 0000000..c128c07 --- /dev/null +++ b/lib/codebreaker/modules/validator.rb @@ -0,0 +1,38 @@ +module Codebreaker + module Validator + NAME_MIN_LENGTH = 3 + NAME_MAX_LENGTH = 20 + + def validate_difficulty(level, errors) + errors << ValidationError.new(I18n.t('difficulty_error')) unless Difficulty::DIFFICULTIES.include? level + end + + def validate_name_class(name, errors) + errors << ValidationError.new(I18n.t('name_is_not_string_error')) unless name.is_a? String + end + + def validate_name_min_length(name, errors) + errors << ValidationError.new(I18n.t('short_name_error')) if name.length < NAME_MIN_LENGTH + end + + def validate_name_max_length(name, errors) + errors << ValidationError.new(I18n.t('long_name_error')) if name.length > NAME_MAX_LENGTH + end + + def validate_guess_for_not_integer(guess) + raise ValidationError, I18n.t('guess_is_not_integer') unless guess[/^\d+$/] + end + + def validate_guess_count(guess) + raise ValidationError, I18n.t('digits_count_error') unless guess.size == Game::DIGITS_NUM + end + + def validate_guess_range(guess) + unless guess.chars.all? do |num| + num.to_i.between? Game::MIN_CODE_NUM, Game::MAX_CODE_NUM + end + raise ValidationError, I18n.t('digit_range_error') + end + end + end +end diff --git a/lib/codebreaker/services/statistics_service.rb b/lib/codebreaker/services/statistics_service.rb new file mode 100644 index 0000000..0c6e454 --- /dev/null +++ b/lib/codebreaker/services/statistics_service.rb @@ -0,0 +1,49 @@ +module Codebreaker + class StatisticsService + attr_reader :game, :path + + def initialize(path) + @path = path + end + + def store(game) + statistics = load || [] + statistics << game_to_h(game) + file = File.open(@path, 'w') + file.write(statistics.to_yaml) + file.close + end + + def sort_statistics + load.sort_by do |user| + [user[:attempts_total], user[:attempts_used], user[:hints_used]] + end + end + + def load + make_files(@path) + YAML.load_file(@path) if File.exist?(@path) && !File.zero?(@path) + end + + private + + def make_files(file_path) + Dir.mkdir('db') unless Dir.exist?('db') + File.new(file_path, 'w') unless File.exist?(file_path) + end + + def game_to_h(game) + game_difficulty = game.difficulty + + { + name: game.user.name, + difficulty: game_difficulty.level.to_s, + attempts_total: game_difficulty.attempts, + attempts_used: game_difficulty.attempts - game.attempts, + hints_total: game_difficulty.hints, + hints_used: game_difficulty.hints - game.hints, + date: game.date + } + end + end +end diff --git a/lib/codebreaker/version.rb b/lib/codebreaker/version.rb index 8a35c43..98e7d70 100644 --- a/lib/codebreaker/version.rb +++ b/lib/codebreaker/version.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - module Codebreaker - VERSION = "0.1.0" + VERSION = '0.1.0'.freeze end diff --git a/locales/en.yml b/locales/en.yml new file mode 100644 index 0000000..b50a86f --- /dev/null +++ b/locales/en.yml @@ -0,0 +1,8 @@ +en: + guess_is_not_integer: 'Guess should be Integer class' + digits_count_error: 'Invalid digits count' + digit_range_error: 'Digit is not in a range' + difficulty_error: 'No such difficulty' + name_is_not_string_error: 'Name should be an instance of String' + short_name_error: 'Name is too short' + long_name_error: 'Name is too long' diff --git a/spec/codebreaker/difficulty_spec.rb b/spec/codebreaker/difficulty_spec.rb new file mode 100644 index 0000000..bad0b61 --- /dev/null +++ b/spec/codebreaker/difficulty_spec.rb @@ -0,0 +1,71 @@ +RSpec.describe Codebreaker::Difficulty do + let(:difficulty) { described_class.new(level) } + let(:level) { 'easy' } + let(:invalid_difficulty) { described_class.new(invalid_level) } + let(:invalid_level) { 'qwerty' } + let(:difficulty_constant) { Difficulty::DIFFICULTIES } + + describe '#level' do + context 'when level set' do + it 'level is a symbol' do + expect(difficulty.level).to eq(:easy) + end + end + end + + describe '#errors' do + context 'when errors set' do + it 'has 0 items' do + expect(difficulty.errors.size).to eq(0) + end + end + end + + describe '#initialize' do + it 'has level and errors field' do + expect(difficulty.instance_variables).to include(:@level, :@errors) + end + end + + describe '#valid?' do + context 'when entered level of difficulty is wrong' do + subject(:invalid_difficulty_valid?) { invalid_difficulty.valid? } + + before do + invalid_difficulty_valid? + end + + it 'validation returns false' do + expect(invalid_difficulty_valid?).to eq false + end + + it 'adds DifficultyError to errors' do + expect(invalid_difficulty.errors).to include Codebreaker::ValidationError + end + end + + context 'when entered level of difficulty is right' do + subject(:valid_difficulty_valid?) { difficulty.valid? } + + before do + valid_difficulty_valid? + end + + it 'validation returns true' do + expect(valid_difficulty_valid?).to eq true + end + + it 'adds nothing to errors' do + expect(difficulty.errors).to be_empty + end + + it 'returs number of attempts of appropriate difficulty' do + expect(difficulty.attempts).to eq(15) + end + + it 'returs number of hints of appropriate difficulty' do + expect(difficulty.hints).to eq(2) + end + end + end +end diff --git a/spec/codebreaker/game_spec.rb b/spec/codebreaker/game_spec.rb new file mode 100644 index 0000000..5bd6da6 --- /dev/null +++ b/spec/codebreaker/game_spec.rb @@ -0,0 +1,120 @@ +RSpec.describe Codebreaker::Game do + let(:user) { Codebreaker::User.new(user_name) } + let(:user_name) { 'Mechetel' } + let(:difficulty) { Codebreaker::Difficulty.new(difficulty_level) } + let(:difficulty_level) { 'hell' } + let(:game) { described_class.new(user, difficulty) } + + describe '#initialize' do + context 'when game starts it initializes with secret number' do + subject(:game_secret_number) { game.secret_code } + + before do + allow(game).to receive(:generate_secret_code) + end + + it 'has valid secret number length' do + expect(game_secret_number.length).to eq(4) + end + + it 'has valid secret number digits' do + game_secret_number.each do |digit| + expect(digit.to_i).to be_between(1, 6) + end + end + + it 'secret code is an Array' do + expect(game_secret_number.class).to eq Array + end + + it 'secret code size equal to DIGITS_NUM constant' do + expect(game_secret_number.size).to eq described_class::DIGITS_NUM + end + + it 'each element of secret code is between MIN_CODE_NUM and MAX_CODE_NUM contstants' do + game_secret_number.each do |digit| + expect(digit.to_i).to be_between(described_class::MIN_CODE_NUM, described_class::MAX_CODE_NUM).inclusive + end + end + + it 'hints list is equal to secret code' do + expect(game.hints_list).not_to eq game_secret_number + end + end + end + + it 'user field class equal to User' do + expect(game.user.class).to eq Codebreaker::User + end + + it 'difficulty field class equal to Difficulty' do + expect(game.difficulty.class).to eq Codebreaker::Difficulty + end + + describe '#take_hint' do + subject(:game_use_hint) { game.use_hint } + + it 'returns Integer' do + expect(game_use_hint.class).to eq Integer + end + + it 'returns only one number' do + expect(game_use_hint.to_s.size).to eq 1 + end + + it 'changes current_hints counter by 1' do + expect { game_use_hint }.to change { game.instance_variable_get(:@hints) }.by(-1) + end + + it 'removes one element from hints list' do + expect { game_use_hint }.to change { game.instance_variable_get(:@hints_list).size }.by(-1) + end + end + + describe '#lose?' do + subject(:game_lose) { game.lose? } + + it 'returns false when attempts are set' do + expect(game_lose).to be_falsey + end + + it 'returns true when attempts are equal to zero' do + game.instance_variable_set(:@attempts, 0) + expect(game_lose).to be_truthy + end + end + + describe '#win?' do + it 'returns false when user code is not equal to secret code' do + game.instance_variable_set(:@secret_code, [1, 2, 3, 4]) + expect(game).not_to be_win('2345') + end + + it 'returns true when user code is equal to secret code' do + expect(game).to be_win(game.secret_code.join) + end + end + + describe '#no_hints?' do + subject(:game_no_hints) { game.no_hints? } + + it 'returns false when hints are set' do + expect(game_no_hints).to be_falsey + end + + it 'returns true when hints are equal to zero' do + game.instance_variable_set(:@hints, 0) + expect(game_no_hints).to be_truthy + end + end + + describe '#check_attempt' do + it 'returns String' do + expect(game.check_attempt('2456').class).to eq String + end + + it 'returns string which size is between 0 and 4' do + expect(game.check_attempt('6163').length).to be_between(0, 4).inclusive + end + end +end diff --git a/spec/codebreaker/guess_checker_spec.rb b/spec/codebreaker/guess_checker_spec.rb new file mode 100644 index 0000000..f58acc7 --- /dev/null +++ b/spec/codebreaker/guess_checker_spec.rb @@ -0,0 +1,45 @@ +RSpec.describe Codebreaker::GuessChecker do + describe '#initialize' do + let(:guess_checker) { described_class.new('1234', '2345') } + + it 'has secret_code and user_input field' do + expect(guess_checker.instance_variables).to include(:@secret_code, :@user_input) + end + end + + describe '#validate' do + it 'raises DigitsCountError when digits count is invalid' do + expect { described_class.validate('102') }.to raise_error Codebreaker::ValidationError + end + + it 'raises DigitsCountError when guess is not a numbers' do + expect { described_class.validate('sdfg') }.to raise_error Codebreaker::ValidationError + end + + it 'raises DigitRangeError when any digit is not in the range' do + expect { described_class.validate('6969') }.to raise_error Codebreaker::ValidationError + end + end + + describe '#result' do + context 'when have some result' do + [ + { secret_number: '6543', input: '5643', result: '++--' }, + { secret_number: '6543', input: '6411', result: '+-' }, + { secret_number: '6543', input: '6544', result: '+++' }, + { secret_number: '6543', input: '3456', result: '----' }, + { secret_number: '6543', input: '6666', result: '+' }, + { secret_number: '6543', input: '2666', result: '-' }, + { secret_number: '6543', input: '2222', result: '' }, + { secret_number: '6666', input: '1661', result: '++' }, + { secret_number: '1234', input: '3124', result: '+---' }, + { secret_number: '1234', input: '1524', result: '++-' }, + { secret_number: '1234', input: '1234', result: '++++' } + ].each do |line| + it "returns #{line[:result]} when secret number - #{line[:secret_number]} and input - #{line[:input]}" do + expect(described_class.new(line[:secret_number].chars.map(&:to_i), line[:input]).check).to eq(line[:result]) + end + end + end + end +end diff --git a/spec/codebreaker/statistics_service_spec.rb b/spec/codebreaker/statistics_service_spec.rb new file mode 100644 index 0000000..87235e8 --- /dev/null +++ b/spec/codebreaker/statistics_service_spec.rb @@ -0,0 +1,25 @@ +RSpec.describe Codebreaker::StatisticsService do + let(:game) { Codebreaker::Game.new(Codebreaker::User.new('Mechetel'), Codebreaker::Difficulty.new('hell')) } + let(:path) { './lib/codebreaker/.yaml' } + let(:service) { described_class.new(path) } + + before { service.store game } + + after { File.delete(path) } + + describe '#store' do + it 'create file' do + expect(File.exist?(path)).to be true + end + end + + describe '#load' do + it 'returns Array' do + expect(service.load.class).to be Array + end + + it 'each array element is a Hash' do + expect(service.load.all?(Hash)).to be true + end + end +end diff --git a/spec/codebreaker/user_spec.rb b/spec/codebreaker/user_spec.rb new file mode 100644 index 0000000..d610799 --- /dev/null +++ b/spec/codebreaker/user_spec.rb @@ -0,0 +1,95 @@ +RSpec.describe Codebreaker::User do + let(:user) { described_class.new name } + let(:name) { 'Mechetel' } + + describe '#name' do + context 'when name set' do + it 'has appropriate name' do + expect(user.name).to eq(name) + end + end + end + + describe '#errors' do + context 'when errors set' do + it 'has 0 items' do + expect(user.errors.size).to eq(0) + end + end + end + + describe '#initialize' do + it 'has name and errors field field' do + expect(user.instance_variables).to include(:@name, :@errors) + end + end + + describe 'check User constants' do + it 'check content of NAME_MIN_LENGTH constant' do + expect(described_class::NAME_MIN_LENGTH).to eq(3) + end + + it 'check content of NAME_MAX_LENGTH constant' do + expect(described_class::NAME_MAX_LENGTH).to eq(20) + end + end + + describe '#valid?' do + context 'when entered name is too short' do + subject(:invalid_user_valid?) { short_name_user.valid? } + + let(:short_name_user) { described_class.new short_name } + let(:short_name) { 'Me' } + + before do + invalid_user_valid? + end + + it 'returns false' do + expect(short_name_user).not_to be_valid + end + + it 'adds ShortNameError to errors' do + expect(short_name_user.errors).to include Codebreaker::ValidationError + end + end + + context 'when entered name is too long' do + subject(:long_name_user_valid?) { long_name_user.valid? } + + let(:long_name_user) { described_class.new long_name } + let(:long_name) { 'dima' * 10 } + + before do + long_name_user_valid? + end + + it 'returns false' do + expect(long_name_user).not_to be_valid + end + + it 'adds longnameerror to errors' do + expect(long_name_user.errors).to include Codebreaker::ValidationError + end + end + + context 'when entered name is not an instance of String' do + subject(:invalid_user_valid?) { invalid_user.valid? } + + let(:invalid_user) { described_class.new inappropriate_user_name } + let(:inappropriate_user_name) { 322 } + + before do + invalid_user_valid? + end + + it 'returns false' do + expect(invalid_user).not_to be_valid + end + + it 'adds NameIsNotStringError to errors' do + expect(invalid_user.errors).to include Codebreaker::ValidationError + end + end + end +end diff --git a/spec/codebreaker_spec.rb b/spec/codebreaker_spec.rb index 49eae29..137754c 100644 --- a/spec/codebreaker_spec.rb +++ b/spec/codebreaker_spec.rb @@ -1,11 +1,5 @@ -# frozen_string_literal: true - RSpec.describe Codebreaker do - it "has a version number" do + it 'has a version number' do expect(Codebreaker::VERSION).not_to be nil end - - it "does something useful" do - expect(false).to eq(true) - end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6827d7b..56d2bd2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,12 +1,17 @@ -# frozen_string_literal: true +require 'simplecov' +require 'bundler/setup' +require 'codebreaker' -require "codebreaker" +SimpleCov.start do + enable_coverage :branch + add_filter 'spec/' +end -RSpec.configure do |config| - # Enable flags like --only-failures and --next-failure - config.example_status_persistence_file_path = ".rspec_status" +SimpleCov.minimum_coverage 95 - # Disable RSpec exposing methods globally on `Module` and `main` +RSpec.configure do |config| + config.example_status_persistence_file_path = '.rspec_status' + config.filter_run_when_matching :focus config.disable_monkey_patching! config.expect_with :rspec do |c|