connect_n_game 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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) }
|