terminal_chess 0.1.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.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +63 -0
- data/Rakefile +11 -0
- data/bin/terminal_chess +3 -0
- data/lib/Board.rb +273 -0
- data/lib/move.rb +349 -0
- data/lib/printer.rb +77 -0
- data/lib/terminal_chess.rb +30 -0
- data/lib/terminal_chess/version.rb +3 -0
- data/terminal_chess.gemspec +24 -0
- metadata +97 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1525bec00c6def643f46e3da0d7b74380dfe7c8e
|
4
|
+
data.tar.gz: 5a12c4bad7a2aa61e67fb5146428041ce781c9c3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 27593e358634d5d7bc01415d3eda57aa4c3358116475620a7ae78ab55352ee63536bc771b32c42a7f747b4370c558e013ab73c45c93818de313ec7f4bf501274
|
7
|
+
data.tar.gz: 15641a90f654dfcb15a1887784227d8301abde058e8060a3ded4d4bb03aaae8c3e321f7345121db4d13a34c8d26e86218c05fbb8f28ab2f77a38ea7d5666e643
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 Jason Willems
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
Terminal-Chess
|
2
|
+
==============
|
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
|
+

|
42
|
+
|
43
|
+
### Requirements
|
44
|
+
Requires the colorize Ruby gem (listed in .gemspec file)
|
45
|
+
```bash
|
46
|
+
$ sudo gem install colorize
|
47
|
+
```
|
48
|
+
|
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.
|
51
|
+
|
52
|
+
Otherwise, clone this repo directly and run:
|
53
|
+
```bash
|
54
|
+
$ git clone git@github.com:at1as/Terminal-Chess.git
|
55
|
+
$ chmod +x lib/terminal_chess.rb
|
56
|
+
$ ruby lib/terminal_chess.rb
|
57
|
+
```
|
58
|
+
|
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
|
data/Rakefile
ADDED
data/bin/terminal_chess
ADDED
data/lib/Board.rb
ADDED
@@ -0,0 +1,273 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
load 'printer.rb'
|
4
|
+
load 'move.rb'
|
5
|
+
require 'colorize'
|
6
|
+
|
7
|
+
class Board
|
8
|
+
|
9
|
+
include PRINTER
|
10
|
+
include MOVE
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@@piece_locations = Hash.new
|
14
|
+
@@piece_locations_buffer = Hash.new
|
15
|
+
@@row_mappings = Hash[("A".."H").zip(1..8)]
|
16
|
+
@@taken_pieces = []
|
17
|
+
@@player_turn = "black"
|
18
|
+
@@checkmate = false
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
# Game logic
|
23
|
+
def move(p1, p2)
|
24
|
+
manifest = piece_manifest
|
25
|
+
p1 = get_index_from_rowcol(p1.to_s)
|
26
|
+
p2 = get_index_from_rowcol(p2.to_s)
|
27
|
+
valid_positions = possible_moves(p1, manifest, true)
|
28
|
+
|
29
|
+
##Subtract king current position from valid positions
|
30
|
+
valid_positions -= king_positions
|
31
|
+
|
32
|
+
# Allow piece movements, unless in checkmate
|
33
|
+
if !@@checkmate
|
34
|
+
# Ensure player is moving in turn
|
35
|
+
if @@player_turn == @@piece_locations[p1]["color"]
|
36
|
+
|
37
|
+
@@piece_locations_buffer = @@piece_locations.clone
|
38
|
+
|
39
|
+
# Chosen destination is within the list of valid destinations
|
40
|
+
if ([p2] - valid_positions).empty?
|
41
|
+
|
42
|
+
@@piece_locations_buffer[p2] = @@piece_locations_buffer[p1]
|
43
|
+
@@piece_locations_buffer[p2]["moved"] = true
|
44
|
+
@@piece_locations_buffer[p1] = {"type" => " ", "number" => nil, "color" => nil}
|
45
|
+
|
46
|
+
# If the current player is not in check at the end of the turn, allow them to proceed
|
47
|
+
if !check?(@@player_turn, @@piece_locations_buffer)
|
48
|
+
|
49
|
+
@@taken_pieces << @@piece_locations[p2] if !@@piece_locations[p2]["number"].nil?
|
50
|
+
|
51
|
+
# Check for Pawn Promotion (if pawn reaches end of the board, promote it)
|
52
|
+
if @@piece_locations_buffer[p2]["type"] == "pawn"
|
53
|
+
if p2 < 9 && @@piece_locations_buffer[p2]["color"] == "red"
|
54
|
+
promote(p2)
|
55
|
+
elsif p2 > 56 && @@piece_locations_buffer[p2]["color"] == "black"
|
56
|
+
promote(p2)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Check for Castling
|
61
|
+
if @@piece_locations_buffer[p2]["type"] == "king" && (p2 - p1).abs == 2
|
62
|
+
|
63
|
+
p2 < 9 ? y_offset = 0 : y_offset = 56
|
64
|
+
|
65
|
+
if p2 > p1
|
66
|
+
@@piece_locations_buffer[6+y_offset] = @@piece_locations_buffer[8+y_offset]
|
67
|
+
@@piece_locations_buffer[8+y_offset] = {"type" => " ", "number" => nil, "color" => nil}
|
68
|
+
else
|
69
|
+
@@piece_locations_buffer[4+y_offset] = @@piece_locations_buffer[1+y_offset]
|
70
|
+
@@piece_locations_buffer[1+y_offset] = {"type" => " ", "number" => nil, "color" => nil}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Clean Up
|
75
|
+
@@piece_locations = @@piece_locations_buffer
|
76
|
+
@@player_turn = (["black", "red"] - [@@player_turn]).first
|
77
|
+
board_refresh
|
78
|
+
else
|
79
|
+
puts "Please move #{@@player_turn} king out of check to continue"
|
80
|
+
end
|
81
|
+
|
82
|
+
else
|
83
|
+
puts "Please select a valid destination."
|
84
|
+
end
|
85
|
+
else
|
86
|
+
puts "It is #{@@player_turn}'s turn. Please move a #{@@player_turn} piece."
|
87
|
+
end
|
88
|
+
else
|
89
|
+
puts "Checkmate! Game Over."
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Return the valid positions for piece at current_pos to move in readable format [A-H][1-8]
|
94
|
+
def valid_destinations(current_pos)
|
95
|
+
readable_positions = []
|
96
|
+
manifest = piece_manifest
|
97
|
+
p1 = get_index_from_rowcol(current_pos.to_s)
|
98
|
+
|
99
|
+
valid_positions = possible_moves(p1, manifest, true)
|
100
|
+
valid_positions.each do |pos|
|
101
|
+
grid_pos = get_rowcol_from_index(pos)
|
102
|
+
# Map first string character 1-8 to [A-H], for column, and then add second string character as [1-8]
|
103
|
+
readable_positions << (@@row_mappings.key(grid_pos[0].to_i) + grid_pos[1].to_s)
|
104
|
+
end
|
105
|
+
return readable_positions.sort
|
106
|
+
end
|
107
|
+
|
108
|
+
# Search piece manifest for kings. Remove them from the list of positions returned
|
109
|
+
# from the MOVE module (so that players cannot take the "king" type piece)
|
110
|
+
def king_positions
|
111
|
+
king_locations = []
|
112
|
+
@@piece_locations.each do |piece, details|
|
113
|
+
if details["type"] == "king"
|
114
|
+
king_locations << piece
|
115
|
+
end
|
116
|
+
end
|
117
|
+
return king_locations
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
# Once a pawn reaches the end, this method is called to swap the pawn
|
122
|
+
# for another piece (from the list below)
|
123
|
+
def promote(p1)
|
124
|
+
puts "Promote to: [Q]ueen, [K]night, [R]ook, [B]ishop"
|
125
|
+
while true
|
126
|
+
promo_piece = gets.chomp.downcase
|
127
|
+
if promo_piece == "q" || promo_piece == "queen"
|
128
|
+
@@piece_locations_buffer[p1]["type"] = "queen"
|
129
|
+
break
|
130
|
+
elsif promo_piece == "k" || promo_piece == "knight"
|
131
|
+
@@piece_locations_buffer[p1]["type"] = "knight"
|
132
|
+
break
|
133
|
+
elsif promo_piece == "r" || promo_piece == "rook"
|
134
|
+
@@piece_locations_buffer[p1]["type"] = "rook"
|
135
|
+
break
|
136
|
+
elsif promo_piece == "b" || promo_piece == "bishop"
|
137
|
+
@@piece_locations_buffer[p1]["type"] = "bishop"
|
138
|
+
break
|
139
|
+
else
|
140
|
+
puts "Please enter one of: [Q]ueen, [K]night, [R]ook, [B]ishop"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
private :promote
|
146
|
+
|
147
|
+
# Return whether the player of a specified color has their king currently in check
|
148
|
+
# by checking the attack vectors of all the opponents players, versus the king location
|
149
|
+
# Also, check whether king currently in check, has all of their valid moves within
|
150
|
+
# their opponents attack vectors, and therefore are in checkmate (@@checkmate)
|
151
|
+
def check?(color, proposed_manifest = @@piece_locations)
|
152
|
+
path, king_loc = [], []
|
153
|
+
enemy_color = (["black", "red"] - ["#{color}"]).first
|
154
|
+
|
155
|
+
proposed_manifest.each do |piece, details|
|
156
|
+
if details["color"] == enemy_color
|
157
|
+
path << possible_moves(piece, proposed_manifest)
|
158
|
+
end
|
159
|
+
if details["color"] == color && details["type"] == "king"
|
160
|
+
king_loc = piece
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
danger_vector = path.flatten.uniq
|
165
|
+
king_positions = possible_moves(king_loc, proposed_manifest)
|
166
|
+
|
167
|
+
# If the King is in the attackable locations for the opposing player
|
168
|
+
if danger_vector.include? king_loc
|
169
|
+
# If all the positions the can move to is also attackable by the opposing player
|
170
|
+
if (king_positions - danger_vector).length != 0
|
171
|
+
# This is flawed. It verified whether the king could move out check
|
172
|
+
# There are two other cases: whether a piece can remove the enemy
|
173
|
+
# And whether the enemy's attack vector can be blocked
|
174
|
+
#@@checkmate = true
|
175
|
+
end
|
176
|
+
return true
|
177
|
+
# Piece not in check
|
178
|
+
else
|
179
|
+
return false
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
# Board spaces that are attackable by opposing pieces
|
185
|
+
# TODO: check? method should use this function
|
186
|
+
def attack_vectors(color = @@player_turn, proposed_manifest = @@piece_locations)
|
187
|
+
enemy_color = (["black", "red"] - ["#{color}"]).first
|
188
|
+
kill_zone = []
|
189
|
+
|
190
|
+
proposed_manifest.each do |piece, details|
|
191
|
+
if details["color"] == enemy_color
|
192
|
+
kill_zone << possible_moves(piece, proposed_manifest)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
kill_zone.flatten.uniq
|
197
|
+
end
|
198
|
+
|
199
|
+
|
200
|
+
# Reprint the board. Called after every valid piece move
|
201
|
+
def board_refresh
|
202
|
+
print_board(@@piece_locations)
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
# Convert index [A-H][1-8] => (1 - 64)
|
207
|
+
def get_index_from_rowcol(row_col)
|
208
|
+
offset = @@row_mappings[row_col[0]].to_i
|
209
|
+
multiplier = row_col[1].to_i - 1
|
210
|
+
return multiplier * 8 + offset
|
211
|
+
end
|
212
|
+
|
213
|
+
|
214
|
+
# Convert index (1 - 64) => [A-H][1-8]
|
215
|
+
def get_rowcol_from_index(index)
|
216
|
+
letter = get_col_from_index(index)
|
217
|
+
number = get_row_from_index(index)
|
218
|
+
"#{letter}#{number}"
|
219
|
+
end
|
220
|
+
|
221
|
+
|
222
|
+
# Intial setup of board. Put pieces into the expected locations
|
223
|
+
def setup_board
|
224
|
+
(1..64).each do |location|
|
225
|
+
@@piece_locations[location] = {"type" => " ", "number" => nil, "color" => nil}
|
226
|
+
end
|
227
|
+
|
228
|
+
# Black Pieces
|
229
|
+
@@piece_locations[1] = {"type" => "rook", "number" => 1, "color" => "black", "moved" => false}
|
230
|
+
@@piece_locations[2] = {"type" => "knight", "number" => 1, "color" => "black", "moved" => false}
|
231
|
+
@@piece_locations[3] = {"type" => "bishop", "number" => 1, "color" => "black", "moved" => false}
|
232
|
+
@@piece_locations[4] = {"type" => "queen", "number" => 1, "color" => "black", "moved" => false}
|
233
|
+
@@piece_locations[5] = {"type" => "king", "number" => 1, "color" => "black", "moved" => false}
|
234
|
+
@@piece_locations[6] = {"type" => "bishop", "number" => 2, "color" => "black", "moved" => false}
|
235
|
+
@@piece_locations[7] = {"type" => "knight", "number" => 2, "color" => "black", "moved" => false}
|
236
|
+
@@piece_locations[8] = {"type" => "rook", "number" => 2, "color" => "black", "moved" => false}
|
237
|
+
(1..8).each do |col|
|
238
|
+
@@piece_locations[col + 8] = {"type" => "pawn", "number" => col, "color" => "black", "moved" => false}
|
239
|
+
end
|
240
|
+
|
241
|
+
# White Pieces
|
242
|
+
@@piece_locations[57] = {"type" => "rook", "number" => 1, "color" => "red", "moved" => false}
|
243
|
+
@@piece_locations[58] = {"type" => "knight", "number" => 1, "color" => "red", "moved" => false}
|
244
|
+
@@piece_locations[59] = {"type" => "bishop", "number" => 1, "color" => "red", "moved" => false}
|
245
|
+
@@piece_locations[60] = {"type" => "queen", "number" => 1, "color" => "red", "moved" => false}
|
246
|
+
@@piece_locations[61] = {"type" => "king", "number" => 1, "color" => "red", "moved" => false}
|
247
|
+
@@piece_locations[62] = {"type" => "bishop", "number" => 2, "color" => "red", "moved" => false}
|
248
|
+
@@piece_locations[63] = {"type" => "knight", "number" => 2, "color" => "red", "moved" => false}
|
249
|
+
@@piece_locations[64] = {"type" => "rook", "number" => 2, "color" => "red", "moved" => false}
|
250
|
+
(1..8).each do |col|
|
251
|
+
@@piece_locations[col + 48] = {"type" => "pawn", "number" => col, "color" => "red", "moved" => false}
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
|
256
|
+
def piece_manifest
|
257
|
+
return @@piece_locations
|
258
|
+
end
|
259
|
+
|
260
|
+
# Not currently used
|
261
|
+
def taken_pieces
|
262
|
+
return @@taken_pieces
|
263
|
+
end
|
264
|
+
|
265
|
+
def checkmate?
|
266
|
+
return @@checkmate
|
267
|
+
end
|
268
|
+
|
269
|
+
def player_turn
|
270
|
+
return @@player_turn
|
271
|
+
end
|
272
|
+
|
273
|
+
end
|
data/lib/move.rb
ADDED
@@ -0,0 +1,349 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module MOVE
|
4
|
+
|
5
|
+
def constants(piece_mapping, color, piece)
|
6
|
+
@@pieces = piece_mapping
|
7
|
+
@@color = color
|
8
|
+
@@enemy_color = (["black", "red"] - ["#{color.downcase}"]).first
|
9
|
+
@@type = piece
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
# Calls methods below to return a list of positions which are valid moves
|
14
|
+
# for piece at index p1, given the current board layout as defined in manifest
|
15
|
+
def possible_moves(p1, manifest, castling = false)
|
16
|
+
allowed = []
|
17
|
+
type = manifest[p1]["type"]
|
18
|
+
my_color = manifest[p1]["color"]
|
19
|
+
constants(manifest, my_color, type)
|
20
|
+
|
21
|
+
unless unoccupied?(p1)
|
22
|
+
|
23
|
+
if type == "king"
|
24
|
+
allowed += [move_lateral(p1, 1)].flatten
|
25
|
+
allowed += [move_diagonal(p1, 1)].flatten
|
26
|
+
allowed += [castle(p1)].flatten if castling
|
27
|
+
|
28
|
+
elsif type == "queen"
|
29
|
+
allowed += [move_lateral(p1)].flatten
|
30
|
+
allowed += [move_diagonal(p1)].flatten
|
31
|
+
|
32
|
+
elsif type == "rook"
|
33
|
+
allowed += [move_lateral(p1)].flatten
|
34
|
+
|
35
|
+
elsif type == "bishop"
|
36
|
+
allowed += [move_diagonal(p1)].flatten
|
37
|
+
|
38
|
+
elsif type == "pawn"
|
39
|
+
allowed += [pawn(p1)].flatten
|
40
|
+
|
41
|
+
elsif type == "knight"
|
42
|
+
allowed += [knight(p1)].flatten
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
return allowed
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
# Returns all valid positions a pawn at index p1 can move to
|
51
|
+
def pawn(p1)
|
52
|
+
row = get_row_from_index(p1)
|
53
|
+
col = get_col_from_index(p1)
|
54
|
+
valid = []
|
55
|
+
|
56
|
+
# Piece color defines direction of travel. Enemy presence defines
|
57
|
+
# the validity of diagonal movements
|
58
|
+
if @@color == "red"
|
59
|
+
if unoccupied?(p1 - 8)
|
60
|
+
valid << (p1 - 8)
|
61
|
+
end
|
62
|
+
if piece_color(p1 - 7) == @@enemy_color && col < 8
|
63
|
+
valid << (p1 - 7)
|
64
|
+
end
|
65
|
+
if piece_color(p1 - 9) == @@enemy_color && col > 1
|
66
|
+
valid << (p1 - 9)
|
67
|
+
end
|
68
|
+
# Only if the pieces is unmoved, can it move forward two rows
|
69
|
+
if !@@pieces[p1]["moved"] && unoccupied?(p1 - 16) && unoccupied?(p1 - 8)
|
70
|
+
valid << (p1 - 16)
|
71
|
+
end
|
72
|
+
elsif @@color == "black"
|
73
|
+
if unoccupied?(p1 + 8)
|
74
|
+
valid << (p1 + 8)
|
75
|
+
end
|
76
|
+
if piece_color(p1 + 7) == @@enemy_color && col > 1
|
77
|
+
valid << (p1 + 7)
|
78
|
+
end
|
79
|
+
if piece_color(p1 + 9) == @@enemy_color && col < 8
|
80
|
+
valid << (p1 + 9)
|
81
|
+
end
|
82
|
+
if !@@pieces[p1]["moved"] && unoccupied?(p1 + 16) && unoccupied?(p1 + 8)
|
83
|
+
valid << (p1 + 16)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
return valid
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
# Returns valid positions a knight at index p1 can move to
|
92
|
+
def knight(p1)
|
93
|
+
row = get_row_from_index(p1)
|
94
|
+
col = get_col_from_index(p1)
|
95
|
+
valid = []
|
96
|
+
valid_no_ff = []
|
97
|
+
|
98
|
+
# Valid knight moves based on its board position
|
99
|
+
if row < 7 && col < 8
|
100
|
+
valid << (p1 + 17)
|
101
|
+
end
|
102
|
+
if row < 7 && col > 1
|
103
|
+
valid << (p1 + 15)
|
104
|
+
end
|
105
|
+
if row < 8 && col < 7
|
106
|
+
valid << (p1 + 10)
|
107
|
+
end
|
108
|
+
if row < 8 && col > 2
|
109
|
+
valid << (p1 + 6)
|
110
|
+
end
|
111
|
+
if row > 1 && col < 7
|
112
|
+
valid << (p1 - 6)
|
113
|
+
end
|
114
|
+
if row > 1 && col > 2
|
115
|
+
valid << (p1 - 10)
|
116
|
+
end
|
117
|
+
if row > 2 && col < 8
|
118
|
+
valid << (p1 - 15)
|
119
|
+
end
|
120
|
+
if row > 2 && col > 1
|
121
|
+
valid << (p1 - 17)
|
122
|
+
end
|
123
|
+
|
124
|
+
# All possible moves for the knight based on board boundaries will added
|
125
|
+
# This iterator filters for friendly fire, and removes indexes pointing to same color pices
|
126
|
+
valid.each do |pos|
|
127
|
+
unless piece_color(pos) == @@color
|
128
|
+
valid_no_ff << pos
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
return valid_no_ff
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
# Lateral movements (Left, Right, Up, Down). Will return all valid lateral movies for a piece at index
|
137
|
+
# By default, it will extend the piece movement laterally until it hits a board edge
|
138
|
+
# or until it hits a piece. This can be changed by passing the limit argument
|
139
|
+
# For example, the king can only move laterally 1 position, so it would pass limit=1
|
140
|
+
def move_lateral(index, limit = 8)
|
141
|
+
row = get_row_from_index(index)
|
142
|
+
col = get_col_from_index(index)
|
143
|
+
left, right = [col-1, limit].min, [8-col, limit].min
|
144
|
+
up, down = [row-1, limit].min, [8-row, limit].min
|
145
|
+
valid = []
|
146
|
+
|
147
|
+
# Move down N places until board limit, piece in the way, or specified limit
|
148
|
+
down.times do |i|
|
149
|
+
next_pos = index + (i+1)*8
|
150
|
+
# Valid move if position is unoccupied
|
151
|
+
if unoccupied?(next_pos)
|
152
|
+
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
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Move up N places until board limit, piece in the way, or specified limit
|
165
|
+
up.times do |i|
|
166
|
+
next_pos = index - (i+1)*8
|
167
|
+
if unoccupied?(next_pos)
|
168
|
+
valid << next_pos
|
169
|
+
else
|
170
|
+
#puts "PC #{piece_color(next_pos)} #{enemy_color}" #INCOMEPSDFJDSLFJDKLFJDASKLF
|
171
|
+
if piece_color(next_pos) == @@enemy_color
|
172
|
+
valid << next_pos
|
173
|
+
end
|
174
|
+
break
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Move right N places until board limit, piece in the way, or specified limit
|
179
|
+
right.times do |i|
|
180
|
+
next_pos = index + (i+1)
|
181
|
+
if unoccupied?(next_pos)
|
182
|
+
valid << next_pos
|
183
|
+
else
|
184
|
+
if piece_color(next_pos) == @@enemy_color
|
185
|
+
valid << next_pos
|
186
|
+
end
|
187
|
+
break
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Move left N places until board limit, piece in the way, or specified limit
|
192
|
+
left.times do |i|
|
193
|
+
next_pos = index - (i+1)
|
194
|
+
if unoccupied?(next_pos)
|
195
|
+
valid << next_pos
|
196
|
+
else
|
197
|
+
if piece_color(next_pos) == @@enemy_color
|
198
|
+
valid << next_pos
|
199
|
+
end
|
200
|
+
break
|
201
|
+
end
|
202
|
+
end
|
203
|
+
return valid
|
204
|
+
end
|
205
|
+
|
206
|
+
|
207
|
+
# Diagonal movements. Will return all valid diagonal movies for a piece at index
|
208
|
+
# By default, it will extend the piece movement diagonally until it hits a board edge
|
209
|
+
# or until it hits a piece. This can be changed by passing the limit argument
|
210
|
+
# For example, the king can only move diagonally 1 position, so it would pass limit=1
|
211
|
+
def move_diagonal(index, limit = 8)
|
212
|
+
|
213
|
+
row = get_row_from_index(index)
|
214
|
+
col = get_col_from_index(index)
|
215
|
+
left, right = [col-1, limit].min, [8-col, limit].min
|
216
|
+
up, down = [row-1, limit].min, [8-row, limit].min
|
217
|
+
valid = []
|
218
|
+
|
219
|
+
# up and to the right
|
220
|
+
([up, right, limit].min).times do |i|
|
221
|
+
next_pos = index - (i+1)*7
|
222
|
+
# Valid move if position is unoccupied
|
223
|
+
if unoccupied?(next_pos)
|
224
|
+
valid << next_pos
|
225
|
+
else
|
226
|
+
# Valid move is piece is an enemy, but then no subsequent tiles are attackable
|
227
|
+
# if the piece is not an enemy, it's not added as a valid move, and no subsequent tiles are attackable
|
228
|
+
# This function doesn't filter out the king from a valid enemy, but the Board class will drop King indexes
|
229
|
+
if piece_color(next_pos) == @@enemy_color
|
230
|
+
valid << next_pos
|
231
|
+
end
|
232
|
+
break
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# up and to the left
|
237
|
+
([up, left, limit].min).times do |i|
|
238
|
+
next_pos = index - (i+1)*9
|
239
|
+
if unoccupied?(next_pos)
|
240
|
+
valid << next_pos
|
241
|
+
else
|
242
|
+
if piece_color(next_pos) == @@enemy_color
|
243
|
+
valid << next_pos
|
244
|
+
end
|
245
|
+
break
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# down and to the right
|
250
|
+
([down, right, limit].min).times do |i|
|
251
|
+
next_pos = index + (i+1)*9
|
252
|
+
if unoccupied?(next_pos)
|
253
|
+
valid << next_pos
|
254
|
+
else
|
255
|
+
if piece_color(next_pos) == @@enemy_color
|
256
|
+
valid << next_pos
|
257
|
+
end
|
258
|
+
break
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# down and to the left
|
263
|
+
([down, left, limit].min).times do |i|
|
264
|
+
next_pos = index + (i+1)*7
|
265
|
+
if unoccupied?(next_pos)
|
266
|
+
valid << next_pos
|
267
|
+
else
|
268
|
+
if piece_color(next_pos) == @@enemy_color
|
269
|
+
valid << next_pos
|
270
|
+
end
|
271
|
+
break
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
return valid
|
276
|
+
end
|
277
|
+
|
278
|
+
# Castle: king cannot move into check, or through check
|
279
|
+
def castle(index)
|
280
|
+
valid = []
|
281
|
+
dangerous_tiles = attack_vectors
|
282
|
+
|
283
|
+
# Valid positions for a King to be in order to castle
|
284
|
+
if index == 5 || index == 61
|
285
|
+
# Empty space between a King and a Rook
|
286
|
+
if unoccupied?(index - 1) && unoccupied?(index - 2) && unoccupied?(index - 3) && @@pieces[index - 4]["moved"] == false
|
287
|
+
# Ensure king does not move through check or into check
|
288
|
+
if !dangerous_tiles.include?(index - 1) && !dangerous_tiles.include?(index - 2)
|
289
|
+
# King's castle position
|
290
|
+
valid << index - 2
|
291
|
+
end
|
292
|
+
elsif unoccupied?(index + 1) && unoccupied?(index + 2) && @@pieces[index + 3]["moved"] == false
|
293
|
+
if !dangerous_tiles.include?(index + 1) && !dangerous_tiles.include?(index + 2)
|
294
|
+
valid << index + 2
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
return valid
|
299
|
+
end
|
300
|
+
|
301
|
+
|
302
|
+
# Check if board tile currently has a piece
|
303
|
+
def unoccupied?(index)
|
304
|
+
if @@pieces[index]["color"].nil?
|
305
|
+
return true
|
306
|
+
else
|
307
|
+
return false
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
|
312
|
+
# Return true if the piece has moved before
|
313
|
+
def moved?(index)
|
314
|
+
if @@pieces[index]["moved"]
|
315
|
+
return true
|
316
|
+
else
|
317
|
+
return false
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
|
322
|
+
# Return piece color ("red" or "black") from index (1 - 64)
|
323
|
+
def piece_color(index)
|
324
|
+
return @@pieces[index]["color"]
|
325
|
+
end
|
326
|
+
|
327
|
+
|
328
|
+
# Method used when moving, to verify the piece at index (1 - 64) is not of type "king"
|
329
|
+
def not_king(index)
|
330
|
+
return @@piece_locations[index]["type"] == "king"
|
331
|
+
end
|
332
|
+
|
333
|
+
|
334
|
+
# Obtain chess board row number (1 + 8) from an index (1 - 64)
|
335
|
+
def get_row_from_index(index)
|
336
|
+
return (index - 1)/8 + 1
|
337
|
+
end
|
338
|
+
|
339
|
+
|
340
|
+
# Obtain chess board column number (1 - 8) from an index (1 - 64)
|
341
|
+
def get_col_from_index(index)
|
342
|
+
if index % 8 == 0
|
343
|
+
return 8
|
344
|
+
else
|
345
|
+
return index % 8
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
end
|
data/lib/printer.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module PRINTER
|
4
|
+
|
5
|
+
VERSION = "0.1.0"
|
6
|
+
COLS = ['A','B','C','D','E','F','G','H']
|
7
|
+
@@n = 0
|
8
|
+
@@print_count = 1
|
9
|
+
|
10
|
+
|
11
|
+
# Prints the board to terminal, based on layout defined by piece_locations
|
12
|
+
def printer
|
13
|
+
|
14
|
+
# Clear and reset counters
|
15
|
+
@@print_count = 1
|
16
|
+
system "clear" or system "cls"
|
17
|
+
|
18
|
+
# Header (title & column labels)
|
19
|
+
print "\n\t>> Welcome to Terminal Chess v#{VERSION}\n\n\s\s\s"
|
20
|
+
COLS.each { |c| print " _#{c}__ " }
|
21
|
+
puts "\n"
|
22
|
+
|
23
|
+
# Print Cells (use printer block and pass cell styling in loop below)
|
24
|
+
(1..8).each do |row|
|
25
|
+
yield "| |"
|
26
|
+
yield "| XX |", "#{row}"
|
27
|
+
yield "|____|"
|
28
|
+
end
|
29
|
+
|
30
|
+
# Footer (print column labels)
|
31
|
+
print "\s\s\s"
|
32
|
+
COLS.each { |c| print " #{c} " }
|
33
|
+
puts ""
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def print_board(piece_locations)
|
38
|
+
printer { |i, j|
|
39
|
+
|
40
|
+
# Print preceeding row index (1...8) if applicable
|
41
|
+
if j then print " #{j} " else print " " end
|
42
|
+
if @@n < 3
|
43
|
+
# Print cell (4 characters wide, 3 characters tall)
|
44
|
+
4.times do
|
45
|
+
color = piece_locations[@@print_count]["color"] || "black"
|
46
|
+
next_color = piece_locations[@@print_count+1]["color"] || "black"
|
47
|
+
print "#{i}".gsub("XX", piece_locations[@@print_count]["type"][0..1].upcase).colorize(:"#{color}").on_light_white
|
48
|
+
print "#{i}".gsub("XX", piece_locations[@@print_count+1]["type"][0..1].upcase).colorize(:"#{next_color}").on_light_black
|
49
|
+
if "#{i}".include? "XX"
|
50
|
+
# Incremenet print_count, unless last cell is being printed, to avoid an out of range error
|
51
|
+
@@print_count += 2 unless @@print_count == 63
|
52
|
+
end
|
53
|
+
end
|
54
|
+
else
|
55
|
+
# Print cell starting with alternative color (4 characters wide, 3 characters tall)
|
56
|
+
4.times do
|
57
|
+
color = piece_locations[@@print_count]["color"] || "black"
|
58
|
+
next_color = piece_locations[@@print_count+1]["color"] || "black"
|
59
|
+
print "#{i}".gsub("XX", piece_locations[@@print_count]["type"][0..1].upcase).colorize(:"#{color}").on_light_black
|
60
|
+
print "#{i}".gsub("XX", piece_locations[@@print_count+1]["type"][0..1].upcase).colorize(:"#{next_color}").on_light_white
|
61
|
+
if "#{i}".include? "XX"
|
62
|
+
@@print_count += 2 unless @@print_count == 63
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Print succeeding row index (1...8) if applicable
|
68
|
+
if (@@n == 1 || @@n == 4) && j then print " #{j}" end
|
69
|
+
|
70
|
+
# Incriment row index. Reset once n reaches 6 (i.e., two complete cell rows have been printed - the pattern to repeat)
|
71
|
+
@@n += 1
|
72
|
+
if @@n == 6 then @@n = 0 end
|
73
|
+
puts ""
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH << '.'
|
4
|
+
|
5
|
+
require_relative "terminal_chess/version"
|
6
|
+
require_relative "printer.rb"
|
7
|
+
require_relative "move.rb"
|
8
|
+
require_relative "board.rb"
|
9
|
+
|
10
|
+
# Setup
|
11
|
+
a = Board.new; a.setup_board; a.board_refresh
|
12
|
+
|
13
|
+
# Gameplay
|
14
|
+
while true
|
15
|
+
|
16
|
+
print "\nPiece to Move [#{a.player_turn.capitalize}]: "
|
17
|
+
from = gets.chomp.upcase
|
18
|
+
|
19
|
+
begin
|
20
|
+
print "Valid destinations: #{a.valid_destinations(from).join(", ")}"
|
21
|
+
|
22
|
+
print "\nLocation: "
|
23
|
+
to = gets.chomp.upcase
|
24
|
+
a.move(from, to)
|
25
|
+
|
26
|
+
rescue Exception => e
|
27
|
+
puts "Invalid selection"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'terminal_chess/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'terminal_chess'
|
8
|
+
s.version = TerminalChess::VERSION
|
9
|
+
s.date = '2015-03-03'
|
10
|
+
s.summary = "Chess game playable via the terminal"
|
11
|
+
s.description = "Two player chess game through the terminal"
|
12
|
+
s.authors = ["Jason Willems"]
|
13
|
+
s.email = 'hello@jasonwillems.com'
|
14
|
+
s.homepage = 'https://github.com/atlas/Terminal-Chess'
|
15
|
+
s.license = 'MIT'
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
s.executables << 'terminal_chess'
|
20
|
+
|
21
|
+
s.add_runtime_dependency "colorize"
|
22
|
+
s.add_development_dependency "bundler", "~> 1.7"
|
23
|
+
s.add_development_dependency "rake", "~> 10.0"
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: terminal_chess
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jason Willems
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-03-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: colorize
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
description: Two player chess game through the terminal
|
56
|
+
email: hello@jasonwillems.com
|
57
|
+
executables:
|
58
|
+
- terminal_chess
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- Gemfile
|
63
|
+
- LICENSE
|
64
|
+
- README.md
|
65
|
+
- Rakefile
|
66
|
+
- bin/terminal_chess
|
67
|
+
- lib/Board.rb
|
68
|
+
- lib/move.rb
|
69
|
+
- lib/printer.rb
|
70
|
+
- lib/terminal_chess.rb
|
71
|
+
- lib/terminal_chess/version.rb
|
72
|
+
- terminal_chess.gemspec
|
73
|
+
homepage: https://github.com/atlas/Terminal-Chess
|
74
|
+
licenses:
|
75
|
+
- MIT
|
76
|
+
metadata: {}
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options: []
|
79
|
+
require_paths:
|
80
|
+
- lib
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
requirements: []
|
92
|
+
rubyforge_project:
|
93
|
+
rubygems_version: 2.2.2
|
94
|
+
signing_key:
|
95
|
+
specification_version: 4
|
96
|
+
summary: Chess game playable via the terminal
|
97
|
+
test_files: []
|