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 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