command_four 1.0.0

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.
@@ -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: []