shogiops 0.2.9 → 0.6.0
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.
- package/README.md +6 -7
- package/attacks.js +7 -7
- package/attacks.js.map +1 -1
- package/board.d.ts +3 -0
- package/board.js +31 -0
- package/board.js.map +1 -1
- package/compat.d.ts +6 -24
- package/compat.js +17 -173
- package/compat.js.map +1 -1
- package/debug.js +30 -56
- package/debug.js.map +1 -1
- package/hand.d.ts +38 -0
- package/hand.js +74 -0
- package/hand.js.map +1 -0
- package/hash.d.ts +4 -3
- package/hash.js +9 -9
- package/hash.js.map +1 -1
- package/index.d.ts +13 -10
- package/index.js +14 -15
- package/index.js.map +1 -1
- package/{csa.d.ts → notation/csa/csa.d.ts} +9 -9
- package/{csa.js → notation/csa/csa.js} +52 -56
- package/notation/csa/csa.js.map +1 -0
- package/notation/japanese.d.ts +3 -0
- package/notation/japanese.js +65 -0
- package/notation/japanese.js.map +1 -0
- package/{kif.d.ts → notation/kif/kif.d.ts} +10 -9
- package/{kif.js → notation/kif/kif.js} +86 -84
- package/notation/kif/kif.js.map +1 -0
- package/{kifHandicaps.d.ts → notation/kif/kifHandicaps.d.ts} +0 -0
- package/{kifHandicaps.js → notation/kif/kifHandicaps.js} +5 -0
- package/notation/kif/kifHandicaps.js.map +1 -0
- package/notation/kitaoKawasaki.d.ts +3 -0
- package/notation/kitaoKawasaki.js +30 -0
- package/notation/kitaoKawasaki.js.map +1 -0
- package/notation/kitaokawasaki.d.ts +0 -0
- package/notation/kitaokawasaki.js +2 -0
- package/notation/kitaokawasaki.js.map +1 -0
- package/notation/notationUtil.d.ts +12 -0
- package/{kifUtil.js → notation/notationUtil.js} +43 -32
- package/notation/notationUtil.js.map +1 -0
- package/notation/western.d.ts +3 -0
- package/notation/western.js +27 -0
- package/notation/western.js.map +1 -0
- package/notation/westernEngine.d.ts +3 -0
- package/notation/westernEngine.js +27 -0
- package/notation/westernEngine.js.map +1 -0
- package/package.json +51 -36
- package/setup.d.ts +2 -32
- package/setup.js +3 -74
- package/setup.js.map +1 -1
- package/sfen.d.ts +33 -0
- package/sfen.js +201 -0
- package/sfen.js.map +1 -0
- package/shogi.d.ts +14 -7
- package/shogi.js +109 -106
- package/shogi.js.map +1 -1
- package/squareSet.d.ts +1 -5
- package/squareSet.js +0 -13
- package/squareSet.js.map +1 -1
- package/src/board.ts +34 -0
- package/src/compat.ts +18 -197
- package/src/debug.ts +26 -56
- package/src/hand.ts +94 -0
- package/src/hash.ts +8 -7
- package/src/index.ts +16 -12
- package/src/{csa.ts → notation/csa/csa.ts} +50 -51
- package/src/notation/japanese.ts +74 -0
- package/src/{kif.ts → notation/kif/kif.ts} +77 -78
- package/src/{kifHandicaps.ts → notation/kif/kifHandicaps.ts} +5 -0
- package/src/notation/kitaoKawasaki.ts +24 -0
- package/src/{kifUtil.ts → notation/notationUtil.ts} +37 -28
- package/src/notation/western.ts +21 -0
- package/src/notation/westernEngine.ts +21 -0
- package/src/setup.ts +4 -91
- package/src/sfen.ts +190 -0
- package/src/shogi.ts +75 -75
- package/src/squareSet.ts +1 -18
- package/src/transform.ts +3 -1
- package/src/types.ts +13 -16
- package/src/util.ts +8 -73
- package/src/variant.ts +44 -3
- package/src/variantUtil.ts +160 -0
- package/transform.js +3 -1
- package/transform.js.map +1 -1
- package/types.d.ts +3 -7
- package/types.js +13 -14
- package/types.js.map +1 -1
- package/util.d.ts +4 -7
- package/util.js +9 -60
- package/util.js.map +1 -1
- package/variant.d.ts +12 -1
- package/variant.js +38 -3
- package/variant.js.map +1 -1
- package/variantUtil.d.ts +13 -0
- package/variantUtil.js +160 -0
- package/variantUtil.js.map +1 -0
- package/csa.js.map +0 -1
- package/csaUtil.d.ts +0 -3
- package/csaUtil.js +0 -21
- package/csaUtil.js.map +0 -1
- package/fen.d.ts +0 -32
- package/fen.js +0 -197
- package/fen.js.map +0 -1
- package/kif.js.map +0 -1
- package/kifHandicaps.js.map +0 -1
- package/kifUtil.d.ts +0 -9
- package/kifUtil.js.map +0 -1
- package/san.d.ts +0 -6
- package/san.js +0 -135
- package/san.js.map +0 -1
- package/src/csaUtil.ts +0 -15
- package/src/fen.ts +0 -183
- package/src/san.ts +0 -136
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { Result } from '@badrap/result';
|
|
2
|
-
import { Board } from '
|
|
3
|
-
import {
|
|
4
|
-
import { Position } from '
|
|
5
|
-
import { Color, isDrop, Move
|
|
6
|
-
import { csaToRole, defined,
|
|
7
|
-
import {
|
|
2
|
+
import { Board } from '../../board';
|
|
3
|
+
import { Setup } from '../../setup';
|
|
4
|
+
import { Position } from '../../shogi';
|
|
5
|
+
import { Color, isDrop, Move } from '../../types';
|
|
6
|
+
import { csaToRole, defined, roleToCsa } from '../../util';
|
|
7
|
+
import { Hand, Hands } from '../../hand';
|
|
8
|
+
import { allRoles, handRoles, promote } from '../../variantUtil';
|
|
9
|
+
import { makeNumberSquare, parseNumberSquare } from '../notationUtil';
|
|
10
|
+
|
|
11
|
+
// Olny supports standard shogi no variants
|
|
8
12
|
|
|
9
13
|
//
|
|
10
14
|
// CSA HEADER
|
|
@@ -14,7 +18,7 @@ export enum InvalidCsa {
|
|
|
14
18
|
CSA = 'ERR_CSA',
|
|
15
19
|
Board = 'ERR_BOARD',
|
|
16
20
|
Handicap = 'ERR_HANDICAP',
|
|
17
|
-
|
|
21
|
+
Hands = 'ERR_HANDS',
|
|
18
22
|
AdditionalInfo = 'ERR_ADDITIONAL',
|
|
19
23
|
}
|
|
20
24
|
|
|
@@ -24,8 +28,8 @@ export class CsaError extends Error {}
|
|
|
24
28
|
export function makeCsaHeader(setup: Setup): string {
|
|
25
29
|
return [
|
|
26
30
|
makeCsaBoard(setup.board),
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
makeCsaHand(setup.hands.sente, 'P+'),
|
|
32
|
+
makeCsaHand(setup.hands.gote, 'P-'),
|
|
29
33
|
setup.turn === 'gote' ? '-' : '+',
|
|
30
34
|
]
|
|
31
35
|
.filter(p => p.length > 0)
|
|
@@ -50,15 +54,16 @@ export function makeCsaBoard(board: Board): string {
|
|
|
50
54
|
return csaBoard;
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
export function
|
|
54
|
-
if (
|
|
57
|
+
export function makeCsaHand(hand: Hand, prefix: string): string {
|
|
58
|
+
if (hand.isEmpty()) return '';
|
|
55
59
|
return (
|
|
56
60
|
prefix +
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
handRoles('shogi')
|
|
62
|
+
.map(role => {
|
|
63
|
+
const r = roleToCsa(role);
|
|
64
|
+
const n = hand[role];
|
|
65
|
+
return ('00' + r).repeat(Math.min(n, 18));
|
|
66
|
+
})
|
|
62
67
|
.filter(p => p.length > 0)
|
|
63
68
|
.join('')
|
|
64
69
|
);
|
|
@@ -75,7 +80,7 @@ export function parseCsaHeader(csa: string): Result<Setup, CsaError> {
|
|
|
75
80
|
return baseBoard.chain(board => {
|
|
76
81
|
const setup = {
|
|
77
82
|
board: board,
|
|
78
|
-
|
|
83
|
+
hands: Hands.empty(),
|
|
79
84
|
turn: turn,
|
|
80
85
|
fullmoves: 1,
|
|
81
86
|
};
|
|
@@ -90,7 +95,7 @@ export function parseCsaHandicap(handicap: string): Result<Board, CsaError> {
|
|
|
90
95
|
const splitted = handicap.substring(2).match(/.{4}/g) || [];
|
|
91
96
|
const intitalBoard = Board.default();
|
|
92
97
|
for (const s of splitted) {
|
|
93
|
-
const sq =
|
|
98
|
+
const sq = parseNumberSquare(s.substring(0, 2));
|
|
94
99
|
if (defined(sq)) {
|
|
95
100
|
intitalBoard.take(sq);
|
|
96
101
|
} else {
|
|
@@ -114,7 +119,7 @@ function parseCsaBoard(csaBoard: string[]): Result<Board, CsaError> {
|
|
|
114
119
|
else {
|
|
115
120
|
if (file >= 9 || rank < 0) return Result.err(new CsaError(InvalidCsa.Board));
|
|
116
121
|
const role = csaToRole(s.substring(1));
|
|
117
|
-
if (defined(role)) {
|
|
122
|
+
if (defined(role) && allRoles('shogi').includes(role)) {
|
|
118
123
|
const square = file + rank * 9;
|
|
119
124
|
const piece = { role: role, color: (s.startsWith('-') ? 'gote' : 'sente') as Color };
|
|
120
125
|
board.set(square, piece);
|
|
@@ -131,13 +136,13 @@ function parseAdditions(initialSetup: Setup, additions: string[]): Result<Setup,
|
|
|
131
136
|
for (const line of additions) {
|
|
132
137
|
const color: Color = line[1] === '+' ? 'sente' : 'gote';
|
|
133
138
|
for (const sp of line.substring(2).match(/.{4}/g) || []) {
|
|
134
|
-
const
|
|
139
|
+
const sqString = sp.substring(0, 2);
|
|
140
|
+
const sq = parseNumberSquare(sqString);
|
|
135
141
|
const role = csaToRole(sp.substring(2, 4));
|
|
136
|
-
if (defined(sq) && defined(role)) {
|
|
137
|
-
if (sq
|
|
138
|
-
if (!(
|
|
139
|
-
|
|
140
|
-
initialSetup.pockets[color][role as PocketRole]++;
|
|
142
|
+
if ((defined(sq) || sqString === '00') && defined(role)) {
|
|
143
|
+
if (!defined(sq)) {
|
|
144
|
+
if (!handRoles('shogi').includes(role)) return Result.err(new CsaError(InvalidCsa.Hands));
|
|
145
|
+
initialSetup.hands[color][role]++;
|
|
141
146
|
} else {
|
|
142
147
|
initialSetup.board.set(sq, { role: role, color: color });
|
|
143
148
|
}
|
|
@@ -153,6 +158,14 @@ export function parseTags(csa: string): [string, string][] {
|
|
|
153
158
|
.map(l => l.substring(1).split(/:(.*)/, 2) as [string, string]);
|
|
154
159
|
}
|
|
155
160
|
|
|
161
|
+
export function normalizedCsaLines(csa: string): string[] {
|
|
162
|
+
return csa
|
|
163
|
+
.replace(/,/g, '\n')
|
|
164
|
+
.split(/[\r\n]+/)
|
|
165
|
+
.map(l => l.trim())
|
|
166
|
+
.filter(l => l);
|
|
167
|
+
}
|
|
168
|
+
|
|
156
169
|
//
|
|
157
170
|
// CSA MOVES
|
|
158
171
|
//
|
|
@@ -166,16 +179,16 @@ export function parseCsaMove(pos: Position, csaMove: string): Move | undefined {
|
|
|
166
179
|
const match = csaMove.match(/(?:[\+-])?00([1-9][1-9])(HI|KA|KI|GI|KE|KY|FU)/);
|
|
167
180
|
if (!match) return;
|
|
168
181
|
const drop = {
|
|
169
|
-
role: csaToRole(match[2])
|
|
170
|
-
to:
|
|
182
|
+
role: csaToRole(match[2])!,
|
|
183
|
+
to: parseNumberSquare(match[1])!,
|
|
171
184
|
};
|
|
172
185
|
return drop;
|
|
173
186
|
}
|
|
174
187
|
const role = csaToRole(match[3])!;
|
|
175
|
-
const orig =
|
|
188
|
+
const orig = parseNumberSquare(match[1])!;
|
|
176
189
|
return {
|
|
177
190
|
from: orig,
|
|
178
|
-
to:
|
|
191
|
+
to: parseNumberSquare(match[2])!,
|
|
179
192
|
promotion: pos.board.get(orig)?.role !== role,
|
|
180
193
|
};
|
|
181
194
|
}
|
|
@@ -193,30 +206,16 @@ export function parseCsaMoves(pos: Position, csaMoves: string[]): Move[] {
|
|
|
193
206
|
}
|
|
194
207
|
|
|
195
208
|
// Making CSA formatted moves
|
|
196
|
-
export function makeCsaMove(pos: Position, move: Move): string {
|
|
209
|
+
export function makeCsaMove(pos: Position, move: Move): string | undefined {
|
|
197
210
|
if (isDrop(move)) {
|
|
198
|
-
return '00' +
|
|
211
|
+
return '00' + makeNumberSquare(move.to) + roleToCsa(move.role);
|
|
199
212
|
} else {
|
|
200
213
|
const role = pos.board.getRole(move.from);
|
|
201
|
-
if (!role) return
|
|
202
|
-
return
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
pos = pos.clone();
|
|
208
|
-
const line = [];
|
|
209
|
-
for (const m of variation) {
|
|
210
|
-
line.push((pos.turn === 'sente' ? '+' : '-') + makeCsaMove(pos, m));
|
|
211
|
-
pos.play(m);
|
|
214
|
+
if (!role) return undefined;
|
|
215
|
+
return (
|
|
216
|
+
makeNumberSquare(move.from) +
|
|
217
|
+
makeNumberSquare(move.to) +
|
|
218
|
+
roleToCsa(move.promotion ? promote('shogi')(role) : role)
|
|
219
|
+
);
|
|
212
220
|
}
|
|
213
|
-
return line.join('\n');
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export function normalizedCsaLines(csa: string): string[] {
|
|
217
|
-
return csa
|
|
218
|
-
.replace(/,/g, '\n')
|
|
219
|
-
.split(/[\r\n]+/)
|
|
220
|
-
.map(l => l.trim())
|
|
221
|
-
.filter(l => l);
|
|
222
221
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { roleTo2Kanji, squareFile, squareRank } from '../util';
|
|
2
|
+
import { Position } from '../shogi';
|
|
3
|
+
import { Move, isDrop, Square, Piece } from '../types';
|
|
4
|
+
import { pieceCanPromote } from '../variantUtil';
|
|
5
|
+
import { makeJapaneseSquare, piecesAiming } from './notationUtil';
|
|
6
|
+
import { SquareSet } from '../squareSet';
|
|
7
|
+
|
|
8
|
+
// 7六歩
|
|
9
|
+
export function makeJapaneseMove(pos: Position, move: Move, lastDest?: Square): string | undefined {
|
|
10
|
+
if (isDrop(move)) {
|
|
11
|
+
const ambStr = piecesAiming(pos, { role: move.role, color: pos.turn }, move.to).isEmpty() ? '' : '打';
|
|
12
|
+
return `${makeJapaneseSquare(move.to)}${roleTo2Kanji(move.role).toUpperCase()}${ambStr}`;
|
|
13
|
+
} else {
|
|
14
|
+
const piece = pos.board.get(move.from);
|
|
15
|
+
if (piece) {
|
|
16
|
+
const destStr = lastDest === move.to ? '同 ' : makeJapaneseSquare(move.to);
|
|
17
|
+
const roleStr = roleTo2Kanji(piece.role).toUpperCase();
|
|
18
|
+
const ambPieces = piecesAiming(pos, piece, move.to).without(move.from);
|
|
19
|
+
const ambStr = ambPieces.isEmpty() ? '' : disambiguate(piece, move.from, move.to, ambPieces);
|
|
20
|
+
const promStr = move.promotion ? '成' : pieceCanPromote(pos.rules)(piece, move.from, move.to) ? '不成' : '';
|
|
21
|
+
return `${destStr}${roleStr}${ambStr}${promStr}`;
|
|
22
|
+
} else return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function disambiguate(piece: Piece, orig: Square, dest: Square, others: SquareSet): string {
|
|
27
|
+
const myRank = squareRank(orig);
|
|
28
|
+
const myFile = squareFile(orig);
|
|
29
|
+
|
|
30
|
+
const destRank = squareRank(dest);
|
|
31
|
+
const destFile = squareFile(dest);
|
|
32
|
+
|
|
33
|
+
const movingUp = myRank < destRank;
|
|
34
|
+
const movingDown = myRank > destRank;
|
|
35
|
+
|
|
36
|
+
// special case if gold/silver like piece is moving directly forward
|
|
37
|
+
if (
|
|
38
|
+
myFile === destFile &&
|
|
39
|
+
(piece.color === 'sente') === movingUp &&
|
|
40
|
+
['gold', 'silver', 'promotedlance', 'promotedknight', 'promotedsilver', 'tokin'].includes(piece.role)
|
|
41
|
+
)
|
|
42
|
+
return '直';
|
|
43
|
+
|
|
44
|
+
// is this the only piece moving in certain vertical direction (up, down, horizontally)
|
|
45
|
+
if (![...others].map(squareRank).some(r => r > destRank === movingDown && r < destRank === movingUp))
|
|
46
|
+
return verticalDisambiguation(piece, movingUp, movingDown);
|
|
47
|
+
|
|
48
|
+
const othersFiles = [...others].map(squareFile);
|
|
49
|
+
const rightest = othersFiles.reduce((prev, cur) => (prev > cur ? prev : cur));
|
|
50
|
+
const leftest = othersFiles.reduce((prev, cur) => (prev < cur ? prev : cur));
|
|
51
|
+
|
|
52
|
+
// is this piece positioned most on one side, not in the middle
|
|
53
|
+
if (rightest < myFile || leftest > myFile || (others.size() === 2 && rightest > myFile && leftest < myFile))
|
|
54
|
+
return sideDisambiguation(piece, rightest < squareFile(orig), leftest > squareFile(orig));
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
sideDisambiguation(piece, rightest <= squareFile(orig), leftest >= squareFile(orig)) +
|
|
58
|
+
verticalDisambiguation(piece, movingUp, movingDown)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function verticalDisambiguation(piece: Piece, up: Boolean, down: Boolean): string {
|
|
63
|
+
return up === down
|
|
64
|
+
? '寄'
|
|
65
|
+
: (piece.color === 'sente') === up
|
|
66
|
+
? ['horse', 'dragon'].includes(piece.role)
|
|
67
|
+
? '行'
|
|
68
|
+
: '上'
|
|
69
|
+
: '引';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sideDisambiguation(piece: Piece, right: Boolean, left: Boolean): string {
|
|
73
|
+
return !left && !right ? '中' : (piece.color === 'sente') === right ? '右' : '左';
|
|
74
|
+
}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { Result } from '@badrap/result';
|
|
2
|
-
import { Board } from '
|
|
3
|
-
import {
|
|
2
|
+
import { Board } from '../../board';
|
|
3
|
+
import { INITIAL_SFEN, makeSfen, parseSfen } from '../../sfen';
|
|
4
4
|
import { handicapNameToSfen, sfenToHandicapName } from './kifHandicaps';
|
|
5
|
-
import {
|
|
6
|
-
import { Position } from '
|
|
7
|
-
import { Color, isDrop, Move,
|
|
8
|
-
import { defined, kanjiToRole,
|
|
5
|
+
import { Setup } from '../../setup';
|
|
6
|
+
import { Position } from '../../shogi';
|
|
7
|
+
import { Color, isDrop, Move, Square } from '../../types';
|
|
8
|
+
import { defined, kanjiToRole, roleTo1Kanji, roleTo2Kanji } from '../../util';
|
|
9
9
|
|
|
10
|
+
import { Hand, Hands } from '../../hand';
|
|
11
|
+
import { allRoles, handRoles, promote } from '../../variantUtil';
|
|
10
12
|
import {
|
|
11
13
|
kanjiToNumber,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
normalizedKifLines,
|
|
14
|
+
makeJapaneseSquare,
|
|
15
|
+
makeNumberSquare,
|
|
15
16
|
numberToKanji,
|
|
16
|
-
|
|
17
|
-
} from '
|
|
17
|
+
parseJapaneseSquare,
|
|
18
|
+
} from '../notationUtil';
|
|
18
19
|
|
|
19
20
|
//
|
|
20
21
|
// KIF HEADER
|
|
@@ -24,34 +25,37 @@ export enum InvalidKif {
|
|
|
24
25
|
Kif = 'ERR_KIF',
|
|
25
26
|
Board = 'ERR_BOARD',
|
|
26
27
|
Handicap = 'ERR_HANDICAP',
|
|
27
|
-
|
|
28
|
+
Hands = 'ERR_HANDS',
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export class KifError extends Error {}
|
|
31
32
|
|
|
32
33
|
// Export
|
|
33
34
|
export function makeKifHeader(setup: Setup): string {
|
|
34
|
-
const handicap = sfenToHandicapName(
|
|
35
|
+
const handicap = sfenToHandicapName(makeSfen(setup, { epd: true }));
|
|
35
36
|
if (defined(handicap)) return '手合割:' + handicap;
|
|
36
37
|
return makeKifPositionHeader(setup);
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export function makeKifPositionHeader(setup: Setup): string {
|
|
40
41
|
return [
|
|
41
|
-
'後手の持駒:' +
|
|
42
|
+
'後手の持駒:' + makeKifHand(setup.hands.gote),
|
|
42
43
|
makeKifBoard(setup.board),
|
|
43
|
-
'先手の持駒:' +
|
|
44
|
+
'先手の持駒:' + makeKifHand(setup.hands.sente),
|
|
44
45
|
...(setup.turn === 'gote' ? ['後手番'] : []),
|
|
45
46
|
].join('\n');
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
export function makeKifBoard(board: Board): string {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const kifFiles = ' 9 8 7 6 5 4 3 2 1'.slice(-(board.numberOfFiles * 2));
|
|
51
|
+
const separator = '+' + '-'.repeat(board.numberOfFiles * 3) + '+';
|
|
52
|
+
const offset = 9 - board.numberOfFiles;
|
|
53
|
+
let kifBoard = ' ' + kifFiles + `\n${separator}\n`;
|
|
54
|
+
for (let rank = 8; rank >= 9 - board.numberOfRanks; rank--) {
|
|
55
|
+
for (let file = offset; file < 9; file++) {
|
|
52
56
|
const square = file + rank * 9;
|
|
53
57
|
const piece = board.get(square);
|
|
54
|
-
if (file ===
|
|
58
|
+
if (file === offset) {
|
|
55
59
|
kifBoard += '|';
|
|
56
60
|
}
|
|
57
61
|
if (!piece) kifBoard += ' ・';
|
|
@@ -64,17 +68,18 @@ export function makeKifBoard(board: Board): string {
|
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
|
-
kifBoard +=
|
|
71
|
+
kifBoard += separator;
|
|
68
72
|
return kifBoard;
|
|
69
73
|
}
|
|
70
74
|
|
|
71
|
-
export function
|
|
72
|
-
if (
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
export function makeKifHand(hand: Hand): string {
|
|
76
|
+
if (hand.isEmpty()) return 'なし';
|
|
77
|
+
return handRoles('shogi')
|
|
78
|
+
.map(role => {
|
|
79
|
+
const r = roleTo1Kanji(role);
|
|
80
|
+
const n = hand[role];
|
|
81
|
+
return n > 1 ? r + numberToKanji(n) : n === 1 ? r : '';
|
|
82
|
+
})
|
|
78
83
|
.filter(p => p.length > 0)
|
|
79
84
|
.join(' ');
|
|
80
85
|
}
|
|
@@ -83,12 +88,12 @@ export function makeKifPocket(material: MaterialSide): string {
|
|
|
83
88
|
export function parseKifHeader(kif: string): Result<Setup, KifError> {
|
|
84
89
|
const lines = normalizedKifLines(kif);
|
|
85
90
|
const handicap = lines.find(l => l.startsWith('手合割:'));
|
|
86
|
-
const hSfen = defined(handicap) ? handicapNameToSfen(handicap.split(':')[1]) :
|
|
91
|
+
const hSfen = defined(handicap) ? handicapNameToSfen(handicap.split(':')[1]) : INITIAL_SFEN;
|
|
87
92
|
return parseKifPositionHeader(kif).unwrap(
|
|
88
93
|
kifBoard => Result.ok(kifBoard),
|
|
89
94
|
() => {
|
|
90
95
|
if (!defined(hSfen)) return Result.err(new KifError(InvalidKif.Handicap));
|
|
91
|
-
return
|
|
96
|
+
return parseSfen(hSfen);
|
|
92
97
|
}
|
|
93
98
|
);
|
|
94
99
|
}
|
|
@@ -96,25 +101,21 @@ export function parseKifHeader(kif: string): Result<Setup, KifError> {
|
|
|
96
101
|
export function parseKifPositionHeader(kif: string): Result<Setup, KifError> {
|
|
97
102
|
const lines = normalizedKifLines(kif);
|
|
98
103
|
|
|
99
|
-
const
|
|
100
|
-
const
|
|
104
|
+
const goteHandStr = lines.find(l => l.startsWith('後手の持駒:'));
|
|
105
|
+
const senteHandStr = lines.find(l => l.startsWith('先手の持駒:'));
|
|
101
106
|
const turn = lines.some(l => l.startsWith('後手番')) ? 'gote' : 'sente';
|
|
102
107
|
|
|
103
108
|
const board: Result<Board, KifError> = parseKifBoard(kif);
|
|
104
109
|
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
: Result.ok(MaterialSide.empty());
|
|
108
|
-
const sentePocket = defined(sentePocketStr)
|
|
109
|
-
? parseKifHand(sentePocketStr.split(':')[1])
|
|
110
|
-
: Result.ok(MaterialSide.empty());
|
|
110
|
+
const goteHand = defined(goteHandStr) ? parseKifHand(goteHandStr.split(':')[1]) : Result.ok(Hand.empty());
|
|
111
|
+
const senteHand = defined(senteHandStr) ? parseKifHand(senteHandStr.split(':')[1]) : Result.ok(Hand.empty());
|
|
111
112
|
|
|
112
113
|
return board.chain(board =>
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
goteHand.chain(gHand =>
|
|
115
|
+
senteHand.map(sHand => {
|
|
115
116
|
return {
|
|
116
117
|
board,
|
|
117
|
-
|
|
118
|
+
hands: new Hands(gHand, sHand),
|
|
118
119
|
turn,
|
|
119
120
|
fullmoves: 1,
|
|
120
121
|
};
|
|
@@ -125,13 +126,19 @@ export function parseKifPositionHeader(kif: string): Result<Setup, KifError> {
|
|
|
125
126
|
|
|
126
127
|
export function parseKifBoard(kifBoard: string): Result<Board, KifError> {
|
|
127
128
|
const lines = normalizedKifLines(kifBoard).filter(l => l.startsWith('|'));
|
|
128
|
-
if (lines.length
|
|
129
|
+
if (lines.length === 0) return Result.err(new KifError(InvalidKif.Board));
|
|
129
130
|
const board = Board.empty();
|
|
130
|
-
|
|
131
|
+
|
|
132
|
+
// assuming square board
|
|
133
|
+
board.numberOfRanks = lines.length;
|
|
134
|
+
board.numberOfFiles = lines.length;
|
|
135
|
+
|
|
136
|
+
const offset = 9 - lines.length;
|
|
137
|
+
let file = offset;
|
|
131
138
|
let rank = 9;
|
|
132
139
|
|
|
133
140
|
for (const l of lines) {
|
|
134
|
-
file =
|
|
141
|
+
file = offset;
|
|
135
142
|
rank--;
|
|
136
143
|
let gote = false;
|
|
137
144
|
let prom = false;
|
|
@@ -149,9 +156,9 @@ export function parseKifBoard(kifBoard: string): Result<Board, KifError> {
|
|
|
149
156
|
default:
|
|
150
157
|
if (file > 9 || rank < 0) return Result.err(new KifError(InvalidKif.Board));
|
|
151
158
|
const role = kanjiToRole(c);
|
|
152
|
-
if (defined(role)) {
|
|
159
|
+
if (defined(role) && allRoles('shogi').includes(role)) {
|
|
153
160
|
const square = file + rank * 9;
|
|
154
|
-
const piece = { role: prom ? promote(role) : role, color: (gote ? 'gote' : 'sente') as Color };
|
|
161
|
+
const piece = { role: prom ? promote('shogi')(role) : role, color: (gote ? 'gote' : 'sente') as Color };
|
|
155
162
|
board.set(square, piece);
|
|
156
163
|
prom = false;
|
|
157
164
|
gote = false;
|
|
@@ -160,27 +167,27 @@ export function parseKifBoard(kifBoard: string): Result<Board, KifError> {
|
|
|
160
167
|
}
|
|
161
168
|
}
|
|
162
169
|
}
|
|
163
|
-
if (rank !==
|
|
170
|
+
if (rank !== 9 - lines.length || file !== 9) return Result.err(new KifError(InvalidKif.Board));
|
|
164
171
|
return Result.ok(board);
|
|
165
172
|
}
|
|
166
173
|
|
|
167
|
-
export function parseKifHand(
|
|
168
|
-
const
|
|
169
|
-
const pieces =
|
|
174
|
+
export function parseKifHand(handPart: string): Result<Hand, KifError> {
|
|
175
|
+
const hand = Hand.empty();
|
|
176
|
+
const pieces = handPart.replace(/ /g, ' ').trim().split(' ');
|
|
170
177
|
|
|
171
|
-
if (
|
|
178
|
+
if (handPart.includes('なし')) return Result.ok(hand);
|
|
172
179
|
for (const piece of pieces) {
|
|
173
180
|
for (let i = 0; i < piece.length; i++) {
|
|
174
181
|
const role = kanjiToRole(piece[i++]);
|
|
175
|
-
if (!role) return Result.err(new KifError(InvalidKif.
|
|
182
|
+
if (!role || !handRoles('shogi').includes(role)) return Result.err(new KifError(InvalidKif.Hands));
|
|
176
183
|
let countStr = '';
|
|
177
184
|
while (i < piece.length && ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'].includes(piece[i]))
|
|
178
185
|
countStr += piece[i++];
|
|
179
186
|
const count = kanjiToNumber(countStr) || 1;
|
|
180
|
-
|
|
187
|
+
hand[role] += count;
|
|
181
188
|
}
|
|
182
189
|
}
|
|
183
|
-
return Result.ok(
|
|
190
|
+
return Result.ok(hand);
|
|
184
191
|
}
|
|
185
192
|
|
|
186
193
|
export function parseTags(kif: string): [string, string][] {
|
|
@@ -189,6 +196,15 @@ export function parseTags(kif: string): [string, string][] {
|
|
|
189
196
|
.map(l => l.replace(':', ':').split(/:(.*)/, 2) as [string, string]);
|
|
190
197
|
}
|
|
191
198
|
|
|
199
|
+
export function normalizedKifLines(kif: string): string[] {
|
|
200
|
+
return kif
|
|
201
|
+
.replace(/:/g, ':')
|
|
202
|
+
.replace(/ /g, ' ') // full-width space to normal space
|
|
203
|
+
.split(/[\r\n]+/)
|
|
204
|
+
.map(l => l.trim())
|
|
205
|
+
.filter(l => l);
|
|
206
|
+
}
|
|
207
|
+
|
|
192
208
|
//
|
|
193
209
|
// KIF MOVES
|
|
194
210
|
//
|
|
@@ -204,15 +220,15 @@ export function parseKifMove(kifMove: string, lastDest: Square | undefined = und
|
|
|
204
220
|
const match = kifMove.match(/((?:[123456789][一二三四五六七八九]|同\s?))(飛|角|金|銀|桂|香|歩)打/);
|
|
205
221
|
if (!match) return;
|
|
206
222
|
const move = {
|
|
207
|
-
role: kanjiToRole(match[2])
|
|
208
|
-
to:
|
|
223
|
+
role: kanjiToRole(match[2])!,
|
|
224
|
+
to: parseJapaneseSquare(match[1]) ?? lastDest!,
|
|
209
225
|
};
|
|
210
226
|
return move;
|
|
211
227
|
}
|
|
212
228
|
|
|
213
229
|
return {
|
|
214
|
-
from:
|
|
215
|
-
to:
|
|
230
|
+
from: parseJapaneseSquare(match[4])!,
|
|
231
|
+
to: parseJapaneseSquare(match[1]) ?? lastDest!,
|
|
216
232
|
promotion: !!match[3],
|
|
217
233
|
};
|
|
218
234
|
}
|
|
@@ -229,30 +245,13 @@ export function parseKifMoves(kifMoves: string[], lastDest: Square | undefined =
|
|
|
229
245
|
}
|
|
230
246
|
|
|
231
247
|
// Making kif formatted moves
|
|
232
|
-
export function makeKifMove(pos: Position, move: Move,
|
|
233
|
-
const moveDest =
|
|
248
|
+
export function makeKifMove(pos: Position, move: Move, lastDest?: Square): string | undefined {
|
|
249
|
+
const moveDest = lastDest === move.to ? '同 ' : makeJapaneseSquare(move.to);
|
|
234
250
|
if (isDrop(move)) {
|
|
235
251
|
return moveDest + roleTo1Kanji(move.role) + '打';
|
|
236
252
|
} else {
|
|
237
253
|
const role = pos.board.getRole(move.from);
|
|
238
|
-
if (!role) return
|
|
239
|
-
return moveDest + roleTo2Kanji(role) + (move.promotion ? '成' : '') + '(' +
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export function makeKifVariation(
|
|
244
|
-
pos: Position,
|
|
245
|
-
variation: Move[],
|
|
246
|
-
lastDest: Square | undefined = undefined,
|
|
247
|
-
startTurn = 1
|
|
248
|
-
): string {
|
|
249
|
-
pos = pos.clone();
|
|
250
|
-
const line = [];
|
|
251
|
-
const padding = (startTurn + variation.length - 1).toString().length;
|
|
252
|
-
for (const m of variation) {
|
|
253
|
-
line.push((startTurn++).toString().padStart(padding) + ' ' + makeKifMove(pos, m, m.to === lastDest));
|
|
254
|
-
pos.play(m);
|
|
255
|
-
lastDest = m.to;
|
|
254
|
+
if (!role) return undefined;
|
|
255
|
+
return moveDest + roleTo2Kanji(role) + (move.promotion ? '成' : '') + '(' + makeNumberSquare(move.from) + ')';
|
|
256
256
|
}
|
|
257
|
-
return line.join('\n');
|
|
258
257
|
}
|
|
@@ -23,6 +23,9 @@ export function sfenToHandicapName(sfen: string): string | undefined {
|
|
|
23
23
|
return '八枚落ち';
|
|
24
24
|
case '4k4/9/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w -':
|
|
25
25
|
return '十枚落ち';
|
|
26
|
+
// minishogi
|
|
27
|
+
case 'rbsgk/4p/5/P4/KGSBR b -':
|
|
28
|
+
return '五々将棋';
|
|
26
29
|
default:
|
|
27
30
|
return undefined;
|
|
28
31
|
}
|
|
@@ -79,6 +82,8 @@ export function handicapNameToSfen(name: string): string | undefined {
|
|
|
79
82
|
return '2sgkgs2/9/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w RB2N2L 1';
|
|
80
83
|
case '八枚得':
|
|
81
84
|
return '3gkg3/9/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w RB2S2N2L 1';
|
|
85
|
+
case '五々将棋':
|
|
86
|
+
return 'rbsgk/4p/5/P4/KGSBR b - 1';
|
|
82
87
|
default:
|
|
83
88
|
return undefined;
|
|
84
89
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { roleTo2Kanji } from '../util';
|
|
2
|
+
import { Position } from '../shogi';
|
|
3
|
+
import { Move, isDrop, Square } from '../types';
|
|
4
|
+
import { pieceCanPromote } from '../variantUtil';
|
|
5
|
+
import { makeNumberSquare, piecesAiming } from './notationUtil';
|
|
6
|
+
|
|
7
|
+
// 歩-76
|
|
8
|
+
export function makeKitaoKawasakiMove(pos: Position, move: Move, lastDest?: Square): string | undefined {
|
|
9
|
+
if (isDrop(move)) {
|
|
10
|
+
return roleTo2Kanji(move.role).toUpperCase() + '*' + makeNumberSquare(move.to);
|
|
11
|
+
} else {
|
|
12
|
+
const piece = pos.board.get(move.from);
|
|
13
|
+
if (piece) {
|
|
14
|
+
const roleStr = roleTo2Kanji(piece.role).toUpperCase();
|
|
15
|
+
const ambStr = piecesAiming(pos, piece, move.to).without(move.from).isEmpty()
|
|
16
|
+
? ''
|
|
17
|
+
: `(${makeNumberSquare(move.from)})`;
|
|
18
|
+
const actionStr = pos.board.has(move.to) ? 'x' : '-';
|
|
19
|
+
const destStr = lastDest === move.to ? '' : makeNumberSquare(move.to);
|
|
20
|
+
const promStr = move.promotion ? '+' : pieceCanPromote(pos.rules)(piece, move.from, move.to) ? '=' : '';
|
|
21
|
+
return `${roleStr}${ambStr}${actionStr}${destStr}${promStr}`;
|
|
22
|
+
} else return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|