command_four 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 72057059e8d1017885ac9dc9a4e4afd0ccc49cdd207e3fd2472a7bb1087345e1
4
+ data.tar.gz: 1c604bf1c478bb1dfe578c7218cf8426eaa3ecb5f04ebfa0cdffa4480be38c7d
5
+ SHA512:
6
+ metadata.gz: 9e2ae842c825a41ad570f04281cb49a9c2878f44e6058b11cc76d4f8f894d2387e117309be1ab1b89fabcb9d73e4ab27fd7f911e53704bd3d0210660c915ca5a
7
+ data.tar.gz: 424508b7559afd8415d9eccab9b1ae6dec4583bf05864b0d1f08c410bf2a392877a5bcc4c6a6d8b6842ffc8a33402a5eb0eef218260e54bd14eb166ff482fc84
@@ -0,0 +1,13 @@
1
+ todo.txt
2
+ .vscode
3
+ /.bundle/
4
+ /.yardoc
5
+ /_yardoc/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
11
+
12
+ # rspec failure tracking
13
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.6.6
6
+ before_install: gem install bundler -v 2.1.4
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "colorize", ">=0.8.1"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rake", "~> 12.0"
@@ -0,0 +1,36 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ command_four (1.0.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ colorize (0.8.1)
10
+ diff-lcs (1.3)
11
+ rake (12.3.3)
12
+ rspec (3.9.0)
13
+ rspec-core (~> 3.9.0)
14
+ rspec-expectations (~> 3.9.0)
15
+ rspec-mocks (~> 3.9.0)
16
+ rspec-core (3.9.2)
17
+ rspec-support (~> 3.9.3)
18
+ rspec-expectations (3.9.2)
19
+ diff-lcs (>= 1.2.0, < 2.0)
20
+ rspec-support (~> 3.9.0)
21
+ rspec-mocks (3.9.1)
22
+ diff-lcs (>= 1.2.0, < 2.0)
23
+ rspec-support (~> 3.9.0)
24
+ rspec-support (3.9.3)
25
+
26
+ PLATFORMS
27
+ x64-mingw32
28
+
29
+ DEPENDENCIES
30
+ colorize (>= 0.8.1)
31
+ command_four!
32
+ rake (~> 12.0)
33
+ rspec (~> 3.0)
34
+
35
+ BUNDLED WITH
36
+ 2.1.4
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+
2
+ MIT License
3
+
4
+ Copyright (c) 2020 Christopher Edwards
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,39 @@
1
+ # Command Four
2
+
3
+ A command-line implementation of Connect Four written in Ruby
4
+
5
+ ## Gameplay
6
+
7
+ From [Wikipedia](https://en.wikipedia.org/wiki/command_Four):
8
+
9
+ >Connect Four is a two-player connection board game in which the players first choose a color and then take turns dropping one colored disc from the top into a seven-column, six-row vertically suspended grid. The pieces fall straight down, occupying the lowest available space within the column. The objective of the game is to be the first to form a horizontal, vertical, or diagonal line of four of one's own discs."
10
+
11
+ This command line app has players enter a column number between 1 and 7 to indicate into which column they wish to drop their disc:
12
+
13
+ ![A game in progress](game_in_progress.png)
14
+
15
+ The game ends when one player gets four in a row, at which point the players get the option to play again. The losing player of one round will go first in the next round:
16
+
17
+ ![A completed game in which Yellow won](finished_game.png)
18
+
19
+ ## Installation
20
+
21
+ Add this line to your application's Gemfile:
22
+
23
+ ```ruby
24
+ gem 'command_four'
25
+ ```
26
+
27
+ And then execute:
28
+
29
+ $ bundle install
30
+
31
+ Or install it yourself as:
32
+
33
+ $ gem install command_four
34
+
35
+ ## Development
36
+
37
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
38
+
39
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require "command_four"
3
+ CLI.new.start
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -0,0 +1,27 @@
1
+ require_relative 'lib/command_four/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "command_four"
5
+ spec.version = CommandFour::VERSION
6
+ spec.authors = ["Christopher Edwards"]
7
+ spec.email = ["cedwards036@gmail.com"]
8
+
9
+ spec.summary = %q{A command-line connect-four game for two players}
10
+ spec.homepage = "https://github.com/cedwards036/command-four"
11
+ spec.license = "MIT"
12
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
13
+
14
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/cedwards036/command-four"
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
+ end
24
+ spec.bindir = "bin"
25
+ spec.executables = ["command_four"]
26
+ spec.require_paths = ["lib"]
27
+ end
Binary file
Binary file
@@ -0,0 +1,4 @@
1
+ require 'command_four/board'
2
+ require 'command_four/board_renderer'
3
+ require 'command_four/version'
4
+ require 'command_four/cli'
@@ -0,0 +1,233 @@
1
+ class Board
2
+ attr_reader :width, :height
3
+
4
+ def initialize(width = 7, height = 6, connect_n = 4)
5
+ @width = width
6
+ @height = height
7
+ @connect_n = connect_n
8
+ @state = GameState.new(false, [])
9
+ @board = Array.new(@width) {Array.new(@height, :empty)}
10
+ @completed_moves = 0
11
+ @max_moves = width * height
12
+ end
13
+
14
+ def drop_piece(column, color)
15
+ if game_over?()
16
+ raise PieceDropError.new("Game is already over")
17
+ elsif invalid_index?(column)
18
+ raise PieceDropError.new("Invalid column index: #{column}")
19
+ elsif full?(column)
20
+ raise PieceDropError.new("Column #{column} is already full")
21
+ else
22
+ first_empty_cell_index = @board[column].index {|cell| cell == :empty}
23
+ @board[column][first_empty_cell_index] = color
24
+ @completed_moves += 1
25
+ @state = ConnectNChecker.new(@board, @connect_n, column, first_empty_cell_index).check
26
+ if @completed_moves >= @max_moves && !game_over?()
27
+ @state = GameState.new(true, [])
28
+ end
29
+ end
30
+ end
31
+
32
+ def game_over?
33
+ @state.game_over?
34
+ end
35
+
36
+ def winning_cells
37
+ @state.winning_cells
38
+ end
39
+
40
+ def winning_color
41
+ if @state.winning_cells.length > 0
42
+ col_idx = @state.winning_cells[0][0]
43
+ row_idx = @state.winning_cells[0][1]
44
+ @board[col_idx][row_idx]
45
+ else
46
+ nil
47
+ end
48
+ end
49
+
50
+ def self.from_a(arr, connect_n = 4)
51
+ board = Board.new(arr.length, arr[0].length, connect_n)
52
+ for i in 0...arr[0].length
53
+ for j in 0...arr.length
54
+ if arr[j][i] != :empty
55
+ board.drop_piece(j, arr[j][i])
56
+ end
57
+ end
58
+ end
59
+ board
60
+ end
61
+
62
+ def to_a
63
+ @board
64
+ end
65
+
66
+ class PieceDropError < StandardError
67
+ end
68
+
69
+ private
70
+
71
+ def invalid_index?(column)
72
+ column < 0 || column >= @width
73
+ end
74
+
75
+ def full?(column)
76
+ @board[column][-1] != :empty
77
+ end
78
+
79
+ class ConnectNChecker
80
+
81
+ def initialize(board, connect_n, col_idx, row_idx)
82
+ @col_idx = col_idx
83
+ @row_idx = row_idx
84
+ @cur_color = board[col_idx][row_idx]
85
+ @width = board.length
86
+ @height = board[0].length
87
+ @board = board
88
+ @connect_n = connect_n
89
+ @winning_cells = []
90
+ end
91
+
92
+ def check
93
+ if check_horizontal || check_vertical || check_left_diag || check_right_diag
94
+ GameState.new(true, @winning_cells)
95
+ else
96
+ GameState.new(false, [])
97
+ end
98
+ end
99
+
100
+ def check_horizontal
101
+ @winning_cells = []
102
+ total_count = get_right_count + get_left_count
103
+ total_count >= @connect_n
104
+ end
105
+
106
+ def check_vertical
107
+ @winning_cells = []
108
+ total_count = get_up_count + get_down_count
109
+ total_count >= @connect_n
110
+ end
111
+
112
+ def check_left_diag
113
+ @winning_cells = []
114
+ total_count = get_up_left_count + get_down_left_count
115
+ total_count >= @connect_n
116
+ end
117
+
118
+ def check_right_diag
119
+ @winning_cells = []
120
+ total_count = get_up_right_count + get_down_right_count
121
+ total_count >= @connect_n
122
+ end
123
+
124
+ def get_right_count
125
+ consecutive_count = 0
126
+ cur_col_idx = @col_idx
127
+ while cur_col_idx < @width && consecutive_count <= @connect_n && @board[cur_col_idx][@row_idx] == @cur_color
128
+ @winning_cells.push([cur_col_idx, @row_idx])
129
+ cur_col_idx += 1
130
+ consecutive_count += 1
131
+ end
132
+ consecutive_count
133
+ end
134
+
135
+ def get_left_count
136
+ consecutive_count = 0
137
+ cur_col_idx = @col_idx - 1
138
+ while cur_col_idx >= 0 && consecutive_count <= @connect_n && @board[cur_col_idx][@row_idx] == @cur_color
139
+ @winning_cells.push([cur_col_idx, @row_idx])
140
+ cur_col_idx -= 1
141
+ consecutive_count += 1
142
+ end
143
+ consecutive_count
144
+ end
145
+
146
+ def get_up_count
147
+ consecutive_count = 0
148
+ cur_row_idx = @row_idx
149
+ while cur_row_idx < @height && consecutive_count <= @connect_n && @board[@col_idx][cur_row_idx] == @cur_color
150
+ @winning_cells.push([@col_idx, cur_row_idx])
151
+ cur_row_idx += 1
152
+ consecutive_count += 1
153
+ end
154
+ consecutive_count
155
+ end
156
+
157
+ def get_down_count
158
+ consecutive_count = 0
159
+ cur_row_idx = @row_idx - 1
160
+ while cur_row_idx >= 0 && consecutive_count <= @connect_n && @board[@col_idx][cur_row_idx] == @cur_color
161
+ @winning_cells.push([@col_idx, cur_row_idx])
162
+ cur_row_idx -= 1
163
+ consecutive_count += 1
164
+ end
165
+ consecutive_count
166
+ end
167
+
168
+ def get_up_left_count
169
+ consecutive_count = 0
170
+ cur_row_idx = @row_idx
171
+ cur_col_idx = @col_idx
172
+ while cur_row_idx < @height && cur_col_idx >= 0 && consecutive_count <= @connect_n && @board[cur_col_idx][cur_row_idx] == @cur_color
173
+ @winning_cells.push([cur_col_idx, cur_row_idx])
174
+ cur_row_idx += 1
175
+ cur_col_idx -= 1
176
+ consecutive_count += 1
177
+ end
178
+ consecutive_count
179
+ end
180
+
181
+ def get_down_left_count
182
+ consecutive_count = 0
183
+ cur_row_idx = @row_idx - 1
184
+ cur_col_idx = @col_idx + 1
185
+ while cur_row_idx >= 0 && cur_col_idx < @width && consecutive_count <= @connect_n && @board[cur_col_idx][cur_row_idx] == @cur_color
186
+ @winning_cells.push([cur_col_idx, cur_row_idx])
187
+ cur_row_idx -= 1
188
+ cur_col_idx += 1
189
+ consecutive_count += 1
190
+ end
191
+ consecutive_count
192
+ end
193
+
194
+ def get_up_right_count
195
+ consecutive_count = 0
196
+ cur_row_idx = @row_idx
197
+ cur_col_idx = @col_idx
198
+ while cur_row_idx < @height && cur_col_idx < @width && consecutive_count <= @connect_n && @board[cur_col_idx][cur_row_idx] == @cur_color
199
+ @winning_cells.push([cur_col_idx, cur_row_idx])
200
+ cur_row_idx += 1
201
+ cur_col_idx += 1
202
+ consecutive_count += 1
203
+ end
204
+ consecutive_count
205
+ end
206
+
207
+ def get_down_right_count
208
+ consecutive_count = 0
209
+ cur_row_idx = @row_idx - 1
210
+ cur_col_idx = @col_idx - 1
211
+ while cur_row_idx >= 0 && cur_col_idx >= 0 && consecutive_count <= @connect_n && @board[cur_col_idx][cur_row_idx] == @cur_color
212
+ @winning_cells.push([cur_col_idx, cur_row_idx])
213
+ cur_row_idx -= 1
214
+ cur_col_idx -= 1
215
+ consecutive_count += 1
216
+ end
217
+ consecutive_count
218
+ end
219
+ end
220
+
221
+ class GameState
222
+ attr_reader :winning_cells
223
+ def initialize(game_is_over, winning_cells)
224
+ @game_is_over = game_is_over
225
+ @winning_cells = winning_cells
226
+ end
227
+
228
+ def game_over?
229
+ @game_is_over
230
+ end
231
+ end
232
+
233
+ end
@@ -0,0 +1,53 @@
1
+ require 'colorize'
2
+
3
+ class BoardRenderer
4
+ def initialize(board)
5
+ @board = board
6
+ end
7
+
8
+ def render_for_printing
9
+ row_divider = ("+" + ("----+" * @board.width)).blue
10
+ col_divider = "|".blue
11
+ result = "" << (row_divider + "\n")
12
+ string_symbol_board = @board.to_a.map do |row|
13
+ row.map {|cell_color| TextCell.new(" ", "#{color_to_s(cell_color)}", " ")}
14
+ end
15
+ if @board.game_over?
16
+ add_winning_cell_highlights(string_symbol_board)
17
+ end
18
+ string_board = string_symbol_board.transpose.reverse.map do |row|
19
+ col_divider + row.map(&:to_s).join(col_divider) + col_divider
20
+ end
21
+ result << string_board.join(+ "\n" + row_divider + "\n") << "\n" <<row_divider
22
+ end
23
+
24
+ private
25
+
26
+ def color_to_s(color)
27
+ large_filled_circle = "\u2B24".encode('utf-8')
28
+ case color
29
+ when :red
30
+ large_filled_circle.red
31
+ when :yellow
32
+ large_filled_circle.yellow
33
+ when :empty
34
+ " "
35
+ else
36
+ raise "Invalid color #{color}"
37
+ end
38
+ end
39
+
40
+ def add_winning_cell_highlights(string_symbol_board)
41
+ @board.winning_cells.each do |coord|
42
+ text_cell = string_symbol_board[coord[0]][coord[1]]
43
+ text_cell.left_bookmark = "["
44
+ text_cell.right_bookmark = " ]"
45
+ end
46
+ end
47
+
48
+ TextCell = Struct.new(:left_bookmark, :text, :right_bookmark) do
49
+ def to_s
50
+ "#{left_bookmark}#{text}#{right_bookmark}"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,101 @@
1
+ require 'colorize'
2
+
3
+ require 'command_four/board'
4
+ require 'command_four/board_renderer'
5
+ require 'command_four/version'
6
+
7
+ class CLI
8
+ def start
9
+ game_is_in_session = true
10
+ colors = Colors.new
11
+ print_welcome_banner
12
+ while game_is_in_session
13
+ play_round(colors)
14
+ game_is_in_session = check_if_game_should_continue
15
+ end
16
+ puts "Thank you for playing!"
17
+ end
18
+
19
+ def print_welcome_banner
20
+ puts "========================="
21
+ puts " Welcome to Command Four"
22
+ puts "========================="
23
+ end
24
+
25
+ def play_round(colors)
26
+ board = Board.new
27
+ renderer = BoardRenderer.new(board)
28
+ until board.game_over?
29
+ puts renderer.render_for_printing
30
+ puts "#{color_to_s(colors.current_color)}'s turn"
31
+ play_turn(board, colors.current_color)
32
+ colors.switch_color
33
+ end
34
+ print_end_of_game_summary(board, renderer)
35
+ end
36
+
37
+ def play_turn(board, color)
38
+ turn_was_successful = false
39
+ until turn_was_successful
40
+ print "Please enter a column number between 1 (far-left) and #{board.width} (far-right): "
41
+ response = gets.chomp
42
+ if column_response_is_valid(response, board)
43
+ column = response.to_i - 1
44
+ begin
45
+ board.drop_piece(column, color)
46
+ turn_was_successful = true
47
+ rescue Board::PieceDropError
48
+ puts "Column #{response} is already full"
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ def column_response_is_valid(response, board)
55
+ /^[0-9]+$/ =~ response && response.to_i > 0 && response.to_i <= board.width
56
+ end
57
+
58
+ def check_if_game_should_continue
59
+ response = ""
60
+ until ["y", "n"].include? response
61
+ print "Would you like to play again (y / n)? "
62
+ response = gets.chomp.downcase
63
+ end
64
+ response == 'y'
65
+ end
66
+
67
+ def print_end_of_game_summary(board, renderer)
68
+ puts renderer.render_for_printing
69
+ if board.winning_color
70
+ puts "#{color_to_s(board.winning_color)} wins!"
71
+ else
72
+ puts "It's a draw!"
73
+ end
74
+ end
75
+
76
+ class Colors
77
+ def initialize
78
+ @colors = [:red, :yellow]
79
+ @cur_colors_index = 0
80
+ end
81
+
82
+ def current_color
83
+ @colors[@cur_colors_index]
84
+ end
85
+
86
+ def switch_color
87
+ @cur_colors_index = (@cur_colors_index + 1) % 2
88
+ end
89
+ end
90
+
91
+ def color_to_s(color)
92
+ case color
93
+ when :red
94
+ color.to_s.capitalize.red
95
+ when :yellow
96
+ color.to_s.capitalize.yellow
97
+ else
98
+ raise "Invalid color #{color}"
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,3 @@
1
+ module CommandFour
2
+ VERSION = "1.0.0"
3
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: command_four
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Christopher Edwards
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-06-21 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - cedwards036@gmail.com
16
+ executables:
17
+ - command_four
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - ".gitignore"
22
+ - ".rspec"
23
+ - ".travis.yml"
24
+ - Gemfile
25
+ - Gemfile.lock
26
+ - LICENSE
27
+ - README.md
28
+ - Rakefile
29
+ - bin/command_four
30
+ - bin/setup
31
+ - command_four.gemspec
32
+ - finished_game.png
33
+ - game_in_progress.png
34
+ - lib/command_four.rb
35
+ - lib/command_four/board.rb
36
+ - lib/command_four/board_renderer.rb
37
+ - lib/command_four/cli.rb
38
+ - lib/command_four/version.rb
39
+ homepage: https://github.com/cedwards036/command-four
40
+ licenses:
41
+ - MIT
42
+ metadata:
43
+ allowed_push_host: https://rubygems.org
44
+ homepage_uri: https://github.com/cedwards036/command-four
45
+ source_code_uri: https://github.com/cedwards036/command-four
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 2.3.0
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubygems_version: 3.0.3
62
+ signing_key:
63
+ specification_version: 4
64
+ summary: A command-line connect-four game for two players
65
+ test_files: []