tak 0.0.0 → 0.0.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 +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: []
|