diff --git a/concepts/ranges/.meta/config.json b/concepts/ranges/.meta/config.json new file mode 100644 index 0000000000..84b9c94c85 --- /dev/null +++ b/concepts/ranges/.meta/config.json @@ -0,0 +1,7 @@ +{ + "blurb": "Ruby has a Range object which represents an interval between two values.", + "authors": [ + "meatball133" + ], + "contributors": ["ihid", "kotp", "ryanplusplus"] +} diff --git a/concepts/ranges/about.md b/concepts/ranges/about.md new file mode 100644 index 0000000000..8509ad3982 --- /dev/null +++ b/concepts/ranges/about.md @@ -0,0 +1,127 @@ +# Ranges + +[Ranges][ranges] represent an interval between two values. +The most common types that support ranges are `Int` and `String`. +They can be used for many things like quickly creating a collection, slicing strings, checking if a value is in a range, and iteration. +They are created using the range operator `..` or `...` (inclusive and exclusive, respectively). + +```ruby +1..5 # => 1..5 +1...5 # => 1...5 + +(1..5).to_a # => [1, 2, 3, 4, 5] +(1...5).to_a # => [1, 2, 3, 4] +``` + +The reason for having two range operators is to allow to create ranges that are inclusive or exclusive of the end value, which can be useful when for example working with indexes that are zero based. + +Ranges can also be created using the `Range` initializer. + +```ruby +Range.new(1, 5) # A range containing 1, 2, 3, 4, 5 +``` + +````exercism/note +When creating a range in Ruby using the range operators `..` or `...`, and wanting to call a method on the range, you need to wrap the range in parentheses. +This is because the otherwise will the method be called on the 2nd argument of the range operator. + +```ruby +(1..5).sum # => 15 +1..5.sum # => Error: undefined method `sum' for 5:Integer (NoMethodError) +``` +```` + +## Getting substrings + +When wanting to slice a string, you can use the range operator to get a substring. +That is, by creating a range with the start and end index of the sub-string. + +```ruby +"Hello World"[0..4] # => "Hello" +"Hello World"[6..10] # => "World" +``` + +You can also use negative indexes to get the substring from the end of the string. + +```ruby +"Hello World"[-5..-1] # => "World" +"Hello World"[6..-4] # => "Wor" +``` + +## Range methods + +Ranges do have a set of methods that can be used to work with them. +For example, these methods can be used to get the sum of all the values in the range or check if the range includes a value. + +| Method | Description | Example | +| ----------------------- | ----------------------------------------------------------------------- | ------------------------------- | +| [`sum`][sum] | Returns the sum of all the values in the range | `(1..5).sum # => 15` | +| [`size`][size] | Returns the size of the range | `(1..5).size # => 5` | +| [`include?`][indlude] | Returns `true` if the range includes the given value, otherwise `false` | `(1..5).include?(3) # => true` | + +## Endless & Beginless ranges + +A range can be endless and beginless. +The endless or beginless range has their start or end value being `nil`, but when defining the range the `nil` can be omitted. + +Using beginless and endless ranges is useful when you want to, for example, slice a string from the beginning or to the end. + +```ruby +"Hello World"[0..] # => "Hello World" +"Hello World"[4..] # => "o World" +"Hello World"[..5] # => "Hello" +``` + +```exercism/caution +If not used on a collection, the endless range can cause an endless sequence, if not used with caution. +``` + +## String ranges + +Strings can also be used in ranges and allow one to get an interval of strings between two strings. +Its behavior can be a bit unexpected when using certain strings, so use it with caution. + +```ruby +"aa".."az".to_a # => ["aa", "ab", "ac", ..., "az"] +``` + +## Custom objects in ranges + +````exercism/advanced +Ruby allows you to use custom objects in ranges. +The requirement for this is that the object implements the following: + +- include the `Comparable` module +- `succ` method +- `<=>` method + +These methods make it so that the range can iterate over the object and compare the objects in the range. + +```ruby +class Foo + include Comparable + + attr_reader :value + + def initialize(value) + @value = value + end + + def succ + Foo.new(value + 1) + end + + def <=>(other) + value <=> other.value + end +end + +(Foo.new(1)..Foo.new(5)) +# => #, #, #, #, # +``` +```` + +[range]: https://rubyapi.org/o/range +[sum]: https://rubyapi.org/o/enumerable#method-i-sum +[size]: https://rubyapi.org/o/range#method-i-size +[indlude]: https://rubyapi.org/o/range#method-i-include-3F diff --git a/concepts/ranges/introduction.md b/concepts/ranges/introduction.md new file mode 100644 index 0000000000..33fe5a894a --- /dev/null +++ b/concepts/ranges/introduction.md @@ -0,0 +1,91 @@ +# Ranges + +[Ranges][ranges] represent an interval between two values. +The most common types that support ranges are `Int` and `String`. +They can be used for many things like quickly creating a collection, slicing strings, checking if a value is in a range, and iteration. +They are created using the range operator `..` or `...` (inclusive and exclusive, respectively). + +```ruby +1..5 # => 1..5 +1...5 # => 1...5 + +(1..5).to_a # => [1, 2, 3, 4, 5] +(1...5).to_a # => [1, 2, 3, 4] +``` + +The reason for having two range operators is to allow to create ranges that are inclusive or exclusive of the end value, which can be useful when for example working with indexes that are zero based. + +Ranges can also be created using the `Range` initializer. + +```ruby +Range.new(1, 5) # A range containing 1, 2, 3, 4, 5 +``` + +````exercism/note +When creating a range in Ruby using the range operators `..` or `...`, and wanting to call a method on the range, you need to wrap the range in parentheses. +This is because the otherwise will the method be called on the 2nd argument of the range operator. + +```ruby +(1..5).sum # => 15 +1..5.sum # => Error: undefined method `sum' for 5:Integer (NoMethodError) +``` +```` + +## Getting substrings + +When wanting to slice a string, you can use the range operator to get a substring. +That is, by creating a range with the start and end index of the sub-string. + +```ruby +"Hello World"[0..4] # => "Hello" +"Hello World"[6..10] # => "World" +``` + +You can also use negative indexes to get the substring from the end of the string. + +```ruby +"Hello World"[-5..-1] # => "World" +"Hello World"[6..-4] # => "Wor" +``` + +## Range methods + +Ranges do have a set of methods that can be used to work with them. +For example, these methods can be used to get the sum of all the values in the range or check if the range includes a value. + +| Method | Description | Example | +| ----------------------- | ----------------------------------------------------------------------- | ------------------------------- | +| [`sum`][sum] | Returns the sum of all the values in the range | `(1..5).sum # => 15` | +| [`size`][size] | Returns the size of the range | `(1..5).size # => 5` | +| [`include?`][indlude] | Returns `true` if the range includes the given value, otherwise `false` | `(1..5).include?(3) # => true` | + +## Endless & Beginless ranges + +A range can be endless and beginless. +The endless or beginless range has their start or end value being `nil`, but when defining the range the `nil` can be omitted. + +Using beginless and endless ranges is useful when you want to, for example, slice a string from the beginning or to the end. + +```ruby +"Hello World"[0..] # => "Hello World" +"Hello World"[4..] # => "o World" +"Hello World"[..5] # => "Hello" +``` + +```exercism/caution +If not used on a collection, the endless range can cause an endless sequence, if not used with caution. +``` + +## String ranges + +Strings can also be used in ranges and allow one to get an interval of strings between two strings. +Its behavior can be a bit unexpected when using certain strings, so use it with caution. + +```ruby +"aa".."az".to_a # => ["aa", "ab", "ac", ..., "az"] +``` + +[range]: https://rubyapi.org/o/range +[sum]: https://rubyapi.org/o/enumerable#method-i-sum +[size]: https://rubyapi.org/o/range#method-i-size +[indlude]: https://rubyapi.org/o/range#method-i-include-3F diff --git a/concepts/ranges/links.json b/concepts/ranges/links.json new file mode 100644 index 0000000000..93ee3102a9 --- /dev/null +++ b/concepts/ranges/links.json @@ -0,0 +1,11 @@ +[ + { + "url": "https://www.rubyguides.com/2016/06/ruby-ranges-how-do-they-work/", + "description": "Ruby Guides: Ruby Ranges: How Do They Work?" + }, + { + "url": "https://rubyapi.org/o/range", + "description": "Ruby api: Ranges" + } + ] + \ No newline at end of file diff --git a/config.json b/config.json index 5047efb28a..099d865cca 100644 --- a/config.json +++ b/config.json @@ -118,6 +118,17 @@ "conditionals" ] }, + { + "slug": "chess-game", + "name": "Chess Game", + "uuid": "3b107bdb-42a1-43de-95d4-ad1370a70cfc", + "concepts": [ + "ranges" + ], + "prerequisites": [ + "symbols" + ] + }, { "slug": "bird-count", "name": "Bird Count", @@ -130,7 +141,8 @@ "instance-variables", "booleans", "symbols", - "conditionals" + "conditionals", + "ranges" ] }, { @@ -1655,7 +1667,12 @@ "uuid": "4b0f3718-cc06-4aa6-8b0f-7db3bbe6af5d", "slug": "symbols", "name": "Symbols" - }, + }, + { + "uuid": "ec83aa13-0861-46d4-a126-2ebf6b8705d3", + "slug": "ranges", + "name": "Ranges" + }, { "uuid": "0189a402-ed46-47ee-9f5b-cfa5f557720d", "slug": "enumeration", diff --git a/exercises/concept/chess-game/.docs/hints.md b/exercises/concept/chess-game/.docs/hints.md new file mode 100644 index 0000000000..f8442ba6d7 --- /dev/null +++ b/exercises/concept/chess-game/.docs/hints.md @@ -0,0 +1,31 @@ +# Hints + +## 1. Define rank & file range + +- You need to define two [constant][constants] that should hold a [`Range`][range] of ranks and files. +- The ranks should be an [`Int`][integers] `range` from 1 to 8. +- The files should be a [`String`][string] `Range` from 'A' to 'H'. +- The constant needs to be defined in the `Chess` [module][module]. + +## 2. Check if square is valid + +- You need to check if a value is within a range. +- There is [a method][include] that can be used to check if a value is within a range. + +## 3. Get player's nickname + +- You can get a slice by using a `Range` as input. +- There is a [method][upcase] that can be used to upcase a string. + +## 4. Create move message + +- You can index the square string to get the rank and file. +- You can use already defined methods to get the nickname of the player and to check if the move is valid. + +[constants]: https://www.rubyguides.com/2017/07/ruby-constants/ +[integers]: https://rubyapi.org/o/integer +[string]: https://rubyapi.org/o/string +[module]: https://rubyapi.org/o/module +[include]: https://rubyapi.org/o/range#method-i-include-3F +[range]: https://rubyapi.org/o/range +[upcase]: https://rubyapi.org/o/string#method-i-upcase diff --git a/exercises/concept/chess-game/.docs/instructions.md b/exercises/concept/chess-game/.docs/instructions.md new file mode 100644 index 0000000000..58326fbb3c --- /dev/null +++ b/exercises/concept/chess-game/.docs/instructions.md @@ -0,0 +1,71 @@ +# Instructions + +As a chess enthusiast, you would like to write your own version of the game. +Yes, there maybe plenty of implementations of chess available online already, but yours will be unique! + +You start with implementing a basic movement system for the pieces. + +The chess game will be played on a board that is 8 squares wide and 8 squares long. +The squares are identified by a letter and a number. + +## 1. Define rank & file range + +The game will have to store the ranks of the board. +The ranks are the rows of the board, and are numbered from 1 to 8. + +The game will also have to store the files of the board. +The files are the columns of the board and are identified by the letters A to H. + +Define the `Chess::Ranks` and `Chess::Files` constants that store the range of ranks and files respectively. + +```ruby +Chess::Ranks +# => 1..8 + +Chess::Files +# => 'A'..'H' +``` + +## 2. Check if square is valid + +The game will have to check if a square is valid. +A square is valid if the rank and file are within the ranges of the ranks and files. + +Define the `Chess.valid_square?` method that takes the arguments `rank` that holds an int of the rank and `file` that holds a char of the file. +The method should return `true` if the rank and file are within the ranges of the ranks and files and return `false` otherwise. + +```ruby +Chess.valid_square?(1, 'A') +# => true +``` + +## 3. Get player's nickname + +The game will have to get the nickname of the player. +The nickname is the first 2 characters of the player's first name and the last 2 characters of the player's last name. +The nickname should be capitalized. + +Define the `Chess.nickname` method that takes the arguments `first_name` that holds a string of the player's first name and `last_name` that holds a string of the player's last name. +The method should return the nickname of the player as capitalized string. + +```ruby +Chess.nickname("John", "Doe") +# => "JOOE" +``` + +## 4. Create move message + +The game will have to create a message for a move to say which player moved to which square. +The message should use the player's nickname and the square they moved to. +The game also has to determine if the move is valid by checking if the file and rank of the square are within the ranges of the files and ranks. + +If the move is valid, the message should be: `"{nickname} moved to {square}}"` +If the move is invalid, the message should be: `"{nickname} attempted to move to {square}, but that is not a valid square"` + +Define the `Chess.move_message` method that takes the arguments `first_name` that holds a string of the player's first_name, `last_name` that holds a string of the player's last_name, and `square` that holds a string of the square the player moved to. +The method should return the message for the move as a string. + +```ruby +Chess.move_message("John", "Doe", "A1") +# => "JOOE moved to A1" +``` diff --git a/exercises/concept/chess-game/.docs/introduction.md b/exercises/concept/chess-game/.docs/introduction.md new file mode 100644 index 0000000000..33fe5a894a --- /dev/null +++ b/exercises/concept/chess-game/.docs/introduction.md @@ -0,0 +1,91 @@ +# Ranges + +[Ranges][ranges] represent an interval between two values. +The most common types that support ranges are `Int` and `String`. +They can be used for many things like quickly creating a collection, slicing strings, checking if a value is in a range, and iteration. +They are created using the range operator `..` or `...` (inclusive and exclusive, respectively). + +```ruby +1..5 # => 1..5 +1...5 # => 1...5 + +(1..5).to_a # => [1, 2, 3, 4, 5] +(1...5).to_a # => [1, 2, 3, 4] +``` + +The reason for having two range operators is to allow to create ranges that are inclusive or exclusive of the end value, which can be useful when for example working with indexes that are zero based. + +Ranges can also be created using the `Range` initializer. + +```ruby +Range.new(1, 5) # A range containing 1, 2, 3, 4, 5 +``` + +````exercism/note +When creating a range in Ruby using the range operators `..` or `...`, and wanting to call a method on the range, you need to wrap the range in parentheses. +This is because the otherwise will the method be called on the 2nd argument of the range operator. + +```ruby +(1..5).sum # => 15 +1..5.sum # => Error: undefined method `sum' for 5:Integer (NoMethodError) +``` +```` + +## Getting substrings + +When wanting to slice a string, you can use the range operator to get a substring. +That is, by creating a range with the start and end index of the sub-string. + +```ruby +"Hello World"[0..4] # => "Hello" +"Hello World"[6..10] # => "World" +``` + +You can also use negative indexes to get the substring from the end of the string. + +```ruby +"Hello World"[-5..-1] # => "World" +"Hello World"[6..-4] # => "Wor" +``` + +## Range methods + +Ranges do have a set of methods that can be used to work with them. +For example, these methods can be used to get the sum of all the values in the range or check if the range includes a value. + +| Method | Description | Example | +| ----------------------- | ----------------------------------------------------------------------- | ------------------------------- | +| [`sum`][sum] | Returns the sum of all the values in the range | `(1..5).sum # => 15` | +| [`size`][size] | Returns the size of the range | `(1..5).size # => 5` | +| [`include?`][indlude] | Returns `true` if the range includes the given value, otherwise `false` | `(1..5).include?(3) # => true` | + +## Endless & Beginless ranges + +A range can be endless and beginless. +The endless or beginless range has their start or end value being `nil`, but when defining the range the `nil` can be omitted. + +Using beginless and endless ranges is useful when you want to, for example, slice a string from the beginning or to the end. + +```ruby +"Hello World"[0..] # => "Hello World" +"Hello World"[4..] # => "o World" +"Hello World"[..5] # => "Hello" +``` + +```exercism/caution +If not used on a collection, the endless range can cause an endless sequence, if not used with caution. +``` + +## String ranges + +Strings can also be used in ranges and allow one to get an interval of strings between two strings. +Its behavior can be a bit unexpected when using certain strings, so use it with caution. + +```ruby +"aa".."az".to_a # => ["aa", "ab", "ac", ..., "az"] +``` + +[range]: https://rubyapi.org/o/range +[sum]: https://rubyapi.org/o/enumerable#method-i-sum +[size]: https://rubyapi.org/o/range#method-i-size +[indlude]: https://rubyapi.org/o/range#method-i-include-3F diff --git a/exercises/concept/chess-game/.meta/config.json b/exercises/concept/chess-game/.meta/config.json new file mode 100644 index 0000000000..96fb0680de --- /dev/null +++ b/exercises/concept/chess-game/.meta/config.json @@ -0,0 +1,12 @@ +{ + "authors": ["meatball133"], + "contributors": ["kotp"], + "files": { + "solution": ["chess_game.rb"], + "test": ["chess_game_test.rb"], + "exemplar": [".meta/exemplar.rb"] + }, + "forked_from": ["elixir/chess-board", "crystal/chess-game"], + "blurb": "Learn about ranges while making a chess game.", + "icon": "chessboard" +} diff --git a/exercises/concept/chess-game/.meta/design.md b/exercises/concept/chess-game/.meta/design.md new file mode 100644 index 0000000000..917551ba13 --- /dev/null +++ b/exercises/concept/chess-game/.meta/design.md @@ -0,0 +1,26 @@ +# Design + +## Goal + +The goal of this exercise is to teach how to use ranges in Crystal and how to get substrings through ranges. + +## Learning objectives + +- Ranges + +## Out of scope + +- Enumeration + +## Concepts + +`Ranges`: + +- How to create ranges +- How to get substrings through ranges +- How to use range methods +- Know about endless and beginless ranges + +## Prerequisites + +- Symbols diff --git a/exercises/concept/chess-game/.meta/exemplar.rb b/exercises/concept/chess-game/.meta/exemplar.rb new file mode 100644 index 0000000000..3b020a0c4b --- /dev/null +++ b/exercises/concept/chess-game/.meta/exemplar.rb @@ -0,0 +1,23 @@ +module Chess + RANKS = 1..8 + FILES = 'A'..'H' + + def self.valid_square?(rank, file) + RANKS.include?(rank) && FILES.include?(file) + end + + def self.nick_name(first_name, last_name) + "#{first_name[...2]}#{last_name[-2..]}".upcase + end + + def self.move_message(first_name, last_name, square) + rank = square[1].to_i + file = square[0] + name = nick_name(first_name, last_name) + if valid_square?(rank, file) + "#{name} moved to #{square}" + else + "#{name} attempted to move to #{square}, but that is not a valid square" + end + end +end diff --git a/exercises/concept/chess-game/chess_game.rb b/exercises/concept/chess-game/chess_game.rb new file mode 100644 index 0000000000..7cb14dea24 --- /dev/null +++ b/exercises/concept/chess-game/chess_game.rb @@ -0,0 +1,16 @@ +module Chess + # TODO: define the 'RANKS' constant + # TODO: define the 'FILES' constant + + def self.valid_square?(rank, file) + raise "Please implement the Chess.valid_square? method" + end + + def self.nick_name(first_name, last_name) + raise "Please implement the Chess.nick_name method" + end + + def self.move_message(first_name, last_name, square) + raise "Please implement the Chess.move_message method" + end +end diff --git a/exercises/concept/chess-game/chess_game_test.rb b/exercises/concept/chess-game/chess_game_test.rb new file mode 100644 index 0000000000..22470663e6 --- /dev/null +++ b/exercises/concept/chess-game/chess_game_test.rb @@ -0,0 +1,60 @@ +require 'minitest/autorun' +require_relative 'chess_game' + +class ChessTest < Minitest::Test + def test_have_8_files + assert_equal 'A'..'H', Chess::FILES + end + + def test_have_8_ranks + assert_equal 1..8, Chess::RANKS + end + + def test_true_when_given_a_valid_square + assert Chess.valid_square?(1, 'A') + end + + def test_true_for_another_valid_square + assert Chess.valid_square?(8, 'H') + end + + def test_false_when_rank_is_out_of_range + refute Chess.valid_square?(9, 'B') + end + + def test_false_when_file_is_out_of_range + refute Chess.valid_square?(1, 'I') + end + + def test_false_when_rank_is_less_than_one + refute Chess.valid_square?(0, 'A') + end + + def test_correct_player_nick_name + assert_equal "JOOE", Chess.nick_name("John", "Doe") + end + + def test_correct_nickname_for_2_letter_last_name + assert_equal "LILI", Chess.nick_name("Lisa", "Li") + end + + def test_correct_nickname_for_2_letter_first_name + assert_equal "DJER", Chess.nick_name("Dj", "Walker") + end + + def test_correct_message_for_a_move + assert_equal "JOOE moved to A2", Chess.move_message("John", "Doe", "A2") + end + + def test_correct_message_when_moving_to_corner + assert_equal "LILI moved to H8", Chess.move_message("Lisa", "Li", "H8") + end + + def test_incorrect_message_when_out_of_board + assert_equal "DJER attempted to move to I9, but that is not a valid square", Chess.move_message("Dj", "Walker", "I9") + end + + def test_incorrect_message_when_being_on_rank_0 + assert_equal "TOON attempted to move to A0, but that is not a valid square", Chess.move_message("Tore", "Anderson", "A0") + end +end