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.
@@ -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,8 @@
1
+ # coding: utf-8
2
+
3
+ module ConnectNGame
4
+
5
+ #An invalid move was made.
6
+ class GameInvalidMove < StandardError; end
7
+
8
+ 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) }