console-shogi 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +14 -0
  3. data/Gemfile.lock +44 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +1 -0
  6. data/Rakefile +4 -0
  7. data/images/gote/fu.png +0 -0
  8. data/images/gote/gin.png +0 -0
  9. data/images/gote/gyoku.png +0 -0
  10. data/images/gote/hisha.png +0 -0
  11. data/images/gote/kaku.png +0 -0
  12. data/images/gote/keima.png +0 -0
  13. data/images/gote/kin.png +0 -0
  14. data/images/gote/kyosha.png +0 -0
  15. data/images/gote/narigin.png +0 -0
  16. data/images/gote/narikei.png +0 -0
  17. data/images/gote/narikyo.png +0 -0
  18. data/images/gote/nonepiece.png +0 -0
  19. data/images/gote/ohsho.png +0 -0
  20. data/images/gote/ryu.png +0 -0
  21. data/images/gote/shori.png +0 -0
  22. data/images/gote/tokin.png +0 -0
  23. data/images/gote/uma.png +0 -0
  24. data/images/sente/fu.png +0 -0
  25. data/images/sente/gin.png +0 -0
  26. data/images/sente/gyoku.png +0 -0
  27. data/images/sente/hisha.png +0 -0
  28. data/images/sente/kaku.png +0 -0
  29. data/images/sente/keima.png +0 -0
  30. data/images/sente/kin.png +0 -0
  31. data/images/sente/kyosha.png +0 -0
  32. data/images/sente/narigin.png +0 -0
  33. data/images/sente/narikei.png +0 -0
  34. data/images/sente/narikyo.png +0 -0
  35. data/images/sente/nonepiece.png +0 -0
  36. data/images/sente/ohsho.png +0 -0
  37. data/images/sente/ryu.png +0 -0
  38. data/images/sente/shori.png +0 -0
  39. data/images/sente/tokin.png +0 -0
  40. data/images/sente/uma.png +0 -0
  41. data/lib/console_shogi/board.rb +53 -0
  42. data/lib/console_shogi/game.rb +57 -0
  43. data/lib/console_shogi/komadai.rb +59 -0
  44. data/lib/console_shogi/new_board_builder.rb +113 -0
  45. data/lib/console_shogi/piece.rb +327 -0
  46. data/lib/console_shogi/piece_mover.rb +82 -0
  47. data/lib/console_shogi/piece_mover_on_komadai.rb +65 -0
  48. data/lib/console_shogi/player.rb +42 -0
  49. data/lib/console_shogi/terminal_operator.rb +211 -0
  50. data/lib/console_shogi/version.rb +5 -0
  51. data/lib/console_shogi.rb +21 -0
  52. metadata +96 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3a6623ab23cf26496870d6849e0e3433035756ad6d0a4d17475f4afa4619c13a
