terminal_chess 0.1.2 → 0.2.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.
- checksums.yaml +4 -4
- data/Gemfile.lock +38 -0
- data/README.md +76 -52
- data/lib/board.rb +357 -0
- data/lib/local_chess_client.rb +45 -0
- data/lib/move.rb +97 -107
- data/lib/network_chess_client.rb +104 -0
- data/lib/printer.rb +129 -54
- data/lib/server.rb +65 -0
- data/lib/terminal_chess.rb +7 -26
- data/lib/terminal_chess/messages.rb +27 -0
- data/lib/terminal_chess/version.rb +1 -1
- data/terminal_chess.gemspec +5 -1
- data/test/test_terminal_chess.rb +240 -41
- metadata +65 -4
- data/lib/Board.rb +0 -266
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH << __FILE__ # '.'
|
4
|
+
|
5
|
+
require_relative "terminal_chess/version"
|
6
|
+
require_relative "printer.rb"
|
7
|
+
require_relative "move.rb"
|
8
|
+
require_relative "board.rb"
|
9
|
+
|
10
|
+
class LocalChessClient
|
11
|
+
def initialize
|
12
|
+
@board = Board.new
|
13
|
+
@turn_by_turn_playback = []
|
14
|
+
@board.display_board
|
15
|
+
|
16
|
+
start
|
17
|
+
end
|
18
|
+
|
19
|
+
def start
|
20
|
+
# Gameplay
|
21
|
+
loop do
|
22
|
+
if @board.checkmate
|
23
|
+
puts "\nTurn by Turn Playback : #{@turn_by_turn_playback}\n"
|
24
|
+
exit
|
25
|
+
end
|
26
|
+
|
27
|
+
print "\nPiece to Move [#{@board.player_turn.capitalize}]: "
|
28
|
+
from = gets.chomp.upcase
|
29
|
+
|
30
|
+
begin
|
31
|
+
print "Valid destinations: #{@board.valid_destinations(from).join(", ")}"
|
32
|
+
|
33
|
+
print "\nLocation: "
|
34
|
+
to = gets.chomp.upcase
|
35
|
+
@board.move(from, to)
|
36
|
+
|
37
|
+
rescue Exception => e
|
38
|
+
puts "Invalid selection #{e if !ENV["DEV"].nil?}"
|
39
|
+
else
|
40
|
+
@turn_by_turn_playback << [from, to]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
data/lib/move.rb
CHANGED
@@ -1,61 +1,63 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
module
|
3
|
+
module Move
|
4
4
|
|
5
5
|
def constants(piece_mapping, color, piece)
|
6
6
|
@@pieces = piece_mapping
|
7
|
-
@@color
|
8
|
-
@@
|
9
|
-
@@
|
7
|
+
@@color = color
|
8
|
+
@@type = piece
|
9
|
+
@@enemy_color = ([:black, :red] - [color]).first
|
10
10
|
end
|
11
11
|
|
12
12
|
|
13
13
|
# Calls methods below to return a list of positions which are valid moves
|
14
14
|
# for piece at index p1, given the current board layout as defined in manifest
|
15
15
|
def possible_moves(p1, manifest, castling = false)
|
16
|
+
return [] if manifest[p1][:type].nil?
|
17
|
+
|
16
18
|
allowed = []
|
17
|
-
type = manifest[p1][
|
18
|
-
my_color = manifest[p1][
|
19
|
-
constants(manifest, my_color, type)
|
19
|
+
type = manifest[p1][:type]
|
20
|
+
my_color = manifest[p1][:color]
|
20
21
|
|
21
|
-
|
22
|
+
constants(manifest, my_color, type)
|
22
23
|
|
23
|
-
|
24
|
-
allowed += [move_lateral(p1, 1)].flatten
|
25
|
-
allowed += [move_diagonal(p1, 1)].flatten
|
26
|
-
allowed += [castle(p1)].flatten if castling
|
24
|
+
return [] if unoccupied?(p1)
|
27
25
|
|
28
|
-
|
29
|
-
|
30
|
-
|
26
|
+
if type == :king
|
27
|
+
allowed += [move_lateral(p1, 1)].flatten
|
28
|
+
allowed += [move_diagonal(p1, 1)].flatten
|
29
|
+
allowed += [castle(p1)].flatten if castling
|
31
30
|
|
32
|
-
|
33
|
-
|
31
|
+
elsif type == :queen
|
32
|
+
allowed += [move_lateral(p1)].flatten
|
33
|
+
allowed += [move_diagonal(p1)].flatten
|
34
34
|
|
35
|
-
|
36
|
-
|
35
|
+
elsif type == :rook
|
36
|
+
allowed += [move_lateral(p1)].flatten
|
37
37
|
|
38
|
-
|
39
|
-
|
38
|
+
elsif type == :bishop
|
39
|
+
allowed += [move_diagonal(p1)].flatten
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
end
|
41
|
+
elsif type == :pawn
|
42
|
+
allowed += [move_pawn(p1)].flatten
|
44
43
|
|
44
|
+
elsif type == :knight
|
45
|
+
allowed += [move_knight(p1)].flatten
|
45
46
|
end
|
46
|
-
|
47
|
+
|
48
|
+
allowed
|
47
49
|
end
|
48
50
|
|
49
51
|
|
50
52
|
# Returns all valid positions a pawn at index p1 can move to
|
51
|
-
def
|
53
|
+
def move_pawn(p1)
|
52
54
|
row = get_row_from_index(p1)
|
53
55
|
col = get_col_from_index(p1)
|
54
56
|
valid = []
|
55
57
|
|
56
58
|
# Piece color defines direction of travel. Enemy presence defines
|
57
59
|
# the validity of diagonal movements
|
58
|
-
if @@color ==
|
60
|
+
if @@color == :red
|
59
61
|
if unoccupied?(p1 - 8)
|
60
62
|
valid << (p1 - 8)
|
61
63
|
end
|
@@ -66,10 +68,12 @@ module MOVE
|
|
66
68
|
valid << (p1 - 9)
|
67
69
|
end
|
68
70
|
# Only if the pieces is unmoved, can it move forward two rows
|
69
|
-
if !@@pieces[p1][
|
71
|
+
if !@@pieces[p1][:moved] && unoccupied?(p1 - 8) && unoccupied?(p1 - 16)
|
70
72
|
valid << (p1 - 16)
|
71
73
|
end
|
72
|
-
|
74
|
+
|
75
|
+
elsif @@color == :black
|
76
|
+
|
73
77
|
if unoccupied?(p1 + 8)
|
74
78
|
valid << (p1 + 8)
|
75
79
|
end
|
@@ -79,21 +83,22 @@ module MOVE
|
|
79
83
|
if piece_color(p1 + 9) == @@enemy_color && col < 8
|
80
84
|
valid << (p1 + 9)
|
81
85
|
end
|
82
|
-
if !@@pieces[p1][
|
86
|
+
if !@@pieces[p1][:moved] && unoccupied?(p1 + 8) && unoccupied?(p1 + 16)
|
83
87
|
valid << (p1 + 16)
|
84
88
|
end
|
85
89
|
end
|
86
90
|
|
87
|
-
|
91
|
+
valid
|
88
92
|
end
|
89
93
|
|
90
94
|
|
91
95
|
# Returns valid positions a knight at index p1 can move to
|
92
|
-
def
|
96
|
+
def move_knight(p1)
|
93
97
|
row = get_row_from_index(p1)
|
94
98
|
col = get_col_from_index(p1)
|
99
|
+
|
95
100
|
valid = []
|
96
|
-
|
101
|
+
valid_moves_no_friendly_fire = []
|
97
102
|
|
98
103
|
# Valid knight moves based on its board position
|
99
104
|
if row < 7 && col < 8
|
@@ -125,11 +130,11 @@ module MOVE
|
|
125
130
|
# This iterator filters for friendly fire, and removes indexes pointing to same color pices
|
126
131
|
valid.each do |pos|
|
127
132
|
unless piece_color(pos) == @@color
|
128
|
-
|
133
|
+
valid_moves_no_friendly_fire << pos
|
129
134
|
end
|
130
135
|
end
|
131
136
|
|
132
|
-
|
137
|
+
valid_moves_no_friendly_fire
|
133
138
|
end
|
134
139
|
|
135
140
|
|
@@ -140,66 +145,70 @@ module MOVE
|
|
140
145
|
def move_lateral(index, limit = 8)
|
141
146
|
row = get_row_from_index(index)
|
142
147
|
col = get_col_from_index(index)
|
148
|
+
|
143
149
|
left, right = [col-1, limit].min, [8-col, limit].min
|
144
150
|
up, down = [row-1, limit].min, [8-row, limit].min
|
151
|
+
|
145
152
|
valid = []
|
146
153
|
|
147
154
|
# Move down N places until board limit, piece in the way, or specified limit
|
148
155
|
down.times do |i|
|
149
|
-
|
150
|
-
|
151
|
-
|
156
|
+
next_pos = index + (i+1)*8
|
157
|
+
# Valid move if position is unoccupied
|
158
|
+
if unoccupied?(next_pos)
|
159
|
+
valid << next_pos
|
160
|
+
else
|
161
|
+
# Valid move is piece is an enemy, but then no subsequent tiles are attackable
|
162
|
+
# if the piece is not an enemy, it's not added as a valid move, and no subsequent tiles are attackable
|
163
|
+
# This function doesn't filter out the king from a valid enemy, but the Board class will drop King indexes
|
164
|
+
if piece_color(next_pos) == @@enemy_color
|
152
165
|
valid << next_pos
|
153
|
-
else
|
154
|
-
# Valid move is piece is an enemy, but then no subsequent tiles are attackable
|
155
|
-
# if the piece is not an enemy, it's not added as a valid move, and no subsequent tiles are attackable
|
156
|
-
# This function doesn't filter out the king from a valid enemy, but the Board class will drop King indexes
|
157
|
-
if piece_color(next_pos) == @@enemy_color
|
158
|
-
valid << next_pos
|
159
|
-
end
|
160
|
-
break
|
161
166
|
end
|
167
|
+
|
168
|
+
break
|
169
|
+
end
|
162
170
|
end
|
163
171
|
|
164
172
|
# Move up N places until board limit, piece in the way, or specified limit
|
165
173
|
up.times do |i|
|
166
|
-
|
167
|
-
|
174
|
+
next_pos = index - (i+1)*8
|
175
|
+
if unoccupied?(next_pos)
|
176
|
+
valid << next_pos
|
177
|
+
else
|
178
|
+
if piece_color(next_pos) == @@enemy_color
|
168
179
|
valid << next_pos
|
169
|
-
else
|
170
|
-
if piece_color(next_pos) == @@enemy_color
|
171
|
-
valid << next_pos
|
172
|
-
end
|
173
|
-
break
|
174
180
|
end
|
181
|
+
break
|
182
|
+
end
|
175
183
|
end
|
176
184
|
|
177
185
|
# Move right N places until board limit, piece in the way, or specified limit
|
178
186
|
right.times do |i|
|
179
|
-
|
180
|
-
|
187
|
+
next_pos = index + (i+1)
|
188
|
+
if unoccupied?(next_pos)
|
189
|
+
valid << next_pos
|
190
|
+
else
|
191
|
+
if piece_color(next_pos) == @@enemy_color
|
181
192
|
valid << next_pos
|
182
|
-
else
|
183
|
-
if piece_color(next_pos) == @@enemy_color
|
184
|
-
valid << next_pos
|
185
|
-
end
|
186
|
-
break
|
187
193
|
end
|
194
|
+
break
|
195
|
+
end
|
188
196
|
end
|
189
197
|
|
190
198
|
# Move left N places until board limit, piece in the way, or specified limit
|
191
199
|
left.times do |i|
|
192
|
-
|
193
|
-
|
200
|
+
next_pos = index - (i+1)
|
201
|
+
if unoccupied?(next_pos)
|
202
|
+
valid << next_pos
|
203
|
+
else
|
204
|
+
if piece_color(next_pos) == @@enemy_color
|
194
205
|
valid << next_pos
|
195
|
-
else
|
196
|
-
if piece_color(next_pos) == @@enemy_color
|
197
|
-
valid << next_pos
|
198
|
-
end
|
199
|
-
break
|
200
206
|
end
|
207
|
+
break
|
208
|
+
end
|
201
209
|
end
|
202
|
-
|
210
|
+
|
211
|
+
valid
|
203
212
|
end
|
204
213
|
|
205
214
|
|
@@ -271,7 +280,7 @@ module MOVE
|
|
271
280
|
end
|
272
281
|
end
|
273
282
|
|
274
|
-
|
283
|
+
valid
|
275
284
|
end
|
276
285
|
|
277
286
|
# Castle: king cannot move into check, or through check
|
@@ -279,70 +288,51 @@ module MOVE
|
|
279
288
|
valid = []
|
280
289
|
dangerous_tiles = attack_vectors
|
281
290
|
|
282
|
-
#
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
valid << index + 2
|
294
|
-
end
|
295
|
-
end
|
291
|
+
# King may never have moved
|
292
|
+
return valid unless [5, 61].include?(index) && @@pieces[index][:moved] == false
|
293
|
+
|
294
|
+
# Ensure empty space between a King and a Rook
|
295
|
+
if unoccupied?(index - 1) && unoccupied?(index - 2) && unoccupied?(index - 3) && @@pieces[index - 4][:moved] == false
|
296
|
+
# Ensure king does not move through check or into check, and then add its castle position
|
297
|
+
valid << index - 2 if !dangerous_tiles.include?(index - 1) && !dangerous_tiles.include?(index - 2)
|
298
|
+
end
|
299
|
+
|
300
|
+
if unoccupied?(index + 1) && unoccupied?(index + 2) && @@pieces[index + 3][:moved] == false
|
301
|
+
valid << index + 2 if !dangerous_tiles.include?(index + 1) && !dangerous_tiles.include?(index + 2)
|
296
302
|
end
|
297
|
-
|
303
|
+
|
304
|
+
valid
|
298
305
|
end
|
299
306
|
|
300
307
|
|
301
308
|
# Check if board tile currently has a piece
|
302
309
|
def unoccupied?(index)
|
303
|
-
|
304
|
-
return true
|
305
|
-
else
|
306
|
-
return false
|
307
|
-
end
|
310
|
+
@@pieces[index][:color] == nil ? true : false
|
308
311
|
end
|
309
312
|
|
310
|
-
|
311
313
|
# Return true if the piece has moved before
|
312
314
|
def moved?(index)
|
313
|
-
|
314
|
-
return true
|
315
|
-
else
|
316
|
-
return false
|
317
|
-
end
|
315
|
+
@@pieces[index][:moved] ? true : false
|
318
316
|
end
|
319
317
|
|
320
|
-
|
321
318
|
# Return piece color ("red" or "black") from index (1 - 64)
|
322
319
|
def piece_color(index)
|
323
|
-
|
320
|
+
@@pieces[index][:color]
|
324
321
|
end
|
325
322
|
|
326
|
-
|
327
323
|
# Method used when moving, to verify the piece at index (1 - 64) is not of type "king"
|
328
324
|
def not_king(index)
|
329
|
-
|
325
|
+
@@piece_locations[index][:type] == :king
|
330
326
|
end
|
331
327
|
|
332
|
-
|
333
328
|
# Obtain chess board row number (1 + 8) from an index (1 - 64)
|
334
329
|
def get_row_from_index(index)
|
335
|
-
|
330
|
+
(index - 1)/8 + 1
|
336
331
|
end
|
337
332
|
|
338
|
-
|
339
333
|
# Obtain chess board column number (1 - 8) from an index (1 - 64)
|
340
334
|
def get_col_from_index(index)
|
341
|
-
|
342
|
-
return 8
|
343
|
-
else
|
344
|
-
return index % 8
|
345
|
-
end
|
335
|
+
index % 8 == 0 ? 8 : index % 8
|
346
336
|
end
|
347
337
|
|
348
338
|
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
|
2
|
+
$LOAD_PATH << __FILE__ # '.'
|
3
|
+
|
4
|
+
require 'eventmachine'
|
5
|
+
require 'faye/websocket'
|
6
|
+
|
7
|
+
require_relative "terminal_chess/version"
|
8
|
+
require_relative "terminal_chess/messages"
|
9
|
+
require_relative "printer.rb"
|
10
|
+
require_relative "move.rb"
|
11
|
+
require_relative "board.rb"
|
12
|
+
|
13
|
+
|
14
|
+
class NetworkChessClient
|
15
|
+
def initialize(ngrok)
|
16
|
+
@board = Board.new
|
17
|
+
@turn_by_turn_playback = []
|
18
|
+
@game_started = false
|
19
|
+
@player_num = nil
|
20
|
+
@player_turn = false
|
21
|
+
@messages = []
|
22
|
+
@socket_url = "ws://#{ngrok}.ngrok.io"
|
23
|
+
start_client
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def start_client
|
29
|
+
Thread.new {
|
30
|
+
EM.run do
|
31
|
+
ws = Faye::WebSocket::Client.new(@socket_url)
|
32
|
+
|
33
|
+
ws.on :open do
|
34
|
+
p [:open]
|
35
|
+
end
|
36
|
+
|
37
|
+
ws.on :message do |msg|
|
38
|
+
p [:message, msg.data]
|
39
|
+
|
40
|
+
if msg.data.match "SETUP: You are player [1|2]"
|
41
|
+
@player_num = msg.data.split(' ').last.strip.to_i
|
42
|
+
@player_turn = true if @player_num == 1
|
43
|
+
@game_started = true
|
44
|
+
|
45
|
+
p [:local, "Awaiting opponent move"] if @player_num == 2
|
46
|
+
end
|
47
|
+
|
48
|
+
if msg.data.match "MOVE: [a-zA-Z][0-9], [a-zA-Z][0-9]"
|
49
|
+
from, to = msg.data.match("MOVE: \([a-zA-Z][0-9]\), \([a-zA-Z][0-9]\)").captures
|
50
|
+
|
51
|
+
@board.move(from, to)
|
52
|
+
@player_turn = true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
ws.on :close do |e|
|
57
|
+
p [:closed, e.code, e.reason]
|
58
|
+
ws = nil
|
59
|
+
EventMachine::stop_event_loop
|
60
|
+
end
|
61
|
+
|
62
|
+
EventMachine::PeriodicTimer.new(1) do
|
63
|
+
next unless @player_turn && @game_started
|
64
|
+
|
65
|
+
piece_moved = local_move
|
66
|
+
if piece_moved
|
67
|
+
@player_turn = false
|
68
|
+
ws.send "MOVE: #{@turn_by_turn_playback.last[0]}, #{@turn_by_turn_playback.last[1]}"
|
69
|
+
puts "Awaiting remote player move"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
}.join
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
def local_move
|
78
|
+
if @board.checkmate
|
79
|
+
puts "\nTurn by Turn Playback : #{@turn_by_turn_playback}\n"
|
80
|
+
exit
|
81
|
+
end
|
82
|
+
|
83
|
+
print "\nPiece to Move [#{@board.player_turn.capitalize}]: "
|
84
|
+
from = gets.chomp.upcase
|
85
|
+
|
86
|
+
begin
|
87
|
+
print "Valid destinations: #{@board.valid_destinations(from).join(", ")}"
|
88
|
+
|
89
|
+
print "\nLocation: "
|
90
|
+
to = gets.chomp.upcase
|
91
|
+
moved = @board.move(from, to)
|
92
|
+
|
93
|
+
return local_move() unless moved == Messages.piece_moved
|
94
|
+
|
95
|
+
rescue Exception => e
|
96
|
+
puts "Invalid selection #{e if !ENV["DEV"].nil?}"
|
97
|
+
else
|
98
|
+
@turn_by_turn_playback << [from, to]
|
99
|
+
true
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|