terminal_chess 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 34fa5a38bd74510d2289f5fec7141397fb691f5f
4
- data.tar.gz: f01641e3e28dbb1e3965a42a2a3da40078442d0c
3
+ metadata.gz: e6a657c0e4f1a9d9e7f1d0c9ee0c62b009f33fdc
4
+ data.tar.gz: e691068c1d7e0ba8c9f0a6e2092a8ac32e4c562b
5
5
  SHA512:
6
- metadata.gz: a9b484ad6b6058cf21c2f0feda9d35bd41285ddea65d7d1926bcfde9ccf10d77d2c0f9d1239d7bf62f1d6a0b37eb13fc7c5a24a893ebbb1c4c90636b2c9dcb40
7
- data.tar.gz: cb690106cc38935ac5528639d222e81d237367584b4c075abe848742ea11645fa35644334d6e38d98863bf1b79d6f63c344da344766aeff44f747b081427a003
6
+ metadata.gz: 27347b75a56f10cb3969257fac8295d948ced1e6002f545fd539a1890d5853c46d11b287c4721d8ca4d1385938b58d562280542f76222ce9e0f351f29e0d7de6
7
+ data.tar.gz: 65ce2d89d403db4310bd179b635b36a0a1d84cfc4ae513826ff17de30e1427bb546a06db39203d8152754398d7053a5ebe8c60b94228e120a9ea085d1c2fbee7
@@ -0,0 +1,38 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ terminal_chess (0.2.0)
5
+ colorize
6
+ em-websocket
7
+ eventmachine
8
+ faye-websocket
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ colorize (0.8.1)
14
+ em-websocket (0.5.1)
15
+ eventmachine (>= 0.12.9)
16
+ http_parser.rb (~> 0.6.0)
17
+ eventmachine (1.2.5)
18
+ faye-websocket (0.10.7)
19
+ eventmachine (>= 0.12.0)
20
+ websocket-driver (>= 0.5.1)
21
+ http_parser.rb (0.6.0)
22
+ minitest (5.8.4)
23
+ rake (10.5.0)
24
+ websocket-driver (0.7.0)
25
+ websocket-extensions (>= 0.1.0)
26
+ websocket-extensions (0.1.3)
27
+
28
+ PLATFORMS
29
+ ruby
30
+
31
+ DEPENDENCIES
32
+ bundler (~> 1.7)
33
+ minitest
34
+ rake (~> 10.0)
35
+ terminal_chess!
36
+
37
+ BUNDLED WITH
38
+ 1.16.0
data/README.md CHANGED
@@ -1,63 +1,87 @@
1
1
  Terminal-Chess
2
2
  ==============
3
3
 
