tak 0.0.0 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +1 -1
- data/Gemfile +3 -0
- data/Gemfile.lock +71 -0
- data/Guardfile +42 -0
- data/README.md +3 -27
- data/Rakefile +35 -0
- data/lib/tak.rb +6 -0
- data/lib/tak/board.rb +267 -0
- data/lib/tak/game.rb +9 -0
- data/lib/tak/move.rb +69 -0
- data/lib/tak/piece_set.rb +58 -0
- data/lib/tak/player.rb +8 -0
- data/lib/tak/ptn.rb +155 -0
- data/lib/tak/version.rb +1 -1
- data/tak.gemspec +1 -1
- metadata +16 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3bc109da83ba6396965054bc21ede3b5a6dbc18d32f888470a99c1d86153d426
|
4
|
+
data.tar.gz: de8afe399c579542ccc04cb51662971ea0b76266bd6decdd56dbdb49122d2a9f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 456cfc6b766ea9f636cef4c22ea87baaa0fb56c26074fd44ee4043620202cba785d1a6e3c4d63b3e30f200efbd1a1b031a772af11ff784254bc24ae2e2358249
|
7
|
+
data.tar.gz: 46248cdf495efd2aef8b53e2ebe587101844292c6cc59ec70ddac0e7f243c8fc9527316364f4c447234c3589b468cb29a868e6d99c55fc17451a1e2e0413bc5b
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
tak (0.0.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
coderay (1.1.3)
|
10
|
+
diff-lcs (1.4.4)
|
11
|
+
ffi (1.15.0)
|
12
|
+
formatador (0.2.5)
|
13
|
+
guard (2.16.2)
|
14
|
+
formatador (>= 0.2.4)
|
15
|
+
listen (>= 2.7, < 4.0)
|
16
|
+
lumberjack (>= 1.0.12, < 2.0)
|
17
|
+
nenv (~> 0.1)
|
18
|
+
notiffany (~> 0.0)
|
19
|
+
pry (>= 0.9.12)
|
20
|
+
shellany (~> 0.0)
|
21
|
+
thor (>= 0.18.1)
|
22
|
+
guard-compat (1.2.1)
|
23
|
+
guard-rspec (4.7.3)
|
24
|
+
guard (~> 2.1)
|
25
|
+
guard-compat (~> 1.1)
|
26
|
+
rspec (>= 2.99.0, < 4.0)
|
27
|
+
listen (3.4.1)
|
28
|
+
rb-fsevent (~> 0.10, >= 0.10.3)
|
29
|
+
rb-inotify (~> 0.9, >= 0.9.10)
|
30
|
+
lumberjack (1.2.8)
|
31
|
+
method_source (1.0.0)
|
32
|
+
nenv (0.3.0)
|
33
|
+
notiffany (0.1.3)
|
34
|
+
nenv (~> 0.1)
|
35
|
+
shellany (~> 0.0)
|
36
|
+
pry (0.14.0)
|
37
|
+
coderay (~> 1.1)
|
38
|
+
method_source (~> 1.0)
|
39
|
+
rake (10.5.0)
|
40
|
+
rb-fsevent (0.10.4)
|
41
|
+
rb-inotify (0.10.1)
|
42
|
+
ffi (~> 1.0)
|
43
|
+
rspec (3.10.0)
|
44
|
+
rspec-core (~> 3.10.0)
|
45
|
+
rspec-expectations (~> 3.10.0)
|
46
|
+
rspec-mocks (~> 3.10.0)
|
47
|
+
rspec-core (3.10.1)
|
48
|
+
rspec-support (~> 3.10.0)
|
49
|
+
rspec-expectations (3.10.1)
|
50
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
51
|
+
rspec-support (~> 3.10.0)
|
52
|
+
rspec-mocks (3.10.2)
|
53
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
54
|
+
rspec-support (~> 3.10.0)
|
55
|
+
rspec-support (3.10.2)
|
56
|
+
shellany (0.0.1)
|
57
|
+
thor (1.1.0)
|
58
|
+
|
59
|
+
PLATFORMS
|
60
|
+
x86_64-darwin-19
|
61
|
+
|
62
|
+
DEPENDENCIES
|
63
|
+
bundler (~> 2.0)
|
64
|
+
guard
|
65
|
+
guard-rspec
|
66
|
+
rake (~> 10.0)
|
67
|
+
rspec (~> 3.0)
|
68
|
+
tak!
|
69
|
+
|
70
|
+
BUNDLED WITH
|
71
|
+
2.2.3
|
data/Guardfile
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
## Uncomment and set this to only include directories you want to watch
|
5
|
+
# directories %w(app lib config test spec features) \
|
6
|
+
# .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
|
7
|
+
|
8
|
+
## Note: if you are using the `directories` clause above and you are not
|
9
|
+
## watching the project directory ('.'), then you will want to move
|
10
|
+
## the Guardfile to a watched dir and symlink it back, e.g.
|
11
|
+
#
|
12
|
+
# $ mkdir config
|
13
|
+
# $ mv Guardfile config/
|
14
|
+
# $ ln -s config/Guardfile .
|
15
|
+
#
|
16
|
+
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
|
17
|
+
|
18
|
+
# Note: The cmd option is now required due to the increasing number of ways
|
19
|
+
# rspec may be run, below are examples of the most common uses.
|
20
|
+
# * bundler: 'bundle exec rspec'
|
21
|
+
# * bundler binstubs: 'bin/rspec'
|
22
|
+
# * spring: 'bin/rspec' (This will use spring if running and you have
|
23
|
+
# installed the spring binstubs per the docs)
|
24
|
+
# * zeus: 'zeus rspec' (requires the server to be started separately)
|
25
|
+
# * 'just' rspec: 'rspec'
|
26
|
+
|
27
|
+
guard :rspec, cmd: "bundle exec rspec" do
|
28
|
+
require "guard/rspec/dsl"
|
29
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
30
|
+
|
31
|
+
# Feel free to open issues for suggestions and improvements
|
32
|
+
|
33
|
+
# RSpec files
|
34
|
+
rspec = dsl.rspec
|
35
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
36
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
37
|
+
watch(rspec.spec_files)
|
38
|
+
|
39
|
+
# Ruby files
|
40
|
+
ruby = dsl.ruby
|
41
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
42
|
+
end
|
data/README.md
CHANGED
@@ -1,38 +1,14 @@
|
|
1
1
|
# Tak
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
6
|
-
|
7
|
-
## Installation
|
8
|
-
|
9
|
-
Add this line to your application's Gemfile:
|
10
|
-
|
11
|
-
```ruby
|
12
|
-
gem 'tak'
|
13
|
-
```
|
14
|
-
|
15
|
-
And then execute:
|
16
|
-
|
17
|
-
$ bundle
|
18
|
-
|
19
|
-
Or install it yourself as:
|
20
|
-
|
21
|
-
$ gem install tak
|
3
|
+
Ruby Tak utilities and gameplay helpers
|
22
4
|
|
23
5
|
## Usage
|
24
6
|
|
25
|
-
|
26
|
-
|
27
|
-
## Development
|
28
|
-
|
29
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
-
|
31
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
7
|
+
TBD
|
32
8
|
|
33
9
|
## Contributing
|
34
10
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
11
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/baweaver/tak. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
36
12
|
|
37
13
|
|
38
14
|
## License
|
data/Rakefile
CHANGED
@@ -4,3 +4,38 @@ require "rspec/core/rake_task"
|
|
4
4
|
RSpec::Core::RakeTask.new(:spec)
|
5
5
|
|
6
6
|
task :default => :spec
|
7
|
+
|
8
|
+
require "bundler/gem_tasks"
|
9
|
+
|
10
|
+
# http://blog.zachallett.com/pry-reload/
|
11
|
+
#
|
12
|
+
# I should gemify this later.
|
13
|
+
task :console do
|
14
|
+
require 'pry'
|
15
|
+
require 'tak'
|
16
|
+
|
17
|
+
# http://stackoverflow.com/questions/9236264/how-to-disable-warning-for-redefining-a-constant-when-loading-a-file
|
18
|
+
def silence_warnings(&block)
|
19
|
+
warn_level, $VERBOSE = $VERBOSE, nil
|
20
|
+
|
21
|
+
result = block.call
|
22
|
+
$VERBOSE = warn_level
|
23
|
+
|
24
|
+
result
|
25
|
+
end
|
26
|
+
|
27
|
+
# Modified a bit to silence some constant reload warnings
|
28
|
+
# and to return a true or the error.
|
29
|
+
def reload!
|
30
|
+
!!silence_warnings do
|
31
|
+
$LOADED_FEATURES
|
32
|
+
.select { |feat| feat =~ /\/tak\// }
|
33
|
+
.each { |file| load file }
|
34
|
+
end
|
35
|
+
rescue => e
|
36
|
+
e
|
37
|
+
end
|
38
|
+
|
39
|
+
ARGV.clear
|
40
|
+
Pry.start
|
41
|
+
end
|
data/lib/tak.rb
CHANGED
data/lib/tak/board.rb
ADDED
@@ -0,0 +1,267 @@
|
|
1
|
+
module Tak
|
2
|
+
class Board
|
3
|
+
attr_accessor :size
|
4
|
+
attr_accessor :board
|
5
|
+
|
6
|
+
attr_reader :white_piece_set
|
7
|
+
attr_reader :black_piece_set
|
8
|
+
|
9
|
+
# What constitutes a road-forming piece
|
10
|
+
ROAD = {
|
11
|
+
white: %w(w Cw),
|
12
|
+
black: %w(b Cb)
|
13
|
+
}
|
14
|
+
|
15
|
+
# Creates a new Tak Board
|
16
|
+
#
|
17
|
+
# @param size [Integer] Size of the board
|
18
|
+
# @param ptn [Array[String]] Previous game moveset to reconstruct from
|
19
|
+
def initialize(size, ptn = nil)
|
20
|
+
@size = size
|
21
|
+
@board = generate_board(ptn)
|
22
|
+
|
23
|
+
@white_piece_set = Tak::PieceSet.new(board_size: size)
|
24
|
+
@black_piece_set = Tak::PieceSet.new(board_size: size)
|
25
|
+
|
26
|
+
@piece_sets = {
|
27
|
+
white: white_piece_set,
|
28
|
+
black: black_piece_set
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
# Gets the current visible flat counts on the board
|
33
|
+
#
|
34
|
+
# @return [Hash[Symbol, Integer]]
|
35
|
+
def flat_counts
|
36
|
+
count_hash = Hash.new { |h,k| h[k] = 0 }
|
37
|
+
|
38
|
+
@board.each_with_object(count_hash) do |row, counts|
|
39
|
+
row.each do |cell|
|
40
|
+
counts[:white] += 1 if cell.last == 'w'
|
41
|
+
counts[:black] += 1 if cell.last == 'b'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Checks to see if any piece sets are empty
|
47
|
+
#
|
48
|
+
# @return [Boolean]
|
49
|
+
def empty_piece_set?
|
50
|
+
@piece_sets.any? { |c, piece_set| piece_set.empty? }
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns who the flat winner is, or false if there is none yet
|
54
|
+
#
|
55
|
+
# Not a fan of the Bool/Sym response, consider refactor later.
|
56
|
+
#
|
57
|
+
# @return [Boolean || Symbol]
|
58
|
+
def flat_winner
|
59
|
+
return false unless empty_piece_set?
|
60
|
+
|
61
|
+
white_flats, black_flats = flat_counts.values
|
62
|
+
case white_flats <=> black_flats
|
63
|
+
when 0 then :tie
|
64
|
+
when 1 then :white
|
65
|
+
when -1 then :black
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Converts the current board into a bit variant in which viable
|
70
|
+
# road pieces for `color` are represented as 1s
|
71
|
+
#
|
72
|
+
# @param color [Symbol]
|
73
|
+
#
|
74
|
+
# @return [Array[Array[Integer]]]
|
75
|
+
def bit_board(color)
|
76
|
+
@board.map { |row|
|
77
|
+
row.map { |cell| ROAD[color].include?(cell.last) ? 1 : 0 }
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
# Checks if a road win for `color` is present
|
82
|
+
#
|
83
|
+
# @param color [Symbol] Color to check for a win
|
84
|
+
#
|
85
|
+
# @return [Boolean]
|
86
|
+
def road_win?(color)
|
87
|
+
current_bit_board = bit_board(color)
|
88
|
+
|
89
|
+
# Begin traversing through the board for potential roads.
|
90
|
+
path_search(0, 0, nil, current_bit_board) || !!@size.times.find { |n|
|
91
|
+
path_search(1, n, :horizontal, current_bit_board) ||
|
92
|
+
path_search(n, 1, :vertical, current_bit_board)
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
# Checks which color player currently owns a square
|
97
|
+
#
|
98
|
+
# @param x [Integer]
|
99
|
+
# @param y [Integer]
|
100
|
+
#
|
101
|
+
# @return [Symbol]
|
102
|
+
def square_owner(x, y)
|
103
|
+
return true if @board[x][y].empty?
|
104
|
+
|
105
|
+
@board[x][y].last == 'b' ? :black : :white
|
106
|
+
end
|
107
|
+
|
108
|
+
# Retrieves the pieces present at a coordinate
|
109
|
+
#
|
110
|
+
# @param x [Integer]
|
111
|
+
# @param y [Integer]
|
112
|
+
#
|
113
|
+
# @return [Array[String]]
|
114
|
+
def pieces_at(x, y)
|
115
|
+
@board[x][y]
|
116
|
+
end
|
117
|
+
|
118
|
+
# Moves pieces according to PTN notation
|
119
|
+
#
|
120
|
+
# @param ptn [String]
|
121
|
+
# String representation of a Tak move
|
122
|
+
#
|
123
|
+
# @param color [Symbol]
|
124
|
+
# Which player is making the move
|
125
|
+
#
|
126
|
+
# @return [Boolean]
|
127
|
+
# Success status
|
128
|
+
def move!(ptn, color)
|
129
|
+
move = Tak::Move.new(ptn, self, color)
|
130
|
+
|
131
|
+
return false unless move.valid? && square_owner(*move.origin)
|
132
|
+
|
133
|
+
case move.type
|
134
|
+
when 'movement'
|
135
|
+
distribute_pieces(move)
|
136
|
+
when 'placement'
|
137
|
+
place_piece(move, color)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Distributes pieces from a stack move across the board
|
142
|
+
#
|
143
|
+
# @param move [Tak::Move]
|
144
|
+
#
|
145
|
+
# @return [Boolean]
|
146
|
+
def distribute_pieces(move)
|
147
|
+
stack = pieces_at(*move.origin).pop(move.size)
|
148
|
+
|
149
|
+
move.coordinates.each do |(x, y)|
|
150
|
+
@board[x][y].push(stack.pop)
|
151
|
+
end
|
152
|
+
|
153
|
+
true
|
154
|
+
end
|
155
|
+
|
156
|
+
# Places a piece at a given move coordinate
|
157
|
+
#
|
158
|
+
# @param move [Tak::Move]
|
159
|
+
# @param color [Symbol]
|
160
|
+
#
|
161
|
+
# @return [Boolean]
|
162
|
+
# Success of placement
|
163
|
+
def place_piece(move, color)
|
164
|
+
square = pieces_at(*move.origin)
|
165
|
+
|
166
|
+
return false unless square.empty? && @piece_sets[color].remove(move.piece_type)
|
167
|
+
|
168
|
+
square.push(move.piece)
|
169
|
+
|
170
|
+
true
|
171
|
+
end
|
172
|
+
|
173
|
+
# Consider moving this all out into a board formatter later. It'd be cleaner.
|
174
|
+
#
|
175
|
+
# May Matz forgive me for my debugging code here.
|
176
|
+
def to_s
|
177
|
+
max_size = board.flatten(1).max.join(' ').size + 1
|
178
|
+
|
179
|
+
counts = flat_counts.map { |c, i|
|
180
|
+
set = @piece_sets[c]
|
181
|
+
"#{c}: #{i} flats, #{set.flats} remaining pieces, #{set.capstones} remaining capstones"
|
182
|
+
}.join("\n")
|
183
|
+
|
184
|
+
board_state = board.reverse.map.with_index { |row, i|
|
185
|
+
row_head = " #{size - i} "
|
186
|
+
|
187
|
+
row_head + row.map { |cell|
|
188
|
+
"[%#{max_size}s]" % cell.join(' ')
|
189
|
+
}.join(' ')
|
190
|
+
}.join("\n")
|
191
|
+
|
192
|
+
footer = ('a'..'h').take(size).map { |c| "%#{max_size + 2}s" % c }.join(' ')
|
193
|
+
|
194
|
+
"#{counts}\n\n#{board_state}\n #{footer}"
|
195
|
+
end
|
196
|
+
|
197
|
+
private
|
198
|
+
|
199
|
+
# Checks whether a given coordinate pair is out of bounds
|
200
|
+
#
|
201
|
+
# @param x [Integer]
|
202
|
+
# @param y [Integer]
|
203
|
+
#
|
204
|
+
# @return [Boolean]
|
205
|
+
def out_of_bounds?(x,y)
|
206
|
+
x < 0 || y < 0 || x > @size - 1 || y > @size - 1
|
207
|
+
end
|
208
|
+
|
209
|
+
# Does a DFS variant to locate a potential road.
|
210
|
+
#
|
211
|
+
# @param x [Integer] Start X coordinate
|
212
|
+
# @param y [Integer] Start Y coordinate
|
213
|
+
# @param direction [Symbol] Direction the search completes on
|
214
|
+
#
|
215
|
+
# @param bit_board [Array[Array[Integer]]] Board to parse against
|
216
|
+
# @param traversed [Array[Array[Boolean]]] Traversal tracker
|
217
|
+
#
|
218
|
+
# @return [Boolean] Whether or not a road was round
|
219
|
+
def path_search(x, y, direction, bit_board, traversed = visited_board)
|
220
|
+
return false if out_of_bounds?(x,y) || traversed[x][y]
|
221
|
+
|
222
|
+
piece_value = bit_board[x][y]
|
223
|
+
return false if piece_value == 0 # Non-road piece
|
224
|
+
return true if road_end?(direction, x, y)
|
225
|
+
|
226
|
+
traversed[x][y] = true
|
227
|
+
|
228
|
+
# Recurse in all four directions. While this may retrace steps it is
|
229
|
+
# necessary as roads in Tak can curve wildly.
|
230
|
+
path_search(x + 1, y, direction, bit_board, traversed) ||
|
231
|
+
path_search(x - 1, y, direction, bit_board, traversed) ||
|
232
|
+
path_search(x, y + 1, direction, bit_board, traversed) ||
|
233
|
+
path_search(x, y - 1, direction, bit_board, traversed)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Checks to see if a given x y coordinate pair is counted as a road win
|
237
|
+
# by being on the appropriate opposite end from the starting direction
|
238
|
+
# of the road.
|
239
|
+
#
|
240
|
+
# @param direction [Symbol] Starting direction of the road
|
241
|
+
# @param x [Integer]
|
242
|
+
# @param y [Integer]
|
243
|
+
#
|
244
|
+
# @return [Boolean]
|
245
|
+
def road_end?(direction, x, y)
|
246
|
+
case direction
|
247
|
+
when :horizontal
|
248
|
+
x == @size - 1
|
249
|
+
when :vertical
|
250
|
+
y == @size - 1
|
251
|
+
else
|
252
|
+
x == @size - 1 || y == @size - 1
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# Creates a board to mark previous traversals of the path search
|
257
|
+
#
|
258
|
+
# @return [Array[Array[Boolean]]]
|
259
|
+
def visited_board
|
260
|
+
Array.new(@size) { Array.new(@size, false) }
|
261
|
+
end
|
262
|
+
|
263
|
+
def generate_board(ptn)
|
264
|
+
return Array.new(size) { Array.new(size) { [] } } unless ptn
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
data/lib/tak/game.rb
ADDED
data/lib/tak/move.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
module Tak
|
2
|
+
class Move
|
3
|
+
attr_reader :move
|
4
|
+
attr_reader :origin
|
5
|
+
|
6
|
+
# Pieces that get in the way of a road
|
7
|
+
OBSTRUCTIONS = %w(Cw Cb Sw Sb)
|
8
|
+
|
9
|
+
def initialize(ptn, tak_board, color)
|
10
|
+
@move = Tak::PTN.new(ptn, tak_board.size)
|
11
|
+
@board = tak_board.board
|
12
|
+
@origin = [@move.x, @move.y]
|
13
|
+
@color = color.to_s[0]
|
14
|
+
end
|
15
|
+
|
16
|
+
def type
|
17
|
+
move.type
|
18
|
+
end
|
19
|
+
|
20
|
+
def piece_type
|
21
|
+
case piece
|
22
|
+
when /C/ then :capstone
|
23
|
+
when /S/ then :standing
|
24
|
+
else :flatstone
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def piece
|
29
|
+
"#{move.special_piece}#{@color}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def valid?
|
33
|
+
return true unless obstructed?
|
34
|
+
return false if coordinates.flatten.any? { |n|
|
35
|
+
n > @board.size || n < 0
|
36
|
+
}
|
37
|
+
|
38
|
+
x, y = move.position
|
39
|
+
top_piece = @board[x][y].last
|
40
|
+
|
41
|
+
if %w(Cw Cb).include?(top_piece)
|
42
|
+
x2, y2 = coordinates.last
|
43
|
+
obstruction = @board[x2][y2].last
|
44
|
+
|
45
|
+
%w(Sw Sb).include?(obstruction)
|
46
|
+
else
|
47
|
+
false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def obstructed?
|
52
|
+
!!coordinates.find { |(x,y)| OBSTRUCTIONS.include?(@board[x][y].last) }
|
53
|
+
end
|
54
|
+
|
55
|
+
def coordinates
|
56
|
+
x, y = move.position
|
57
|
+
times = move.size.times
|
58
|
+
|
59
|
+
return [[x,y]] if move.size.zero?
|
60
|
+
|
61
|
+
case move.direction
|
62
|
+
when '+' then times.map { |n| [x, y + n] }
|
63
|
+
when '-' then times.map { |n| [x, y - n] }
|
64
|
+
when '<' then times.map { |n| [x - n, y] }
|
65
|
+
when '>' then times.map { |n| [x + n, y + n] }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Tak
|
2
|
+
class PieceSet
|
3
|
+
attr_reader :flats, :capstones
|
4
|
+
|
5
|
+
# https://en.wikipedia.org/wiki/Tak_(game)#Rules
|
6
|
+
#
|
7
|
+
# 21 pieces is correct for 5s
|
8
|
+
#
|
9
|
+
# size => [pieces, capstones]
|
10
|
+
PIECE_TOTALS = {
|
11
|
+
3 => [10, 0],
|
12
|
+
4 => [15, 0],
|
13
|
+
5 => [21, 1],
|
14
|
+
6 => [30, 1],
|
15
|
+
7 => [40, 2],
|
16
|
+
8 => [50, 2]
|
17
|
+
}
|
18
|
+
|
19
|
+
def initialize(board_size: 5, flats: nil, capstones: nil)
|
20
|
+
default_flats, default_capstones = PIECE_TOTALS[board_size]
|
21
|
+
|
22
|
+
@flats = flats || default_flats
|
23
|
+
@capstones = capstones || default_capstones
|
24
|
+
|
25
|
+
@pieces = {
|
26
|
+
capstone: @capstones,
|
27
|
+
flatstone: @flats
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
# Whether or not the set is empty
|
32
|
+
#
|
33
|
+
# @note It's often forgotten rule that a capstone needs to be played to
|
34
|
+
# consider a piece set as empty and trigger a flat win count.
|
35
|
+
#
|
36
|
+
# @return [Boolean]
|
37
|
+
def empty?
|
38
|
+
@capstones + @flats == 0
|
39
|
+
end
|
40
|
+
|
41
|
+
# Removes a piece from a set
|
42
|
+
#
|
43
|
+
# @param type [Symbol] Type of piece
|
44
|
+
#
|
45
|
+
# @return [Boolean] Whether the removal was valid
|
46
|
+
def remove(type)
|
47
|
+
return false if @pieces[type] - 1 < 0
|
48
|
+
|
49
|
+
if type == :capstone
|
50
|
+
@capstones -= 1
|
51
|
+
else
|
52
|
+
@flats -= 1
|
53
|
+
end
|
54
|
+
|
55
|
+
true
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/tak/player.rb
ADDED
data/lib/tak/ptn.rb
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
module Tak
|
2
|
+
class PTN
|
3
|
+
NOTATION_REGEX = /
|
4
|
+
(?<number>\d+)?
|
5
|
+
(?<special_piece>[CS])?
|
6
|
+
(?<position>[a-h][1-8])
|
7
|
+
(
|
8
|
+
(?<direction>[<>+-])
|
9
|
+
(?<stack>\d+)?
|
10
|
+
)?
|
11
|
+
/xi
|
12
|
+
|
13
|
+
KTN_SPECIAL_TO_PTN = {
|
14
|
+
'C' => 'C',
|
15
|
+
'W' => 'S',
|
16
|
+
nil => ''
|
17
|
+
}
|
18
|
+
|
19
|
+
KTN_PLACEMENT = 'P'
|
20
|
+
KTN_MOVEMENT = 'M'
|
21
|
+
|
22
|
+
PTN_NORTH = '+'
|
23
|
+
PTN_SOUTH = '-'
|
24
|
+
PTN_RIGHT = '>'
|
25
|
+
PTN_LEFT = '<'
|
26
|
+
|
27
|
+
attr_reader :errors, :ptn_match, :direction, :special_piece
|
28
|
+
|
29
|
+
def initialize(notation, board_size = 5)
|
30
|
+
@ptn_match = NOTATION_REGEX.match(notation)
|
31
|
+
@board_size = board_size
|
32
|
+
@notation = notation
|
33
|
+
|
34
|
+
if @ptn_match
|
35
|
+
@number = @ptn_match[:number]
|
36
|
+
@special_piece = @ptn_match[:special_piece]
|
37
|
+
@position = @ptn_match[:position]
|
38
|
+
@direction = @ptn_match[:direction]
|
39
|
+
@stack = @ptn_match[:stack]
|
40
|
+
end
|
41
|
+
|
42
|
+
@errors = []
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_s
|
46
|
+
"#{@number}#{@special_piece}#{@position}#{@direction}#{@stack}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def type
|
50
|
+
@direction ? 'movement' : 'placement'
|
51
|
+
end
|
52
|
+
|
53
|
+
def position
|
54
|
+
[x, y]
|
55
|
+
end
|
56
|
+
|
57
|
+
def y
|
58
|
+
alpha_range[@position.chars.first] - 1
|
59
|
+
end
|
60
|
+
|
61
|
+
def x
|
62
|
+
@position.chars.last.to_i - 1
|
63
|
+
end
|
64
|
+
|
65
|
+
def stack_total
|
66
|
+
@stack_total ||= @stack ? @stack.chars.sum(&:to_i) : 0
|
67
|
+
end
|
68
|
+
|
69
|
+
def size
|
70
|
+
stack_total
|
71
|
+
end
|
72
|
+
|
73
|
+
def error(msg)
|
74
|
+
@errors << msg
|
75
|
+
end
|
76
|
+
|
77
|
+
# Placement of a special piece (Capstone or Standing) cannot also be a move
|
78
|
+
def movement_and_placement?
|
79
|
+
!!(@special_piece && @number || @direction || @stack)
|
80
|
+
end
|
81
|
+
|
82
|
+
def over_stack?
|
83
|
+
stack_total > @number.to_i
|
84
|
+
end
|
85
|
+
|
86
|
+
def under_stack?
|
87
|
+
stack_total < @number.to_i
|
88
|
+
end
|
89
|
+
|
90
|
+
def above_handsize?
|
91
|
+
stack_total > @board_size || @number.to_i > @board_size
|
92
|
+
end
|
93
|
+
|
94
|
+
def out_of_bounds?
|
95
|
+
x > @board_size || y.to_i > @board_size
|
96
|
+
end
|
97
|
+
|
98
|
+
def distrubutes_out_of_bounds?
|
99
|
+
x + stack_total > @board_size ||
|
100
|
+
y.to_i + stack_total > @board_size
|
101
|
+
end
|
102
|
+
|
103
|
+
def valid?
|
104
|
+
@valid ||= begin
|
105
|
+
# Break before we do anything else here.
|
106
|
+
error 'Did not match PTN Format' and return false unless @ptn_match
|
107
|
+
|
108
|
+
error 'Cannot move more pieces than the board size!' if above_handsize?
|
109
|
+
error 'Cannot distribute more pieces than were picked up' if over_stack?
|
110
|
+
error 'Cannot distribute less pieces than were picked up' if under_stack?
|
111
|
+
error 'Cannot move and place a piece' if movement_and_placement?
|
112
|
+
error 'Cannot place or move a piece out of bounds' if out_of_bounds?
|
113
|
+
error 'Cannot distribute pieces out of bounds' if distrubutes_out_of_bounds?
|
114
|
+
|
115
|
+
@errors.none?
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def alpha_range
|
120
|
+
@alpha_range ||= ('a'..'h').each.with_index(1).zip.flatten(1).to_h
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.from_ktn(move)
|
124
|
+
move_type, *remainder = move.split(' ')
|
125
|
+
|
126
|
+
ptn_string =
|
127
|
+
case move_type
|
128
|
+
when KTN_PLACEMENT
|
129
|
+
square, special_piece = remainder
|
130
|
+
stone = KTN_SPECIAL_TO_PTN[special_piece]
|
131
|
+
|
132
|
+
"#{stone}#{square.downcase}"
|
133
|
+
when KTN_MOVEMENT
|
134
|
+
from_square, to_square, *distribution = remainder
|
135
|
+
|
136
|
+
from_square_col, from_square_row = from_square.chars
|
137
|
+
to_square_col, to_square_row = to_square.chars
|
138
|
+
|
139
|
+
direction = if from_square_col == to_square_col
|
140
|
+
to_square_row > from_square_row ? PTN_NORTH : PTN_SOUTH
|
141
|
+
else
|
142
|
+
to_square_col > from_square_col ? PTN_RIGHT : PTN_LEFT
|
143
|
+
end
|
144
|
+
|
145
|
+
hand_size = distribution.map(&:to_i).sum
|
146
|
+
|
147
|
+
"#{hand_size}#{from_square.downcase}#{direction}#{distribution.join}"
|
148
|
+
else
|
149
|
+
''
|
150
|
+
end
|
151
|
+
|
152
|
+
new(ptn_string)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
data/lib/tak/version.rb
CHANGED
data/tak.gemspec
CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
22
|
spec.require_paths = ["lib"]
|
23
23
|
|
24
|
-
spec.add_development_dependency "bundler", "~>
|
24
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
25
25
|
spec.add_development_dependency "rake", "~> 10.0"
|
26
26
|
spec.add_development_dependency "rspec", "~> 3.0"
|
27
27
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tak
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brandon Weaver
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-03-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '2.0'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '2.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -64,19 +64,27 @@ files:
|
|
64
64
|
- ".travis.yml"
|
65
65
|
- CODE_OF_CONDUCT.md
|
66
66
|
- Gemfile
|
67
|
+
- Gemfile.lock
|
68
|
+
- Guardfile
|
67
69
|
- LICENSE.txt
|
68
70
|
- README.md
|
69
71
|
- Rakefile
|
70
72
|
- bin/console
|
71
73
|
- bin/setup
|
72
74
|
- lib/tak.rb
|
75
|
+
- lib/tak/board.rb
|
76
|
+
- lib/tak/game.rb
|
77
|
+
- lib/tak/move.rb
|
78
|
+
- lib/tak/piece_set.rb
|
79
|
+
- lib/tak/player.rb
|
80
|
+
- lib/tak/ptn.rb
|
73
81
|
- lib/tak/version.rb
|
74
82
|
- tak.gemspec
|
75
83
|
homepage: https://github.com/baweaver/tak
|
76
84
|
licenses:
|
77
85
|
- MIT
|
78
86
|
metadata: {}
|
79
|
-
post_install_message:
|
87
|
+
post_install_message:
|
80
88
|
rdoc_options: []
|
81
89
|
require_paths:
|
82
90
|
- lib
|
@@ -91,9 +99,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
91
99
|
- !ruby/object:Gem::Version
|
92
100
|
version: '0'
|
93
101
|
requirements: []
|
94
|
-
|
95
|
-
|
96
|
-
signing_key:
|
102
|
+
rubygems_version: 3.2.3
|
103
|
+
signing_key:
|
97
104
|
specification_version: 4
|
98
105
|
summary: Tak is a Tak board and ruleset handler
|
99
106
|
test_files: []
|