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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 81e9a8abccf6d37f5e596c350591f2a525f31f3e
4
- data.tar.gz: b2691c0cba1a8b48e056e018c2c04975da2751dc
2
+ SHA256:
3
+ metadata.gz: 3bc109da83ba6396965054bc21ede3b5a6dbc18d32f888470a99c1d86153d426
4
+ data.tar.gz: de8afe399c579542ccc04cb51662971ea0b76266bd6decdd56dbdb49122d2a9f
5
5
  SHA512:
6
- metadata.gz: a16b8e60677e841d70d6f2d1c1a4ff1d8eb8f2ed28009c3a620843bdc00570577a373df03781fabff22cd9f23f676e5795844fdd1822d14351b16c823af79b3a
7
- data.tar.gz: 4aef0aee1fc4ef45e21a5a921bbad10fba0e804d91f42437e081289b7ab94be17e24dcc5265378ae6c329a721d9d98e9db08a464254ad5cc451f0126aa7fff2a
6
+ metadata.gz: 456cfc6b766ea9f636cef4c22ea87baaa0fb56c26074fd44ee4043620202cba785d1a6e3c4d63b3e30f200efbd1a1b031a772af11ff784254bc24ae2e2358249
7
+ data.tar.gz: 46248cdf495efd2aef8b53e2ebe587101844292c6cc59ec70ddac0e7f243c8fc9527316364f4c447234c3589b468cb29a868e6d99c55fc17451a1e2e0413bc5b
data/.gitignore CHANGED
@@ -1,9 +1,9 @@
1
1
  /.bundle/
2
2
  /.yardoc
3
- /Gemfile.lock
4
3
  /_yardoc/
5
4
  /coverage/
6
5
  /doc/
7
6
  /pkg/
8
7
  /spec/reports/
9
8
  /tmp/
9
+ /*.gem
data/Gemfile CHANGED
@@ -2,3 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in tak.gemspec
4
4
  gemspec
5
+
6
+ gem 'guard'
7
+ gem 'guard-rspec'
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
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/tak`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- TODO: Write usage instructions here
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/[USERNAME]/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.
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
@@ -3,3 +3,9 @@ require "tak/version"
3
3
  module Tak
4
4
  # Your code goes here...
5
5
  end
6
+
7
+ require 'tak/ptn'
8
+ require 'tak/board'
9
+ require 'tak/piece_set'
10
+ require 'tak/player'
11
+ require 'tak/move'
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
@@ -0,0 +1,9 @@
1
+ module Tak
2
+ class Game
3
+ def initialize(size)
4
+ @tak_board = Tak::Board.new(size)
5
+ @turn = :white
6
+ @first_move = true
7
+ end
8
+ end
9
+ end
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
@@ -0,0 +1,8 @@
1
+ module Tak
2
+ class Player
3
+ def initialize(name, color)
4
+ @name = name
5
+ @color = color
6
+ end
7
+ end
8
+ end
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
@@ -1,3 +1,3 @@
1
1
  module Tak
2
- VERSION = "0.0.0"
2
+ VERSION = "0.0.1"
3
3
  end
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", "~> 1.13"
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.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: 2017-02-07 00:00:00.000000000 Z
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: '1.13'
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: '1.13'
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
- rubyforge_project:
95
- rubygems_version: 2.5.1
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: []