connect_n_game 0.0.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.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +74 -0
- data/Rakefile +48 -0
- data/bin/connect_n_game +16 -0
- data/connect_n_game.gemspec +26 -0
- data/lib/cli/cli.rb +72 -0
- data/lib/cli/cli_debug.rb +20 -0
- data/lib/cli/cli_human.rb +41 -0
- data/lib/cli/cli_rack.rb +30 -0
- data/lib/cli/process_options.rb +62 -0
- data/lib/cli/select_players.rb +54 -0
- data/lib/connect_n_game.rb +13 -0
- data/lib/connect_n_game/exceptions.rb +8 -0
- data/lib/connect_n_game/game.rb +93 -0
- data/lib/connect_n_game/game/rack.rb +180 -0
- data/lib/connect_n_game/human.rb +30 -0
- data/lib/connect_n_game/player.rb +70 -0
- data/lib/connect_n_game/players/basic.rb +39 -0
- data/lib/connect_n_game/players/echo.rb +45 -0
- data/lib/connect_n_game/players/just_random.rb +40 -0
- data/lib/connect_n_game/players/middle.rb +41 -0
- data/lib/connect_n_game/players/prudent.rb +67 -0
- data/lib/connect_n_game/ui.rb +9 -0
- data/lib/connect_n_game/util.rb +26 -0
- data/lib/connect_n_game/version.rb +5 -0
- data/tests/test_game.rb +53 -0
- data/tests/test_module.rb +20 -0
- data/tests/test_player.rb +55 -0
- data/tests/test_rack.rb +222 -0
- data/tests/test_ui.rb +15 -0
- metadata +133 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
#Display and select player options.
|
3
|
+
|
4
|
+
module ConnectNGame
|
5
|
+
|
6
|
+
class CLI
|
7
|
+
|
8
|
+
#Select all new players
|
9
|
+
def select_new_players
|
10
|
+
@players = []
|
11
|
+
top_up_players
|
12
|
+
end
|
13
|
+
|
14
|
+
#Make sure we have two players
|
15
|
+
def top_up_players
|
16
|
+
pick_a_player while @players.length < 2
|
17
|
+
end
|
18
|
+
|
19
|
+
#Have the user pick a player.
|
20
|
+
def pick_a_player
|
21
|
+
begin
|
22
|
+
show_players
|
23
|
+
print "\nEnter player #{@players.length+1} name: "
|
24
|
+
input = gets.strip
|
25
|
+
player = find_player(input)
|
26
|
+
puts "invalid entry #{input.inspect}" unless player
|
27
|
+
end until player
|
28
|
+
|
29
|
+
@players << player
|
30
|
+
end
|
31
|
+
|
32
|
+
#Display the available players
|
33
|
+
def show_players
|
34
|
+
puts
|
35
|
+
puts "Player Selection: "
|
36
|
+
|
37
|
+
width = (Player.players.map do |player|
|
38
|
+
player.name.length
|
39
|
+
end).max
|
40
|
+
|
41
|
+
Player.players.each do |player|
|
42
|
+
puts " #{player.name.ljust(width+1)} #{player.description}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
#Find the selected player.
|
47
|
+
def find_player(arg)
|
48
|
+
Player.players.find do |player|
|
49
|
+
player.name.downcase == arg.downcase
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require_relative 'connect_n_game/util'
|
4
|
+
require_relative 'connect_n_game/exceptions'
|
5
|
+
require_relative 'connect_n_game/game'
|
6
|
+
require_relative 'connect_n_game/player'
|
7
|
+
require_relative 'connect_n_game/ui'
|
8
|
+
require_relative 'connect_n_game/version'
|
9
|
+
|
10
|
+
#The Connect N \Game main module
|
11
|
+
module ConnectNGame
|
12
|
+
#WIP
|
13
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require_relative 'game/rack.rb'
|
4
|
+
|
5
|
+
module ConnectNGame
|
6
|
+
|
7
|
+
#The \Game class of the connect_n_game.
|
8
|
+
#<br>Responsibility
|
9
|
+
#* This class is responsible for the overall operation of the game. It holds
|
10
|
+
# the playing "rack", and the players. It also keeps a log of moves,
|
11
|
+
# determines if the game has been won or if a stalemate has occurred.
|
12
|
+
#<br>Notes
|
13
|
+
#* The \Game class does NOT interact with the outside world. That is the
|
14
|
+
# job of the UI object.
|
15
|
+
class Game
|
16
|
+
|
17
|
+
#The \game playing surface
|
18
|
+
attr_reader :rack
|
19
|
+
|
20
|
+
#Whose playing?
|
21
|
+
attr_reader :players
|
22
|
+
|
23
|
+
#The play log.
|
24
|
+
attr_reader :log
|
25
|
+
|
26
|
+
#The current player number.
|
27
|
+
attr_reader :current
|
28
|
+
|
29
|
+
#The last played turn number.
|
30
|
+
attr_reader :turn
|
31
|
+
|
32
|
+
#Create an instance of a \game object.
|
33
|
+
#<br>Parameters
|
34
|
+
#* player_ex - The player who moves first
|
35
|
+
#* payer_oh - The player who moves next
|
36
|
+
#* game_size - The size of the game connection (4..8). Default: 4
|
37
|
+
#<br>Returns
|
38
|
+
#* An instance of a \Game.
|
39
|
+
def initialize(player_ex, player_oh, game_size=4)
|
40
|
+
@game_size = game_size
|
41
|
+
#Set up player related data.
|
42
|
+
@players = { 1 => player_ex, 2 => player_oh }
|
43
|
+
end
|
44
|
+
|
45
|
+
#What player moves next?
|
46
|
+
#<br>Returns
|
47
|
+
#* An instance of a \Player.
|
48
|
+
def current_player
|
49
|
+
players[current]
|
50
|
+
end
|
51
|
+
|
52
|
+
def previous_player
|
53
|
+
players[(@current % 2) + 1]
|
54
|
+
end
|
55
|
+
|
56
|
+
#Get ready to start a game
|
57
|
+
def game_initialize
|
58
|
+
#Set up game play data.
|
59
|
+
@turn = 0
|
60
|
+
@current = 1
|
61
|
+
@rack = Rack.new(@game_size)
|
62
|
+
@log = []
|
63
|
+
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
#Play the next move!
|
68
|
+
#<br>Returns
|
69
|
+
#* :victory, :stalemate, :continue, or :invalid_move
|
70
|
+
def next_move
|
71
|
+
channel = current_player.make_move(self, current)
|
72
|
+
score = rack.play_channel(channel, current)
|
73
|
+
@log << channel
|
74
|
+
@turn += 1
|
75
|
+
|
76
|
+
if score >= rack.order
|
77
|
+
:victory
|
78
|
+
elsif rack.rack_full?
|
79
|
+
:stalemate
|
80
|
+
else
|
81
|
+
@current = (@current % 2) + 1
|
82
|
+
:continue
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
#What was the last move?
|
87
|
+
def last_move
|
88
|
+
log[-1]
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module ConnectNGame
|
4
|
+
|
5
|
+
#The class of game racks. That is a matrix of cells
|
6
|
+
class Rack
|
7
|
+
|
8
|
+
#The number in a row needed for victory.
|
9
|
+
attr_reader :order
|
10
|
+
|
11
|
+
#The depth of the playable area.
|
12
|
+
attr_reader :depth
|
13
|
+
|
14
|
+
#The width of the playable area.
|
15
|
+
attr_reader :width
|
16
|
+
|
17
|
+
#The raw playable area.
|
18
|
+
attr_reader :rack
|
19
|
+
|
20
|
+
#The names of the channels
|
21
|
+
attr_reader :channel_names
|
22
|
+
|
23
|
+
#The column weight values.
|
24
|
+
attr_reader :weights
|
25
|
+
|
26
|
+
#Create a rack of the appropriate order.
|
27
|
+
#<br>Parameters
|
28
|
+
#* order - The order of the game, that is the winning
|
29
|
+
# number of pieces in a row.
|
30
|
+
def initialize(order)
|
31
|
+
unless (4..8).include?(order)
|
32
|
+
fail "Invalid game dimension #{order} not (4 .. 8)"
|
33
|
+
end
|
34
|
+
|
35
|
+
@order = order
|
36
|
+
@depth = @order + (order >> 1)
|
37
|
+
@width = @depth + (@depth.odd? ? 2 : 1)
|
38
|
+
|
39
|
+
@weights = [0.5]
|
40
|
+
weight = 4
|
41
|
+
|
42
|
+
(@width/2).times do
|
43
|
+
@weights << 1.0/weight
|
44
|
+
weight *= 2
|
45
|
+
@weights.insert(0, 1.0/weight)
|
46
|
+
weight *= 2
|
47
|
+
end
|
48
|
+
|
49
|
+
@channel_names = %w(A B C D E F G H I J K L M).first(width)
|
50
|
+
|
51
|
+
@rack = Array.new(@width) { [ ] }
|
52
|
+
end
|
53
|
+
|
54
|
+
#Get the required play channel
|
55
|
+
#<br>Parameters
|
56
|
+
#* channel - The channel number 1 .. width
|
57
|
+
#<br>Returns
|
58
|
+
#* The requested channel (array) or nil for invalid channels.
|
59
|
+
def get_channel(channel)
|
60
|
+
rack[channel-1]
|
61
|
+
end
|
62
|
+
|
63
|
+
#Is the selected channel full?
|
64
|
+
#<br>Parameters
|
65
|
+
#* channel - The channel number 1 .. width
|
66
|
+
#<br>Returns
|
67
|
+
#* true if full else false.
|
68
|
+
def channel_full?(channel)
|
69
|
+
get_channel(channel).length >= depth
|
70
|
+
end
|
71
|
+
|
72
|
+
#Is the rack full?
|
73
|
+
#<br>Returns
|
74
|
+
#* true if full (or invalid) else false.
|
75
|
+
def rack_full?
|
76
|
+
rack.each do |channel|
|
77
|
+
return false if channel.length < depth
|
78
|
+
end
|
79
|
+
|
80
|
+
true
|
81
|
+
end
|
82
|
+
|
83
|
+
#Get the specified cell.
|
84
|
+
#<br>Parameters
|
85
|
+
#* channel - The channel number 1 .. width
|
86
|
+
#* row - The row number 1 .. depth
|
87
|
+
#<br>Returns
|
88
|
+
#* The contents of the cell or nil
|
89
|
+
def get_cell(channel, row)
|
90
|
+
return nil unless valid_channel?(channel)
|
91
|
+
return nil unless valid_row?(row)
|
92
|
+
|
93
|
+
rack[channel-1][row-1]
|
94
|
+
end
|
95
|
+
|
96
|
+
#Play a specified channel.
|
97
|
+
#<br>Parameters
|
98
|
+
#* channel - The channel number 1 .. width
|
99
|
+
#* piece - The piece to be played.
|
100
|
+
#<br>Returns
|
101
|
+
#* The score of the move or raises \GameInvalidMove exception.
|
102
|
+
def play_channel(channel, piece)
|
103
|
+
score = score_move(channel, piece)
|
104
|
+
rack[channel-1] << piece if score > 0
|
105
|
+
score
|
106
|
+
end
|
107
|
+
|
108
|
+
#Determine the score obtained for moving to a specified channel
|
109
|
+
#<br>Parameters
|
110
|
+
#* channel - The channel number 1 .. width
|
111
|
+
#* piece - The piece to be played.
|
112
|
+
#<br>Returns
|
113
|
+
#* The score for that play 0 .. n
|
114
|
+
def score_move(channel, piece)
|
115
|
+
return -9 if channel_full?(channel)
|
116
|
+
|
117
|
+
row = channel_to_row(channel)
|
118
|
+
|
119
|
+
([[0,1], [1,1], [1,0], [1,-1]].map do |delta|
|
120
|
+
dx, dy = delta
|
121
|
+
count_pieces(channel, row, dx, dy, piece) + 1 +
|
122
|
+
count_pieces(channel, row, -dx, -dy, piece)
|
123
|
+
end).max
|
124
|
+
end
|
125
|
+
|
126
|
+
#Count the pieces along the designated path.
|
127
|
+
#<br>Parameters
|
128
|
+
#* channel - The starting channel
|
129
|
+
#* row - The starting row
|
130
|
+
#* dx - The channel step value
|
131
|
+
#* dy - The row step value
|
132
|
+
#* piece - The piece we are looking for.
|
133
|
+
#<br>Returns
|
134
|
+
#* The score for that play 0 .. n
|
135
|
+
def count_pieces(channel, row, dx, dy, piece)
|
136
|
+
result = 0
|
137
|
+
|
138
|
+
while result < width
|
139
|
+
channel, row = channel+dx, row+dy
|
140
|
+
|
141
|
+
return result unless piece == get_cell(channel, row)
|
142
|
+
|
143
|
+
result += 1
|
144
|
+
end
|
145
|
+
|
146
|
+
fail "Looping error"
|
147
|
+
end
|
148
|
+
|
149
|
+
#Get the free row for the specified channel.
|
150
|
+
#<br>Parameters
|
151
|
+
#* channel - The channel number 1 .. width
|
152
|
+
#<br>Returns
|
153
|
+
#* The row number or nil.
|
154
|
+
def channel_to_row(channel)
|
155
|
+
channel_full?(channel) ? nil : rack[channel-1].length + 1
|
156
|
+
end
|
157
|
+
|
158
|
+
#Is this a valid channel?
|
159
|
+
def valid_channel?(channel)
|
160
|
+
(1..width).include?(channel)
|
161
|
+
end
|
162
|
+
|
163
|
+
#Is this a valid row?
|
164
|
+
def valid_row?(row)
|
165
|
+
(1..depth).include?(row)
|
166
|
+
end
|
167
|
+
|
168
|
+
#Clone the internal array.
|
169
|
+
def deep_clone
|
170
|
+
@rack = @rack.clone
|
171
|
+
|
172
|
+
(0...@width).each do |index|
|
173
|
+
@rack[index] = @rack[index].clone
|
174
|
+
end
|
175
|
+
|
176
|
+
self
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module ConnectNGame
|
4
|
+
|
5
|
+
#The Human player simply makes carbon based moves.
|
6
|
+
class Human < Player
|
7
|
+
|
8
|
+
#Build the player
|
9
|
+
def initialize(name = "Human")
|
10
|
+
super(name, "An actual player.", :carbon)
|
11
|
+
end
|
12
|
+
|
13
|
+
#Move generation is part of the user interface.
|
14
|
+
|
15
|
+
#The thrill of victory.
|
16
|
+
def winners_comments
|
17
|
+
"Congratulations #{name}! You're our winner today!"
|
18
|
+
end
|
19
|
+
|
20
|
+
#The agony of defeat
|
21
|
+
def losers_comments
|
22
|
+
"Too bad #{name}, you lose. Hang your head in shame."
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
Player.players << Human.new
|
28
|
+
Player.players << Human.new("Bruce")
|
29
|
+
Player.players << Human.new("Sheila")
|
30
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module ConnectNGame
|
4
|
+
|
5
|
+
#The abstract Player class of the connect_n_game.
|
6
|
+
#<br>Responsibility
|
7
|
+
#* This mostly abstract class is responsible for specifying the shared
|
8
|
+
# behavior of all the sorts of players, both human and automaton. This is
|
9
|
+
# where moves are generated, victory celebrated, and defeat mourned.
|
10
|
+
class Player
|
11
|
+
#Set up class instance data.
|
12
|
+
@players = []
|
13
|
+
|
14
|
+
class << self
|
15
|
+
#The list of available players.
|
16
|
+
attr_reader :players
|
17
|
+
end
|
18
|
+
|
19
|
+
# The name of this player.
|
20
|
+
attr_reader :name
|
21
|
+
|
22
|
+
# The description of this player.
|
23
|
+
attr_reader :description
|
24
|
+
|
25
|
+
#The type of this player: :carbon or :silicon
|
26
|
+
attr_reader :type
|
27
|
+
|
28
|
+
#Do the common player setup
|
29
|
+
#<br>Parameters
|
30
|
+
#* name - The name of the player.
|
31
|
+
#* description - A witty description of the player.
|
32
|
+
#* type - :carbon or :silicon
|
33
|
+
#<br>Returns
|
34
|
+
#* An instance of \Player
|
35
|
+
def initialize(name, description, type)
|
36
|
+
fail "Invalid type #{type}" unless [:carbon, :silicon].include?(type)
|
37
|
+
@name, @description, @type = name, description, type
|
38
|
+
end
|
39
|
+
|
40
|
+
#Is this an automaton?
|
41
|
+
def silicon?
|
42
|
+
type == :silicon
|
43
|
+
end
|
44
|
+
|
45
|
+
#Is this an organic life-form?
|
46
|
+
def carbon?
|
47
|
+
type == :carbon
|
48
|
+
end
|
49
|
+
|
50
|
+
#Make a move. This is dummy code for testing only.
|
51
|
+
#<br>Parameters
|
52
|
+
#* _game - the game being played - not used here.
|
53
|
+
#* piece - the piece to be played, 1 or 2.
|
54
|
+
#<br>Returns
|
55
|
+
#* A move, 1 .. rack.width
|
56
|
+
def make_move(_game, piece)
|
57
|
+
piece
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
require_relative 'human' #Humans come first!!!
|
65
|
+
|
66
|
+
#Load up the players!
|
67
|
+
Dir[File.dirname(__FILE__) + '/players/*.rb'].each {|file| require file }
|
68
|
+
|
69
|
+
#Sort them
|
70
|
+
ConnectNGame::Player.players.sort! {|a,b| a.name.casecmp(b.name) }
|