4
- A two-player chess game for the Terminal, written in Ruby (output below is colorized when run in the terminal)
5
- <pre>
6
- >> Welcome to Terminal Chess v0.1.0
7
-
8
- _A__ _B__ _C__ _D__ _E__ _F__ _G__ _H__
9
- | || || || || || || || |
10
- 1 | || KN || BI || QU || KI || BI || KN || RO | 1
11
- |____||____||____||____||____||____||____||____|
12
- | || || || || || || || |
13
- 2 | || PA || PA || PA || PA || PA || PA || PA | 2
14
- |____||____||____||____||____||____||____||____|
15
- | || || || || || || || |
16
- 3 | || || || || RO || || || | 3
17
- |____||____||____||____||____||____||____||____|
18
- | || || || || || || || |
19
- 4 | PA || || || || || || || | 4
20
- |____||____||____||____||____||____||____||____|
21
- | || || || || || || || |
22
- 5 | PA || || || || || || || | 5
23
- |____||____||____||____||____||____||____||____|
24
- | || || || || || || || |
25
- 6 | || || || || || || || | 6
26
- |____||____||____||____||____||____||____||____|
27
- | || || || || || || || |
28
- 7 | || PA || PA || PA || PA || PA || PA || PA | 7
29
- |____||____||____||____||____||____||____||____|
30
- | || || || || || || || |
31
- 8 | RO || KN || BI || QU || KI || BI || KN || RO | 8
32
- |____||____||____||____||____||____||____||____|
33
- A B C D E F G H
34
-
35
- Piece to Move: B8
36
- Valid destinations: C6, A6
37
- Location: c6
38
-
39
- </pre>
40
-
41
- ![Screenshot](http://at1as.github.io/github_repo_assets/terminal_chess.jpg)
42
-
43
- ### Requirements
44
- Requires the colorize Ruby gem (listed in .gemspec file)
45
- ```bash
46
- $ sudo gem install colorize
47
- ```
4
+ A lightweight two-player chess game for the Terminal, written in Ruby
5
+
6
+ ![Screenshot](http://at1as.github.io/github_repo_assets/terminal_chess-2.png)
7
+
48
8
 
49
- ### Usage
50
- The easiest way to use terminal_chess is to install it via the [Rubygem](https://rubygems.org/gems/terminal_chess). This is likely to be a few commits behind, but generally more stable.
9
+ ### Installing
10
+
11
+ The easiest way to use terminal_chess is to install it via the [Rubygem](https://rubygems.org/gems/terminal_chess). Note that the Gem is *way* out of date when compared to the repo
51
12
 
52
13
  Otherwise, clone this repo directly and run:
53
- ```bash
14
+
15
+ ```bash
16
+ # Download Repo
54
17
  $ git clone git@github.com:at1as/Terminal-Chess.git
55
18
  $ chmod +x lib/terminal_chess.rb
19
+
20
+ # Install Dependencies. Only dependency is the colorize gem
21
+ $ bundle install
22
+ ```
23
+
24
+ ### Running
25
+
26
+ #### Local Gameplay
27
+
28
+ In this mode, the game will launch in Terminal and allow the player to make moves for both sides of the board
29
+
30
+ ```
31
+ # Run program in terminal
32
+
56
33
  $ ruby lib/terminal_chess.rb
57
34
  ```
58
35
 
59
- ### Limitations
60
- * Built and tested on Terminal in OS 10.10
61
- * For now, checkmate will need to be verified manually
62
- * TODO: Code cleanup. Printer module is painful to read.
63
- * Niether player can be automated
36
+ #### Multiplayer
37
+
38
+ Terminal Chess can connect to an opponent using websockets over ngrok. The requires first starting the server:
39
+
40
+ ```
41
+ # The webserver must be running either on one of the players machines
42
+ # Or somewhere else. This will need to be running before either client can connect
43
+ $ lib/server.rb
44
+
45
+ # The host running the server will need to tunnel the connection through ngrok
46
+ # on the free plan the URL will change every time ngrok is launched
47
+ # therefor the `9cf13f35` component will need to be passed to the clients in order
48
+ # to connect
49
+ $ ngrok http 4567
50
+ => http://9cf13f35.ngrok.io -> localhost:4567
51
+ ```
52
+
53
+ And then the client can connect via the NGROK environment variable. if this environment variable is set, the client will attempt to start a session over the network. If the connection
54
+
55
+ ```
56
+ # Replace the NGROK enviroment variable with whatever URL the ngrok server returned
57
+ $ NGROK=9cf13f5 ruby lib/terminal_chess.rb
58
+
59
+ # => [:message, "INFO: Awaiting second player..."]
60
+ ```
61
+
62
+ And once the second client connects, game on!
63
+
64
+ ```
65
+ $ NGROK=9cf13f5 ruby lib/terminal_chess.rb
66
+
67
+ # => [:message, "INFO: Connected to remote player"]
68
+ # => [:message, "SETUP: You are player 2"]
69
+ # => [:message, "INFO: Starting game!"]
70
+ ```
71
+
72
+ Any subsequent clients that attempt to connect while the game is in session will have their connections dropped
73
+
74
+ ### Testing:
75
+
76
+ ```bash
77
+ $ bundle exec rake test --trace
78
+ ```
79
+
80
+ ### Notes
81
+ * Built and tested on macOS 10.11 with Ruby 2.4.0
82
+ * Neither player can currently be automated
83
+
84
+ ### TODO:
85
+ * Update Gem to reflect the last two years' repo changes...
86
+ * Automate one of two players (note: tried this. Not easy to make it competent)
87
+ * Switch written Chess pieces to unicode characters (note: tried this. Didn't look great)
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ load 'printer.rb'
4
+ load 'move.rb'
5
+ load 'terminal_chess/messages.rb'
6
+ require 'colorize'
7
+
8
+ class Board
9
+
10
+ include Printer
11
+ include Move
12
+
13
+ attr_reader :piece_locations, :checkmate, :player_turn
14
+ attr_reader :taken_pieces
15
+
16
+ alias piece_manifest piece_locations
17
+
18
+
19
+ def initialize
20
+ @piece_locations_buffer = Hash.new
21
+ @piece_locations = Hash.new
22
+ @row_mappings = Hash[("A".."H").zip(1..8)]
23
+ @taken_pieces = Array.new
24
+ @player_turn = :black
25
+ @checkmate = false
26
+
27
+ setup_board
28
+ end
29
+
30
+ # Game logic
31
+ def move(p1, p2)
32
+
33
+ manifest = piece_manifest()
34
+
35
+ p1 = get_index_from_rowcol(p1.to_s)
36
+ p2 = get_index_from_rowcol(p2.to_s)
37
+
38
+ # Find valid positions and subtract king current position as nobody can directly take king piece
39
+ valid_positions = possible_moves(p1, manifest, true)
40
+ valid_positions -= king_positions
41
+
42
+ # If player is moving out of turn, display message
43
+ # `return p ...` is so we print value and return it from the function
44
+ # this is so the unit tests can get the value directly. There are better ways to do this
45
+ unless @player_turn == @piece_locations[p1][:color]
46
+ return p "It is #{@player_turn}'s turn. Please move a #{@player_turn} piece."
47
+ end
48
+
49
+ return p "Please select a valid destination." unless valid_positions.include?(p2)
50
+
51
+ create_board_after_piece_moved(p1, p2)
52
+
53
+ # If player is in check with the new proposed board, disallow the movement
54
+ if check?(@player_turn, @piece_locations_buffer)
55
+ return p "Please move #{@player_turn} king out of check to continue"
56
+ end
57
+
58
+ # At this point, the move appears to be valid
59
+ @taken_pieces << @piece_locations[p2] unless @piece_locations[p2][:type].nil?
60
+
61
+ # Check for Pawn Promotion (if pawn reaches end of the board, promote it)
62
+ if @piece_locations_buffer[p2][:type] == :pawn
63
+ if p2 < 9 && @piece_locations_buffer[p2][:color] == :red
64
+ promote(p2)
65
+ elsif p2 > 56 && @piece_locations_buffer[p2][:color] == :black
66
+ promote(p2)
67
+ end
68
+ end
69
+
70
+ # Check for Castling - https://en.m.wikipedia.org/wiki/Castling
71
+ if @piece_locations_buffer[p2][:type] == :king && (p2 - p1).abs == 2
72
+
73
+ p2 < 9 ? y_offset = 0 : y_offset = 56
74
+
75
+ if p2 > p1
76
+ @piece_locations_buffer[6+y_offset] = @piece_locations_buffer[8+y_offset]
77
+ @piece_locations_buffer[8+y_offset] = {
78
+ :type => nil,
79
+ :number => nil,
80
+ :color => nil
81
+ }
82
+ else
83
+ @piece_locations_buffer[4+y_offset] = @piece_locations_buffer[1+y_offset]
84
+ @piece_locations_buffer[1+y_offset] = {
85
+ :type => nil,
86
+ :number => nil,
87
+ :color => nil
88
+ }
89
+ end
90
+ end
91
+
92
+ commit_board_piece_movement
93
+
94
+ if (winner = player_in_checkmate(@piece_locations))
95
+ return p Messages.black_winner if winner == :black
96
+ return p Messages.red_winner if winner == :red
97
+ end
98
+
99
+ return p Messages.piece_moved
100
+ end
101
+
102
+
103
+ def commit_board_piece_movement
104
+ @piece_locations = @piece_locations_buffer
105
+ @player_turn = opposing_color(@player_turn)
106
+ display_board
107
+ end
108
+
109
+
110
+ def create_board_after_piece_moved(p1, p2)
111
+ # Store state of board after proposed move in @piece_locations_buffer
112
+ @piece_locations_buffer = @piece_locations.clone
113
+ @piece_locations_buffer[p2] = @piece_locations_buffer[p1]
114
+ @piece_locations_buffer[p2][:moved] = true
115
+ @piece_locations_buffer[p1] = {
116
+ :type => nil,
117
+ :number => nil,
118
+ :color => nil
119
+ }
120
+ end
121
+
122
+
123
+ # Return the valid positions for piece at current_pos to move in readable format [A-H][1-8]
124
+ def valid_destinations(current_pos)
125
+ readable_positions = []
126
+ manifest = piece_manifest
127
+ p1 = get_index_from_rowcol(current_pos.to_s)
128
+
129
+ valid_positions = possible_moves(p1, manifest, true)
130
+
131
+ valid_positions.each do |pos|
132
+ grid_pos = get_rowcol_from_index(pos)
133
+ # Map first string character 1-8 to [A-H], for column, and then add second string character as [1-8]
134
+ readable_positions << (@row_mappings.key(grid_pos[0].to_i) + grid_pos[1].to_s)
135
+ end
136
+
137
+ readable_positions.sort
138
+ end
139
+
140
+
141
+ # Search piece manifest for kings. Remove them from the list of positions returned
142
+ # from the Move module (so that players cannot take the "king" type piece)
143
+ def king_positions
144
+ king_locations = []
145
+
146
+ @piece_locations.each do |piece, details|
147
+ king_locations << piece if details.fetch(:type) == :king
148
+ end
149
+
150
+ king_locations
151
+ end
152
+
153
+
154
+ # Once a pawn reaches the end, this method is called to swap the pawn
155
+ # for another piece (from the list below)
156
+ def promote(p1)
157
+ puts "Promote to: [Q]ueen, [K]night, [R]ook, [B]ishop"
158
+
159
+ loop do
160
+ promo_piece = gets.chomp.downcase
161
+
162
+ if promo_piece == "q" || promo_piece == :queen
163
+ @piece_locations_buffer[p1][:type] = :queen
164
+ break
165
+
166
+ elsif promo_piece == "k" || promo_piece == :knight
167
+ @piece_locations_buffer[p1][:type] = :knight
168
+ break
169
+
170
+ elsif promo_piece == "r" || promo_piece == :rook
171
+ @piece_locations_buffer[p1][:type] = :rook
172
+ break
173
+
174
+ elsif promo_piece == "b" || promo_piece == :bishop
175
+ @piece_locations_buffer[p1][:type] = :bishop
176
+ break
177
+
178
+ else
179
+ puts "Please enter one of: [Q]ueen, [K]night, [R]ook, [B]ishop"
180
+ end
181
+ end
182
+ end
183
+
184
+ private :promote
185
+
186
+
187
+ def player_in_checkmate(manifest = @piece_locations)
188
+ return :red if checkmate?(:black, manifest)
189
+ return :black if checkmate?(:red, manifest)
190
+ end
191
+
192
+ def checkmate?(color, manifest)
193
+ check?(color, manifest, recurse_for_checkmate=true) && @checkmate
194
+ end
195
+
196
+ # Return whether the player of a specified color has their king currently in check
197
+ # by checking the attack vectors of all the opponents players against the king location
198
+ # Also, check whether king currently in check, has all of their valid moves within
199
+ # their opponents attack vectors, and therefore are in checkmate (@checkmate)
200
+ def check?(color, proposed_manifest = @piece_locations, recurse_for_checkmate = false)
201
+
202
+ enemy_attack_vectors = {}
203
+ player_attack_vectors = {}
204
+
205
+ king_loc = []
206
+ enemy_color = opposing_color(color)
207
+
208
+ proposed_manifest.each do |piece, details|
209
+
210
+ if details[:color] == enemy_color
211
+ enemy_attack_vectors[piece] = possible_moves(piece, proposed_manifest)
212
+
213
+ elsif details[:color] == color
214
+ begin
215
+ player_attack_vectors[piece] = possible_moves(piece, proposed_manifest)
216
+ rescue
217
+ # TODO: Fix possible_moves() so it doesn't throw exceptions
218
+ # This happens because it is searching board for where pieces
219
+ # will be, as as a result some pieces are nil
220
+ end
221
+ end
222
+
223
+ king_loc = piece if details[:color] == color && details[:type] == :king
224
+ end
225
+
226
+ danger_vector = enemy_attack_vectors.values.flatten.uniq
227
+ defence_vector = player_attack_vectors.values.flatten.uniq
228
+ king_positions = possible_moves(king_loc, proposed_manifest)
229
+
230
+ # The King is in the attackable locations by the opposing player
231
+ return false unless danger_vector.include? king_loc
232
+
233
+ # If all the positions the king piece can move to is also attackable by the opposing player
234
+ if recurse_for_checkmate && ((king_positions - danger_vector).length == 0)
235
+
236
+ is_in_check = []
237
+ player_attack_vectors.each do |piece_index, piece_valid_moves|
238
+ piece_valid_moves.each do |possible_new_location|
239
+
240
+ # Check if board is still in check after piece moves to its new location
241
+ @new_piece_locations = @piece_locations.clone
242
+ @new_piece_locations[possible_new_location] = @new_piece_locations[piece_index]
243
+ @new_piece_locations[piece_index] = {
244
+ :type => nil,
245
+ :number => nil,
246
+ :color => nil
247
+ }
248
+
249
+ is_in_check << check?(color, @new_piece_locations)
250
+ end
251
+ end
252
+
253
+ if is_in_check.include?(false)
254
+ return false
255
+ else
256
+ @checkmate = true
257
+ end
258
+ end
259
+
260
+ true
261
+ end
262
+
263
+
264
+ def opposing_color(player_color)
265
+ ([:black, :red] - [player_color]).first
266
+ end
267
+
268
+ # Board spaces that are attackable by opposing pieces
269
+ # TODO: check? method should use this function
270
+ def attack_vectors(color = @player_turn, proposed_manifest = @piece_locations)
271
+ enemy_color = opposing_color(color)
272
+ kill_zone = Array.new
273
+
274
+ proposed_manifest.each do |piece, details|
275
+ kill_zone << possible_moves(piece, proposed_manifest) if details.fetch(:color) == enemy_color
276
+ end
277
+
278
+ kill_zone.flatten.uniq
279
+ end
280
+
281
+
282
+ # Reprint the board. Called after every valid piece move
283
+ def display_board
284
+ print_board @piece_locations
285
+ end
286
+
287
+
288
+ # Convert index [A-H][1-8] => (1 - 64)
289
+ def get_index_from_rowcol(row_col)
290
+ (row_col[1].to_i - 1) * 8 + @row_mappings.fetch(row_col[0]).to_i
291
+ end
292
+
293
+
294
+ # Convert index (1 - 64) => [A-H][1-8]
295
+ def get_rowcol_from_index(index)
296
+ letter = get_col_from_index(index)
297
+ number = get_row_from_index(index)
298
+
299
+ "#{letter}#{number}"
300
+ end
301
+
302
+
303
+ def setup_board
304
+ # Intial setup of board. Put pieces into the expected locations
305
+ setup_empty_tiles
306
+ place_player_pieces(:black)
307
+ place_player_pieces(:red)
308
+ end
309
+
310
+
311
+ def setup_empty_tiles
312
+ # Initialize chess board tiles without any pieces
313
+ (1..64).each do |tile_num|
314
+ @piece_locations[tile_num] = {
315
+ :type => nil,
316
+ :number => nil,
317
+ :color => nil
318
+ }
319
+ end
320
+ end
321
+
322
+
323
+ def place_player_pieces(color)
324
+ # Place pieces on chess board
325
+ place_first_row(color)
326
+ place_pawns(color)
327
+ end
328
+
329
+
330
+ def place_pawns(color)
331
+ offset = color == :black ? 8 : 48
332
+
333
+ (1..8).each do |piece_num|
334
+ @piece_locations[piece_num + offset] = {
335
+ :type => :pawn,
336
+ :number => piece_num,
337
+ :color => color,
338
+ :moved => false
339
+ }
340
+ end
341
+ end
342
+
343
+
344
+ def place_first_row(color)
345
+ row_offset = color == :black ? 0 : 56
346
+
347
+ @piece_locations[row_offset + 1] = {:type => :rook, :number => 1, :color => color, :moved => false}
348
+ @piece_locations[row_offset + 2] = {:type => :knight, :number => 1, :color => color, :moved => false}
349
+ @piece_locations[row_offset + 3] = {:type => :bishop, :number => 1, :color => color, :moved => false}
350
+ @piece_locations[row_offset + 4] = {:type => :queen, :number => 1, :color => color, :moved => false}
351
+ @piece_locations[row_offset + 5] = {:type => :king, :number => 1, :color => color, :moved => false}
352
+ @piece_locations[row_offset + 6] = {:type => :bishop, :number => 2, :color => color, :moved => false}
353
+ @piece_locations[row_offset + 7] = {:type => :knight, :number => 2, :color => color, :moved => false}
354
+ @piece_locations[row_offset + 8] = {:type => :rook, :number => 2, :color => color, :moved => false}
355
+ end
356
+
357
+ end