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 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: []