4
+ data.tar.gz: d2ef4c45e1f79a20a7d7105a21d5b2b2d33a1373e6ef918c13471dd111478640
5
+ SHA512:
6
+ metadata.gz: eea768b6b1d02aedf1be04c06691b9ed5ba7e4ac4cce6e8da479cbf8ffc7ab1b3e7411018640f751b72556485d8edafbb1ecd9e4d116552050226152982a73af
7
+ data.tar.gz: 2559ca6fa74b5154ac626eb6c0de61dd15b0d48fe6758cce573672360f5d6eea0c980be9112017fefb9907a44046d0529eca66ae8622fca784aaacae537f60df
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in console-shogi.gemspec
6
+ gemspec
7
+
8
+ ruby "~> 3.2.0"
9
+
10
+ gem "rake", "~> 13.0"
11
+ gem "matrix", "~> 0.4.2"
12
+ gem "pry", "~> 0.14.2"
13
+
14
+ gem "tty-prompt", "~> 0.23.1"
data/Gemfile.lock ADDED
@@ -0,0 +1,44 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ console-shogi (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ coderay (1.1.3)
10
+ matrix (0.4.2)
11
+ method_source (1.0.0)
12
+ pastel (0.8.0)
13
+ tty-color (~> 0.5)
14
+ pry (0.14.2)
15
+ coderay (~> 1.1)
16
+ method_source (~> 1.0)
17
+ rake (13.0.6)
18
+ tty-color (0.6.0)
19
+ tty-cursor (0.7.1)
20
+ tty-prompt (0.23.1)
21
+ pastel (~> 0.8)
22
+ tty-reader (~> 0.8)
23
+ tty-reader (0.9.0)
24
+ tty-cursor (~> 0.7)
25
+ tty-screen (~> 0.8)
26
+ wisper (~> 2.0)
27
+ tty-screen (0.8.1)
28
+ wisper (2.0.1)
29
+
30
+ PLATFORMS
31
+ arm64-darwin-21
32
+
33
+ DEPENDENCIES
34
+ console-shogi!
35
+ matrix (~> 0.4.2)
36
+ pry (~> 0.14.2)
37
+ rake (~> 13.0)
38
+ tty-prompt (~> 0.23.1)
39
+
40
+ RUBY VERSION
41
+ ruby 3.2.2p53
42
+
43
+ BUNDLED WITH
44
+ 2.4.12
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 pekepek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1 @@
1
+ # console-shogi
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'matrix'
4
+
5
+ module ConsoleShogi
6
+ class Board
7
+ def initialize(pieces:)
8
+ @pieces = Matrix.rows(pieces)
9
+ end
10
+
11
+ def matrix
12
+ @pieces
13
+ end
14
+
15
+ def within_range?(x:, y:)
16
+ case {x: x, y: y}
17
+ in x: 0..8, y: 0..8
18
+ true
19
+ else
20
+ false
21
+ end
22
+ end
23
+
24
+ def fetch_piece(x:, y:)
25
+ pieces[y, x]
26
+ end
27
+
28
+ def promote_piece!(x:, y:)
29
+ @pieces[y, x] = pieces[y, x].promote
30
+ end
31
+
32
+ def put_piece!(piece:, to:)
33
+ @pieces[to[:y], to[:x]] = piece
34
+ end
35
+
36
+ def move_piece!(from:, to:)
37
+ from_piece = @pieces[from[:y], from[:x]]
38
+ @pieces[from[:y], from[:x]] = NonePiece.new
39
+
40
+ to_piece = @pieces[to[:y], to[:x]]
41
+
42
+ # NOTE 駒を取る、リファクタする
43
+ # from_piece.player で player 取るの違和感
44
+ from_piece.player.capture_piece!(to_piece) unless to_piece.none?
45
+
46
+ @pieces[to[:y], to[:x]] = from_piece
47
+ end
48
+
49
+ private
50
+
51
+ attr_reader :pieces
52
+ end
53
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+
5
+ module ConsoleShogi
6
+ class Game
7
+ def initialize
8
+ @sente_player = Player.new(teban: Player::Teban::SENTE)
9
+ @gote_player = Player.new(teban: Player::Teban::GOTE)
10
+ @board = NewBoardBuilder.build(sente_player: @sente_player, gote_player: @gote_player)
11
+ end
12
+
13
+ def start
14
+ TerminalOperator.print_board(board: board, sente_komadai: sente_player.komadai, gote_komadai: gote_player.komadai)
15
+
16
+ while key = STDIN.getch
17
+ # NOTE Ctrl-C を押したら終了
18
+ if key == "\C-c"
19
+ exit
20
+ # NOTE 矢印キーを押したらカーソルを移動
21
+ elsif key == "\e" && STDIN.getch == "["
22
+ key = STDIN.getch
23
+
24
+ TerminalOperator.move_cursor(key)
25
+ # NOTE Enter を押したら駒を移動
26
+ elsif key == "\r"
27
+ # TODO このまま Hash にするかは要検討
28
+ index = TerminalOperator.squares_index
29
+
30
+ case index[:location]
31
+ when :board
32
+ PieceMover.new(board: board, from: index).move!
33
+ when :sente_komadai
34
+ PieceMoverOnKomadai.new(board: board, komadai: sente_player.komadai, from: index).drop!
35
+ when :gote_komadai
36
+ PieceMoverOnKomadai.new(board: board, komadai: gote_player.komadai, from: index).drop!
37
+ end
38
+
39
+ # TODO 描写に時間がかかるので、差分のみ表示できる様にする
40
+ TerminalOperator.print_board(board: board, sente_komadai: sente_player.komadai, gote_komadai: gote_player.komadai)
41
+
42
+ # TODO ひとまず動く物を作った。リファクタリングする
43
+ [sente_player, gote_player].each do |player|
44
+ next unless player.win?
45
+
46
+ TerminalOperator.print_winner(player.teban)
47
+ exit
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ attr_reader :board, :sente_player, :gote_player
56
+ end
57
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'matrix'
4
+
5
+ module ConsoleShogi
6
+ class Komadai
7
+ class NoSpaceError < StandardError; end
8
+
9
+ attr_reader :pieces
10
+
11
+ def initialize
12
+ @pieces = empty_pieces
13
+ end
14
+
15
+ def expand!
16
+ @pieces = pieces.hstack(empty_pieces)
17
+ end
18
+
19
+ def have_space?
20
+ pieces.any?(&:none?)
21
+ end
22
+
23
+ def fetch_piece(x:, y:)
24
+ pieces[y, x]
25
+ end
26
+
27
+ def put(piece:)
28
+ index = pieces.index(&:none?)
29
+
30
+ raise NoSpaceError if index.nil?
31
+
32
+ @pieces[*index] = piece
33
+
34
+ @pieces = sort_pieces
35
+ end
36
+
37
+ def pick_up_piece!(from:)
38
+ @pieces[from[:y], from[:x]] = NonePiece.new
39
+ @pieces = sort_pieces
40
+ end
41
+
42
+ private
43
+
44
+ def sort_pieces
45
+ column_num = pieces.count / 3
46
+ Matrix.rows(pieces.sort_by {|p| -p.number }.each_slice(column_num).to_a)
47
+ end
48
+
49
+ def empty_pieces
50
+ Matrix.rows(
51
+ [
52
+ [NonePiece.new, NonePiece.new, NonePiece.new],
53
+ [NonePiece.new, NonePiece.new, NonePiece.new],
54
+ [NonePiece.new, NonePiece.new, NonePiece.new]
55
+ ]
56
+ )
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConsoleShogi
4
+ class NewBoardBuilder
5
+ class << self
6
+ def build(sente_player: , gote_player:)
7
+ Board.new(
8
+ pieces: [
9
+ [
10
+ Kyosha.new(player: gote_player),
11
+ Keima.new(player: gote_player),
12
+ Gin.new(player: gote_player),
13
+ Kin.new(player: gote_player),
14
+ Ohsho.new(player: gote_player),
15
+ Kin.new(player: gote_player),
16
+ Gin.new(player: gote_player),
17
+ Keima.new(player: gote_player),
18
+ Kyosha.new(player: gote_player)
19
+ ],
20
+ [
21
+ NonePiece.new,
22
+ Hisha.new(player: gote_player),
23
+ NonePiece.new,
24
+ NonePiece.new,
25
+ NonePiece.new,
26
+ NonePiece.new,
27
+ NonePiece.new,
28
+ Kaku.new(player: gote_player),
29
+ NonePiece.new
30
+ ],
31
+ [
32
+ Fu.new(player: gote_player),
33
+ Fu.new(player: gote_player),
34
+ Fu.new(player: gote_player),
35
+ Fu.new(player: gote_player),
36
+ Fu.new(player: gote_player),
37
+ Fu.new(player: gote_player),
38
+ Fu.new(player: gote_player),
39
+ Fu.new(player: gote_player),
40
+ Fu.new(player: gote_player)
41
+ ],
42
+ [
43
+ NonePiece.new,
44
+ NonePiece.new,
45
+ NonePiece.new,
46
+ NonePiece.new,
47
+ NonePiece.new,
48
+ NonePiece.new,
49
+ NonePiece.new,
50
+ NonePiece.new,
51
+ NonePiece.new
52
+ ],
53
+ [
54
+ NonePiece.new,
55
+ NonePiece.new,
56
+ NonePiece.new,
57
+ NonePiece.new,
58
+ NonePiece.new,
59
+ NonePiece.new,
60
+ NonePiece.new,
61
+ NonePiece.new,
62
+ NonePiece.new
63
+ ],
64
+ [
65
+ NonePiece.new,
66
+ NonePiece.new,
67
+ NonePiece.new,
68
+ NonePiece.new,
69
+ NonePiece.new,
70
+ NonePiece.new,
71
+ NonePiece.new,
72
+ NonePiece.new,
73
+ NonePiece.new
74
+ ],
75
+ [
76
+ Fu.new(player: sente_player),
77
+ Fu.new(player: sente_player),
78
+ Fu.new(player: sente_player),
79
+ Fu.new(player: sente_player),
80
+ Fu.new(player: sente_player),
81
+ Fu.new(player: sente_player),
82
+ Fu.new(player: sente_player),
83
+ Fu.new(player: sente_player),
84
+ Fu.new(player: sente_player)
85
+ ],
86
+ [
87
+ NonePiece.new,
88
+ Kaku.new(player: sente_player),
89
+ NonePiece.new,
90
+ NonePiece.new,
91
+ NonePiece.new,
92
+ NonePiece.new,
93
+ NonePiece.new,
94
+ Hisha.new(player: sente_player),
95
+ NonePiece.new
96
+ ],
97
+ [
98
+ Kyosha.new(player: sente_player),
99
+ Keima.new(player: sente_player),
100
+ Gin.new(player: sente_player),
101
+ Kin.new(player: sente_player),
102
+ Ohsho.new(player: sente_player),
103
+ Kin.new(player: sente_player),
104
+ Gin.new(player: sente_player),
105
+ Keima.new(player: sente_player),
106
+ Kyosha.new(player: sente_player)
107
+ ]
108
+ ]
109
+ )
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,327 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConsoleShogi
4
+ class Piece
5
+ attr_reader :player
6
+
7
+ def initialize(player: nil)
8
+ @player = player
9
+ end
10
+
11
+ def move
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def promote
16
+ return self unless can_promote?
17
+
18
+ promoted
19
+ end
20
+
21
+ def promoted?
22
+ false
23
+ end
24
+
25
+ def number
26
+ self::class::NUMBER
27
+ end
28
+
29
+ def image
30
+ if player.sente?
31
+ File.read("images/sente/#{self::class.name.split('::').last.downcase}.png")
32
+ else
33
+ File.read("images/gote/#{self::class.name.split('::').last.downcase}.png")
34
+ end
35
+ end
36
+
37
+ def none?
38
+ self::class == NonePiece
39
+ end
40
+
41
+ def kaku?
42
+ self::class == Kaku
43
+ end
44
+
45
+ def hisha?
46
+ self::class == Hisha
47
+ end
48
+
49
+ def kyosha?
50
+ self::class == Kyosha
51
+ end
52
+
53
+ def fu?
54
+ self::class == Fu
55
+ end
56
+
57
+ def moves
58
+ ms = base_moves
59
+
60
+ if player.sente?
61
+ ms
62
+ else
63
+ ms.map {|m| m.transform_values {|v| v * -1 } }
64
+ end
65
+ end
66
+
67
+ def change_player!(player)
68
+ @player = player
69
+ end
70
+ end
71
+
72
+ class NonePiece < Piece
73
+ NUMBER = 0
74
+
75
+ def moves
76
+ []
77
+ end
78
+
79
+ def player
80
+ NonPlayer.new
81
+ end
82
+
83
+ def can_promote?
84
+ false
85
+ end
86
+ end
87
+
88
+ class Fu < Piece
89
+ NUMBER = 1
90
+
91
+ def base_moves
92
+ [
93
+ {x: 0, y: -1}
94
+ ]
95
+ end
96
+
97
+ def can_promote?
98
+ true
99
+ end
100
+
101
+ def promoted
102
+ PromotedPiece::Tokin.new(original: self)
103
+ end
104
+ end
105
+
106
+ class Kyosha < Piece
107
+ NUMBER = 2
108
+
109
+ def base_moves
110
+ (1..8).map {|n|
111
+ {x: 0, y: -1 * n}
112
+ }
113
+ end
114
+
115
+ def can_promote?
116
+ true
117
+ end
118
+
119
+ def promoted
120
+ PromotedPiece::Narikyo.new(original: self)
121
+ end
122
+ end
123
+
124
+ class Keima < Piece
125
+ NUMBER = 3
126
+
127
+ def base_moves
128
+ [
129
+ {x: 1, y: -2},
130
+ {x: -1, y: -2},
131
+ ]
132
+ end
133
+
134
+ def can_promote?
135
+ true
136
+ end
137
+
138
+ def promoted
139
+ PromotedPiece::Narikei.new(original: self)
140
+ end
141
+ end
142
+
143
+ class Gin < Piece
144
+ NUMBER = 4
145
+
146
+ def base_moves
147
+ [
148
+ {x: -1, y: -1},
149
+ {x: 0, y: -1},
150
+ {x: 1, y: -1},
151
+ {x: 1, y: 1},
152
+ {x: -1, y: 1}
153
+ ]
154
+ end
155
+
156
+ def can_promote?
157
+ true
158
+ end
159
+
160
+ def promoted
161
+ PromotedPiece::Narigin.new(original: self)
162
+ end
163
+ end
164
+
165
+ class Kin < Piece
166
+ NUMBER = 5
167
+
168
+ def base_moves
169
+ [
170
+ {x: -1, y: -1},
171
+ {x: 0, y: -1},
172
+ {x: 1, y: -1},
173
+ {x: 1, y: 0},
174
+ {x: -1, y: 0},
175
+ {x: 0, y: 1}
176
+ ]
177
+ end
178
+
179
+ def can_promote?
180
+ false
181
+ end
182
+ end
183
+
184
+ class Kaku < Piece
185
+ NUMBER = 6
186
+
187
+ def base_moves
188
+ (1..8).flat_map {|n|
189
+ [
190
+ {x: -1 * n, y: -1 * n},
191
+ {x: 1 * n, y: -1 * n},
192
+ {x: -1 * n, y: 1 * n},
193
+ {x: 1 * n, y: 1 * n}
194
+ ]
195
+ }
196
+ end
197
+
198
+ def can_promote?
199
+ true
200
+ end
201
+
202
+ def promoted
203
+ PromotedPiece::Uma.new(original: self)
204
+ end
205
+ end
206
+
207
+ class Hisha < Piece
208
+ NUMBER = 7
209
+
210
+ def base_moves
211
+ (1..8).flat_map {|n|
212
+ [
213
+ {x: 0, y: -1 * n},
214
+ {x: 0, y: 1 * n},
215
+ {x: -1 * n, y: 0},
216
+ {x: 1 * n, y: 0}
217
+ ]
218
+ }
219
+ end
220
+
221
+ def can_promote?
222
+ true
223
+ end
224
+
225
+ def promoted
226
+ PromotedPiece::Ryu.new(original: self)
227
+ end
228
+ end
229
+
230
+ class Ohsho < Piece
231
+ NUMBER = 8
232
+
233
+ def base_moves
234
+ [
235
+ {x: -1, y: -1},
236
+ {x: 0, y: -1},
237
+ {x: 1, y: -1},
238
+ {x: 1, y: 0},
239
+ {x: -1, y: 0},
240
+ {x: -1, y: 1},
241
+ {x: 0, y: 1},
242
+ {x: 1, y: 1}
243
+ ]
244
+ end
245
+
246
+ def can_promote?
247
+ false
248
+ end
249
+ end
250
+
251
+ class PromotedPiece < Piece
252
+ attr_reader :player, :original
253
+
254
+ def initialize(original: nil)
255
+ @player = original.player
256
+ @original = original
257
+ end
258
+
259
+ def can_promote?
260
+ false
261
+ end
262
+
263
+ def promoted?
264
+ true
265
+ end
266
+
267
+ class NariKin < PromotedPiece
268
+ NUMBER = 9
269
+
270
+ def base_moves
271
+ [
272
+ {x: -1, y: -1},
273
+ {x: 0, y: -1},
274
+ {x: 1, y: -1},
275
+ {x: 1, y: 0},
276
+ {x: -1, y: 0},
277
+ {x: 0, y: 1}
278
+ ]
279
+ end
280
+ end
281
+
282
+ class Narigin < NariKin; end
283
+ class Narikei < NariKin; end
284
+ class Narikyo < NariKin; end
285
+ class Tokin < NariKin; end
286
+
287
+ class Uma < PromotedPiece
288
+ NUMBER = 10
289
+
290
+ def base_moves
291
+ (1..8).flat_map {|n|
292
+ [
293
+ {x: -1 * n, y: -1 * n},
294
+ {x: 1 * n, y: -1 * n},
295
+ {x: -1 * n, y: 1 * n},
296
+ {x: 1 * n, y: 1 * n}
297
+ ]
298
+ } + [
299
+ {x: 0, y: -1},
300
+ {x: 1, y: 0},
301
+ {x: -1, y: 0},
302
+ {x: 0, y: 1}
303
+ ]
304
+ end
305
+ end
306
+
307
+ class Ryu < PromotedPiece
308
+ NUMBER = 11
309
+
310
+ def base_moves
311
+ (1..8).flat_map {|n|
312
+ [
313
+ {x: 0, y: -1 * n},
314
+ {x: 0, y: 1 * n},
315
+ {x: -1 * n, y: 0},
316
+ {x: 1 * n, y: 0}
317
+ ]
318
+ } + [
319
+ {x: -1, y: -1},
320
+ {x: 1, y: -1},
321
+ {x: -1, y: 1},
322
+ {x: 1, y: 1}
323
+ ]
324
+ end
325
+ end
326
+ end
327
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConsoleShogi
4
+ class PieceMover
5
+ def initialize(board:, from:)
6
+ @board = board
7
+ @from_piece_index = from
8
+ end
9
+
10
+ def move!
11
+ return if target_piece.nil? || target_piece.none?
12
+
13
+ while key = STDIN.getch
14
+ if key == "\e" && STDIN.getch == "["
15
+ key = STDIN.getch
16
+
17
+ TerminalOperator.move_cursor(key)
18
+ elsif key == "\r"
19
+ to_piece_index = TerminalOperator.squares_index
20
+
21
+ return unless can_move?(piece: target_piece, to_piece_index: to_piece_index)
22
+
23
+ board.move_piece!(
24
+ from: {x: from_piece_index[:x], y: from_piece_index[:y]},
25
+ to: {x: to_piece_index[:x], y: to_piece_index[:y]}
26
+ )
27
+
28
+ # TODO とりあえずここに実装してしまっている。整理したい
29
+ piece = board.fetch_piece(x: to_piece_index[:x], y: to_piece_index[:y])
30
+ return unless can_promote?(piece, from_piece_index, to_piece_index)
31
+
32
+ board.promote_piece!(x: to_piece_index[:x], y: to_piece_index[:y]) if TerminalOperator.select_promotion
33
+
34
+ return
35
+ end
36
+ end
37
+ end
38
+
39
+ def can_move?(piece:, to_piece_index:)
40
+ return false unless board.within_range?(x: to_piece_index[:x], y: to_piece_index[:y])
41
+
42
+ diff = {x: to_piece_index[:x] - from_piece_index[:x], y: to_piece_index[:y] - from_piece_index[:y]}
43
+
44
+ return false if piece.moves.none? {|m| m[:x] == diff[:x] && m[:y] == diff[:y] }
45
+
46
+ to_piece = board.fetch_piece(x: to_piece_index[:x], y: to_piece_index[:y])
47
+
48
+ return false if piece.player.teban == to_piece.player.teban
49
+
50
+ return true unless piece.kaku? || piece.hisha? || piece.kyosha?
51
+
52
+ distance = (diff[:x].nonzero? || diff[:y]).abs
53
+ element = [diff[:x] / distance, diff[:y] / distance]
54
+
55
+ 1.upto(distance - 1) do |d|
56
+ piece = board.fetch_piece(x: from_piece_index[:x] + element[0] * d, y: from_piece_index[:y] + element[1] * d)
57
+
58
+ return false unless piece.none?
59
+ end
60
+
61
+ true
62
+ end
63
+
64
+ private
65
+
66
+ attr_reader :board, :from_piece_index
67
+
68
+ def target_piece
69
+ @target_piece ||= board.fetch_piece(x: from_piece_index[:x], y: from_piece_index[:y])
70
+ end
71
+
72
+ def can_promote?(piece, from, to)
73
+ return false unless piece.can_promote?
74
+
75
+ if piece.player.sente?
76
+ from[:y].between?(0, 2) || to[:y].between?(0, 2)
77
+ elsif piece.player.gote?
78
+ from[:y].between?(6, 8) || to[:y].between?(6, 8)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConsoleShogi
4
+ class PieceMoverOnKomadai
5
+ def initialize(board:, komadai:, from:)
6
+ @board = board
7
+ @komadai = komadai
8
+ # 駒台の index に直す必要がある
9
+ @from_piece_index = from
10
+ end
11
+
12
+ def drop!
13
+ return if target_piece.nil? || target_piece.none?
14
+
15
+ while key = STDIN.getch
16
+ if key == "\e" && STDIN.getch == "["
17
+ key = STDIN.getch
18
+
19
+ TerminalOperator.move_cursor(key)
20
+ elsif key == "\r"
21
+ to_piece_index = TerminalOperator.squares_index
22
+
23
+ return if to_piece_index[:location] != :board
24
+ return unless can_drop?(piece: target_piece, to_piece_index: to_piece_index)
25
+
26
+ komadai.pick_up_piece!(from: from_piece_index)
27
+ board.put_piece!(piece: target_piece, to: to_piece_index)
28
+
29
+ return
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :board, :komadai, :from_piece_index
37
+
38
+ def target_piece
39
+ @target_piece ||= komadai.fetch_piece(x: from_piece_index[:x], y: from_piece_index[:y])
40
+ end
41
+
42
+ def can_drop?(piece:, to_piece_index:)
43
+ to_piece = board.fetch_piece(x: to_piece_index[:x], y: to_piece_index[:y])
44
+
45
+ return false unless to_piece.none?
46
+
47
+ return false if nifu?(piece, to_piece_index)
48
+
49
+ can_move_next_turn?(piece, to_piece_index)
50
+ end
51
+
52
+ def can_move_next_turn?(piece, to_piece_index)
53
+ piece_mover = PieceMover.new(board: board, from: to_piece_index)
54
+
55
+ piece.moves.any? {|m|
56
+ piece_mover.can_move?(piece: piece, to_piece_index: {x: to_piece_index[:x] + m[:x], y: to_piece_index[:y] + m[:y]})
57
+ }
58
+ end
59
+
60
+ def nifu?(piece, to_piece_index)
61
+ piece.fu? &&
62
+ board.matrix.column(to_piece_index[:x]).any? {|p| p.fu? && piece.player == p.player }
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConsoleShogi
4
+ class Player
5
+ module Teban
6
+ SENTE = :sente
7
+ GOTE = :gote
8
+ end
9
+
10
+ attr_reader :teban, :komadai
11
+
12
+ def initialize(teban: nil)
13
+ @teban = teban
14
+ # TODO Komadai, Piece, Player が循環してる
15
+ @komadai = Komadai.new
16
+ end
17
+
18
+ def sente?
19
+ teban == Teban::SENTE
20
+ end
21
+
22
+ def gote?
23
+ teban == Teban::GOTE
24
+ end
25
+
26
+ def win?
27
+ komadai.pieces.any? {|p| p.class == Ohsho }
28
+ end
29
+
30
+ def capture_piece!(piece)
31
+ @komadai.expand! unless komadai.have_space?
32
+
33
+ piece = piece.original if piece.promoted?
34
+
35
+ piece.change_player!(self)
36
+ komadai.put(piece: piece)
37
+ end
38
+ end
39
+
40
+ class NonPlayer < Player
41
+ end
42
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'tty-prompt'
5
+
6
+ module ConsoleShogi
7
+ class TerminalOperator
8
+ class << self
9
+ CursorPosition = Struct.new(:x, :y)
10
+
11
+ HORIZONTAL_DISTANCE = 2
12
+
13
+ KOMADAI_START_X = 21
14
+ KOMADAI_GOTE_START_Y = 1
15
+ KOMADAI_SENTE_START_Y = 7
16
+
17
+ module EscapeSequence
18
+ RESET = "\e[0m"
19
+ RESET_CURSOR = "\e[1;1H"
20
+ BACKGROUND_COLOR_YELLOW = "\e[43m"
21
+ BACKGROUND_COLOR_GREEN = "\e[42m"
22
+ TEXT_COLOR_SENTE = "\e[37m"
23
+ TEXT_COLOR_GOTE = "\e[30m"
24
+ end
25
+
26
+ module Location
27
+ NONE = :none
28
+ BOARD = :board
29
+ SENTE_KOMADAI = :sente_komadai
30
+ GOTE_KOMADAI = :gote_komadai
31
+ end
32
+
33
+ def cursor_position
34
+ @cursor_position ||= CursorPosition.new(x: 1, y: 1)
35
+ end
36
+
37
+ def image_height
38
+ @image_height ||= calculate_fit_image_height
39
+ end
40
+
41
+ def print_board(board:, sente_komadai:, gote_komadai:)
42
+ # NOTE 画面をクリア
43
+ print "\e[2J"
44
+
45
+ # NOTE カーソルを1行1列に移動
46
+ print EscapeSequence::RESET_CURSOR
47
+
48
+ board.matrix.row_vectors.each_with_index do |vector, i|
49
+ vector.each_with_index do |piece, j|
50
+ print_image(image: piece.image, height: image_height)
51
+ end
52
+
53
+ print "#{EscapeSequence::RESET}\n"
54
+ end
55
+
56
+ # NOTE 駒台を表示
57
+ print_komadai(sente_komadai, KOMADAI_SENTE_START_Y, EscapeSequence::TEXT_COLOR_SENTE)
58
+ print_komadai(gote_komadai, KOMADAI_GOTE_START_Y, EscapeSequence::TEXT_COLOR_GOTE)
59
+
60
+ # NOTE back a cursor
61
+ back_to_cursor
62
+ end
63
+
64
+ def active_piece(board:, piece_index:)
65
+ back_to_cursor
66
+
67
+ board.fetch_piece(x: piece_index[:x], y: piece_index[:y])
68
+ end
69
+
70
+ def move_cursor(key)
71
+ return unless %w(A B C D).include?(key)
72
+
73
+ direction =
74
+ case key
75
+ when 'A', 'B'
76
+ key
77
+ when 'C', 'D'
78
+ "#{HORIZONTAL_DISTANCE}#{key}"
79
+ end
80
+
81
+ print "\e[#{direction}"
82
+
83
+ reload_cursor_position_in_stdin!
84
+ end
85
+
86
+ # TODO 駒台の大きさが変わるのをどうするか考える、メソッド名考える
87
+ def squares_index
88
+ case cursor_position
89
+ in x: 1..18, y: 1..9
90
+ {x: (cursor_position.x - 1) / HORIZONTAL_DISTANCE, y: cursor_position.y - 1, location: Location::BOARD}
91
+ in x: 21..38, y: 1..3
92
+ {x: (cursor_position.x - 21) / HORIZONTAL_DISTANCE, y: cursor_position.y - 1, location: Location::GOTE_KOMADAI}
93
+ in x: 21..38, y: 7..9
94
+ {x: (cursor_position.x - 21) / HORIZONTAL_DISTANCE, y: cursor_position.y - 7, location: Location::SENTE_KOMADAI}
95
+ else
96
+ {x: (cursor_position.x - 1) / HORIZONTAL_DISTANCE, y: cursor_position.y - 1, location: Location::NONE}
97
+ end
98
+ end
99
+
100
+ def select_promotion
101
+ print "\e[10;1H"
102
+
103
+ prompt = TTY::Prompt.new
104
+
105
+ prompt.select('成りますか?', show_help: :never) {|menu|
106
+ menu.choice '成る', true
107
+ menu.choice '成らない', false
108
+ }
109
+ end
110
+
111
+ def print_winner(teban)
112
+ print "\e[4;7H"
113
+
114
+ print_image(image: File.read("images/#{teban}/shori.png"), height: image_height * 3)
115
+
116
+ print "\e[10;1H"
117
+ end
118
+
119
+ private
120
+
121
+ IMAGE_HEIGHT_PIXEL = 240
122
+
123
+ # TODO 無理やりなので、実現方法を再考する
124
+ def calculate_fit_image_height
125
+ start_position = fetch_cursor_position_in_stdin
126
+ # NOTE NonePiece を選んでいることに意味はない
127
+ print_image(image: NonePiece.new.image)
128
+ end_position = fetch_cursor_position_in_stdin
129
+
130
+ fixed_height = IMAGE_HEIGHT_PIXEL / (end_position[1].to_i - start_position[1].to_i)
131
+
132
+ print "\e[2J"
133
+ # NOTE カーソルを1行1列に移動
134
+ print EscapeSequence::RESET_CURSOR
135
+
136
+ fixed_height
137
+ end
138
+
139
+ # TODO view 用の Player 作って整理する
140
+ def print_komadai(komadai, start_y, text_color)
141
+ komadai.pieces.row_vectors.each_with_index do |row_pieces, i|
142
+ print "\e[#{start_y + i};#{KOMADAI_START_X}H"
143
+ print EscapeSequence::BACKGROUND_COLOR_YELLOW
144
+ print text_color
145
+
146
+ row_pieces.each do |p|
147
+ print_image(image: p.image, height: image_height)
148
+ end
149
+
150
+ print EscapeSequence::RESET
151
+ end
152
+ end
153
+
154
+ def fetch_cursor_position_in_stdin
155
+ stdout = ''
156
+
157
+ $stdin.raw do |stdin|
158
+ $stdout << "\e[6n"
159
+ $stdout.flush
160
+
161
+ while (c = stdin.getc) != 'R'
162
+ stdout += c
163
+ end
164
+ end
165
+
166
+ stdout.match /(\d+);(\d+)/
167
+ end
168
+
169
+ def reload_cursor_position_in_stdin!
170
+ positions = fetch_cursor_position_in_stdin
171
+
172
+ @cursor_position = CursorPosition.new(x: positions[2].to_i, y: positions[1].to_i)
173
+ end
174
+
175
+ def back_to_cursor
176
+ print "\e[#{cursor_position.y};#{cursor_position.x}H"
177
+ end
178
+
179
+ # TODO 画像を表示するためのメソッド、場所検討
180
+ def print_image(image:, width: nil, height: nil)
181
+ encoded_image = Base64.strict_encode64(image)
182
+
183
+ print_osc
184
+ print "1337;File=inline=1"
185
+ print ";size=#{image.size}"
186
+ print ";name=#{encoded_image}"
187
+ print ";width=#{width}px" unless width.nil?
188
+ print ";height=#{height}px" unless height.nil?
189
+ print ":#{encoded_image}"
190
+ print_st
191
+ end
192
+
193
+ def print_osc
194
+ # NOTE \e] で始まるシーケンスは全てのターミナルで扱えるわけでは無い
195
+ if ENV['TERM'].start_with?('screen') || ENV['TERM'].start_with?('tmux')
196
+ print "\ePtmux;\e\e]"
197
+ else
198
+ print "\e]"
199
+ end
200
+ end
201
+
202
+ def print_st
203
+ if ENV['TERM'].start_with?('screen') || ENV['TERM'].start_with?('tmux')
204
+ print "\a\e\\"
205
+ else
206
+ print "\a"
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConsoleShogi
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'console_shogi/version'
4
+ require_relative 'console_shogi/game'
5
+ require_relative 'console_shogi/player'
6
+ require_relative 'console_shogi/board'
7
+ require_relative 'console_shogi/piece'
8
+ require_relative 'console_shogi/komadai'
9
+
10
+ require_relative 'console_shogi/terminal_operator'
11
+ require_relative 'console_shogi/new_board_builder'
12
+ require_relative 'console_shogi/piece_mover'
13
+ require_relative 'console_shogi/piece_mover_on_komadai'
14
+
15
+ module ConsoleShogi
16
+ class Error < StandardError; end
17
+
18
+ def self.game_start
19
+ Game.new.start
20
+ end
21
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: console-shogi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - pekepek
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-05-24 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: You can play the Shogi in terminal
14
+ email:
15
+ - ishihata@sansan.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - Gemfile
21
+ - Gemfile.lock
22
+ - LICENSE.txt
23
+ - README.md
24
+ - Rakefile
25
+ - images/gote/fu.png
26
+ - images/gote/gin.png
27
+ - images/gote/gyoku.png
28
+ - images/gote/hisha.png
29
+ - images/gote/kaku.png
30
+ - images/gote/keima.png
31
+ - images/gote/kin.png
32
+ - images/gote/kyosha.png
33
+ - images/gote/narigin.png
34
+ - images/gote/narikei.png
35
+ - images/gote/narikyo.png
36
+ - images/gote/nonepiece.png
37
+ - images/gote/ohsho.png
38
+ - images/gote/ryu.png
39
+ - images/gote/shori.png
40
+ - images/gote/tokin.png
41
+ - images/gote/uma.png
42
+ - images/sente/fu.png
43
+ - images/sente/gin.png
44
+ - images/sente/gyoku.png
45
+ - images/sente/hisha.png
46
+ - images/sente/kaku.png
47
+ - images/sente/keima.png
48
+ - images/sente/kin.png
49
+ - images/sente/kyosha.png
50
+ - images/sente/narigin.png
51
+ - images/sente/narikei.png
52
+ - images/sente/narikyo.png
53
+ - images/sente/nonepiece.png
54
+ - images/sente/ohsho.png
55
+ - images/sente/ryu.png
56
+ - images/sente/shori.png
57
+ - images/sente/tokin.png
58
+ - images/sente/uma.png
59
+ - lib/console_shogi.rb
60
+ - lib/console_shogi/board.rb
61
+ - lib/console_shogi/game.rb
62
+ - lib/console_shogi/komadai.rb
63
+ - lib/console_shogi/new_board_builder.rb
64
+ - lib/console_shogi/piece.rb
65
+ - lib/console_shogi/piece_mover.rb
66
+ - lib/console_shogi/piece_mover_on_komadai.rb
67
+ - lib/console_shogi/player.rb
68
+ - lib/console_shogi/terminal_operator.rb
69
+ - lib/console_shogi/version.rb
70
+ homepage: https://github.com/pekepek/console-shogi
71
+ licenses:
72
+ - MIT
73
+ metadata:
74
+ homepage_uri: https://github.com/pekepek/console-shogi
75
+ source_code_uri: https://github.com/pekepek/console-shogi
76
+ changelog_uri: https://github.com/pekepek/console-shogi/releases
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 2.6.0
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.4.10
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: You can play the Shogi in terminal
96
+ test_files: []