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,24 +1,42 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Position } from '../shogi';
|
|
2
|
+
import { Piece, Square } from '../types';
|
|
3
|
+
import { parseSquare, squareFile, squareRank } from '../util';
|
|
4
|
+
import { SquareSet } from '../squareSet';
|
|
3
5
|
|
|
4
|
-
export function
|
|
6
|
+
export function piecesAiming(pos: Position, piece: Piece, to: Square): SquareSet {
|
|
7
|
+
// Disambiguation
|
|
8
|
+
let pieces = SquareSet.empty();
|
|
9
|
+
for (const s of pos.board.pieces(pos.turn, piece.role))
|
|
10
|
+
if (pos.dests(s).has(to)) pieces = pieces.union(SquareSet.fromSquare(s));
|
|
11
|
+
return pieces;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function makeNumberSquare(sq: Square): string {
|
|
15
|
+
return (9 - squareFile(sq)).toString() + (9 - squareRank(sq));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseNumberSquare(str: string): Square | undefined {
|
|
5
19
|
if (str.length !== 2) return;
|
|
6
|
-
const file = str
|
|
7
|
-
const rank =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return parseSquare(file + rank);
|
|
20
|
+
const file = Math.abs(str.charCodeAt(0) - '9'.charCodeAt(0));
|
|
21
|
+
const rank = Math.abs(str.charCodeAt(1) - '9'.charCodeAt(0));
|
|
22
|
+
if (file < 0 || file >= 9 || rank < 0 || rank >= 9) return;
|
|
23
|
+
return file + 9 * rank;
|
|
11
24
|
}
|
|
12
25
|
|
|
13
|
-
export function
|
|
26
|
+
export function makeJapaneseSquare(sq: Square): string {
|
|
14
27
|
return (
|
|
15
28
|
String.fromCharCode((9 - squareFile(sq)).toString().charCodeAt(0) + 0xfee0) +
|
|
16
29
|
toKanjiDigit((9 - squareRank(sq)).toString())
|
|
17
30
|
);
|
|
18
31
|
}
|
|
19
32
|
|
|
20
|
-
export function
|
|
21
|
-
|
|
33
|
+
export function parseJapaneseSquare(str: string): Square | undefined {
|
|
34
|
+
if (str.length !== 2) return;
|
|
35
|
+
const file = str[0].charCodeAt(0) >= 65297 ? String.fromCharCode(str[0].charCodeAt(0) - 65248) : str[0];
|
|
36
|
+
const rank = String.fromCharCode(
|
|
37
|
+
('一二三四五六七八九'.includes(str[1]) ? fromKanjiDigit(str[1]).toString() : str[1]).charCodeAt(0) + 48
|
|
38
|
+
);
|
|
39
|
+
return parseSquare(file + rank);
|
|
22
40
|
}
|
|
23
41
|
|
|
24
42
|
export function toKanjiDigit(str: string): string {
|
|
@@ -48,13 +66,6 @@ export function toKanjiDigit(str: string): string {
|
|
|
48
66
|
}
|
|
49
67
|
}
|
|
50
68
|
|
|
51
|
-
// max 99, meant for pieces in hand, you don't need more than 99 pieces...
|
|
52
|
-
export function numberToKanji(n: number): string {
|
|
53
|
-
n = Math.max(0, Math.min(n, 99));
|
|
54
|
-
const res = n >= 20 ? toKanjiDigit(Math.floor(n / 10).toString()) + '十' : n >= 10 ? '十' : '';
|
|
55
|
-
return res + toKanjiDigit(Math.floor(n % 10).toString());
|
|
56
|
-
}
|
|
57
|
-
|
|
58
69
|
export function fromKanjiDigit(str: string): number {
|
|
59
70
|
switch (str) {
|
|
60
71
|
case '一':
|
|
@@ -82,7 +93,14 @@ export function fromKanjiDigit(str: string): number {
|
|
|
82
93
|
}
|
|
83
94
|
}
|
|
84
95
|
|
|
85
|
-
// max 99
|
|
96
|
+
// max 99
|
|
97
|
+
export function numberToKanji(n: number): string {
|
|
98
|
+
n = Math.max(0, Math.min(n, 99));
|
|
99
|
+
const res = n >= 20 ? toKanjiDigit(Math.floor(n / 10).toString()) + '十' : n >= 10 ? '十' : '';
|
|
100
|
+
return res + toKanjiDigit(Math.floor(n % 10).toString());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// max 99
|
|
86
104
|
export function kanjiToNumber(str: string): number {
|
|
87
105
|
let res = str.startsWith('十') ? 1 : 0;
|
|
88
106
|
for (const s of str) {
|
|
@@ -91,12 +109,3 @@ export function kanjiToNumber(str: string): number {
|
|
|
91
109
|
}
|
|
92
110
|
return Math.max(0, Math.min(res, 99));
|
|
93
111
|
}
|
|
94
|
-
|
|
95
|
-
export function normalizedKifLines(kif: string): string[] {
|
|
96
|
-
return kif
|
|
97
|
-
.replace(/:/g, ':')
|
|
98
|
-
.replace(/ /g, ' ') // full-width space to normal space
|
|
99
|
-
.split(/[\r\n]+/)
|
|
100
|
-
.map(l => l.trim())
|
|
101
|
-
.filter(l => l);
|
|
102
|
-
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { roleToString } from '../util';
|
|
2
|
+
import { Position } from '../shogi';
|
|
3
|
+
import { Move, isDrop } from '../types';
|
|
4
|
+
import { pieceCanPromote } from '../variantUtil';
|
|
5
|
+
import { makeNumberSquare, piecesAiming } from './notationUtil';
|
|
6
|
+
|
|
7
|
+
// P-76
|
|
8
|
+
export function makeWesternMove(pos: Position, move: Move): string | undefined {
|
|
9
|
+
if (isDrop(move)) {
|
|
10
|
+
return roleToString(move.role).toUpperCase() + '*' + makeNumberSquare(move.to);
|
|
11
|
+
} else {
|
|
12
|
+
const piece = pos.board.get(move.from);
|
|
13
|
+
if (piece) {
|
|
14
|
+
const roleStr = roleToString(piece.role).toUpperCase();
|
|
15
|
+
const ambStr = piecesAiming(pos, piece, move.to).without(move.from).isEmpty() ? '' : makeNumberSquare(move.from);
|
|
16
|
+
const actionStr = pos.board.has(move.to) ? 'x' : '-';
|
|
17
|
+
const promStr = move.promotion ? '+' : pieceCanPromote(pos.rules)(piece, move.from, move.to) ? '=' : '';
|
|
18
|
+
return `${roleStr}${ambStr}${actionStr}${makeNumberSquare(move.to)}${promStr}`;
|
|
19
|
+
} else return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { makeSquare, roleToString } from '../util';
|
|
2
|
+
import { Position } from '../shogi';
|
|
3
|
+
import { Move, isDrop } from '../types';
|
|
4
|
+
import { pieceCanPromote } from '../variantUtil';
|
|
5
|
+
import { piecesAiming } from './notationUtil';
|
|
6
|
+
|
|
7
|
+
// P-7f
|
|
8
|
+
export function makeWesternEngineMove(pos: Position, move: Move): string | undefined {
|
|
9
|
+
if (isDrop(move)) {
|
|
10
|
+
return roleToString(move.role).toUpperCase() + '*' + makeSquare(move.to);
|
|
11
|
+
} else {
|
|
12
|
+
const piece = pos.board.get(move.from);
|
|
13
|
+
if (piece) {
|
|
14
|
+
const roleStr = roleToString(piece.role).toUpperCase();
|
|
15
|
+
const ambStr = piecesAiming(pos, piece, move.to).without(move.from).isEmpty() ? '' : makeSquare(move.from);
|
|
16
|
+
const actionStr = pos.board.has(move.to) ? 'x' : '-';
|
|
17
|
+
const promStr = move.promotion ? '+' : pieceCanPromote(pos.rules)(piece, move.from, move.to) ? '=' : '';
|
|
18
|
+
return `${roleStr}${ambStr}${actionStr}${makeSquare(move.to)}${promStr}`;
|
|
19
|
+
} else return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/setup.ts
CHANGED
|
@@ -1,97 +1,10 @@
|
|
|
1
|
-
import { Color
|
|
1
|
+
import { Color } from './types';
|
|
2
2
|
import { Board } from './board';
|
|
3
|
-
|
|
4
|
-
export class MaterialSide {
|
|
5
|
-
pawn: number;
|
|
6
|
-
lance: number;
|
|
7
|
-
knight: number;
|
|
8
|
-
silver: number;
|
|
9
|
-
gold: number;
|
|
10
|
-
bishop: number;
|
|
11
|
-
rook: number;
|
|
12
|
-
|
|
13
|
-
private constructor() {}
|
|
14
|
-
|
|
15
|
-
static empty(): MaterialSide {
|
|
16
|
-
const m = new MaterialSide();
|
|
17
|
-
for (const role of POCKET_ROLES) m[role] = 0;
|
|
18
|
-
return m;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
static fromBoard(board: Board, color: Color): MaterialSide {
|
|
22
|
-
const m = new MaterialSide();
|
|
23
|
-
for (const role of POCKET_ROLES) m[role] = board.pieces(color, role).size();
|
|
24
|
-
return m;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
clone(): MaterialSide {
|
|
28
|
-
const m = new MaterialSide();
|
|
29
|
-
for (const role of POCKET_ROLES) m[role] = this[role];
|
|
30
|
-
return m;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
equals(other: MaterialSide): boolean {
|
|
34
|
-
return POCKET_ROLES.every(role => this[role] === other[role]);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
add(other: MaterialSide): MaterialSide {
|
|
38
|
-
const m = new MaterialSide();
|
|
39
|
-
for (const role of POCKET_ROLES) m[role] = this[role] + other[role];
|
|
40
|
-
return m;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
nonEmpty(): boolean {
|
|
44
|
-
return POCKET_ROLES.some(role => this[role] > 0);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
isEmpty(): boolean {
|
|
48
|
-
return !this.nonEmpty();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
count(): number {
|
|
52
|
-
return this.pawn + this.lance + this.knight + this.silver + this.gold + this.bishop + this.rook;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export class Material {
|
|
57
|
-
constructor(public gote: MaterialSide, public sente: MaterialSide) {}
|
|
58
|
-
|
|
59
|
-
static empty(): Material {
|
|
60
|
-
return new Material(MaterialSide.empty(), MaterialSide.empty());
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
static fromBoard(board: Board): Material {
|
|
64
|
-
return new Material(MaterialSide.fromBoard(board, 'gote'), MaterialSide.fromBoard(board, 'sente'));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
clone(): Material {
|
|
68
|
-
return new Material(this.gote.clone(), this.sente.clone());
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
equals(other: Material): boolean {
|
|
72
|
-
return this.gote.equals(other.gote) && this.sente.equals(other.sente);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
add(other: Material): Material {
|
|
76
|
-
return new Material(this.gote.add(other.gote), this.sente.add(other.sente));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
count(): number {
|
|
80
|
-
return this.gote.count() + this.sente.count();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
isEmpty(): boolean {
|
|
84
|
-
return this.gote.isEmpty() && this.sente.isEmpty();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
nonEmpty(): boolean {
|
|
88
|
-
return !this.isEmpty();
|
|
89
|
-
}
|
|
90
|
-
}
|
|
3
|
+
import { Hands } from './hand';
|
|
91
4
|
|
|
92
5
|
export interface Setup {
|
|
93
6
|
board: Board;
|
|
94
|
-
|
|
7
|
+
hands: Hands;
|
|
95
8
|
turn: Color;
|
|
96
9
|
fullmoves: number;
|
|
97
10
|
}
|
|
@@ -99,7 +12,7 @@ export interface Setup {
|
|
|
99
12
|
export function defaultSetup(): Setup {
|
|
100
13
|
return {
|
|
101
14
|
board: Board.default(),
|
|
102
|
-
|
|
15
|
+
hands: Hands.empty(),
|
|
103
16
|
turn: 'sente',
|
|
104
17
|
fullmoves: 1,
|
|
105
18
|
};
|
package/src/sfen.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { Result } from '@badrap/result';
|
|
2
|
+
import { Piece, Color } from './types';
|
|
3
|
+
import { Board } from './board';
|
|
4
|
+
import { Setup } from './setup';
|
|
5
|
+
import { defined, roleToString, stringToRole, toBW } from './util';
|
|
6
|
+
import { Hand, Hands } from './hand';
|
|
7
|
+
import { ROLES } from './types';
|
|
8
|
+
|
|
9
|
+
export const INITIAL_BOARD_SFEN = 'lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL';
|
|
10
|
+
export const INITIAL_EPD = INITIAL_BOARD_SFEN + ' b -';
|
|
11
|
+
export const INITIAL_SFEN = INITIAL_EPD + ' 1';
|
|
12
|
+
export const EMPTY_BOARD_SFEN = '9/9/9/9/9/9/9/9/9';
|
|
13
|
+
export const EMPTY_EPD = EMPTY_BOARD_SFEN + ' b -';
|
|
14
|
+
export const EMPTY_SFEN = EMPTY_EPD + ' 1';
|
|
15
|
+
|
|
16
|
+
export enum InvalidSfen {
|
|
17
|
+
Sfen = 'ERR_SFEN',
|
|
18
|
+
Board = 'ERR_BOARD',
|
|
19
|
+
Hands = 'ERR_HANDS',
|
|
20
|
+
Turn = 'ERR_TURN',
|
|
21
|
+
Fullmoves = 'ERR_FULLMOVES',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class SfenError extends Error {}
|
|
25
|
+
|
|
26
|
+
function parseSmallUint(str: string): number | undefined {
|
|
27
|
+
return /^\d{1,4}$/.test(str) ? parseInt(str, 10) : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stringToPiece(s: string): Piece | undefined {
|
|
31
|
+
const role = stringToRole(s);
|
|
32
|
+
return role && { role, color: s.toLowerCase() === s ? 'gote' : 'sente' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function parseBoardSfen(boardPart: string): Result<Board, SfenError> {
|
|
36
|
+
const board = Board.empty();
|
|
37
|
+
const ranks = boardPart.split('/');
|
|
38
|
+
board.numberOfRanks = ranks.length;
|
|
39
|
+
// we assume the board is square
|
|
40
|
+
// since that's good enough for now...
|
|
41
|
+
board.numberOfFiles = board.numberOfRanks;
|
|
42
|
+
const offset = 9 - board.numberOfFiles;
|
|
43
|
+
let rank = 8;
|
|
44
|
+
let file = offset;
|
|
45
|
+
for (let i = 0; i < boardPart.length; i++) {
|
|
46
|
+
let c = boardPart[i];
|
|
47
|
+
if (c === '/' && file === 9) {
|
|
48
|
+
file = offset;
|
|
49
|
+
rank--;
|
|
50
|
+
} else {
|
|
51
|
+
const step = parseInt(c, 10);
|
|
52
|
+
if (step > 0) file += step;
|
|
53
|
+
else {
|
|
54
|
+
if (file >= 9 || rank < 0) return Result.err(new SfenError(InvalidSfen.Board));
|
|
55
|
+
if (c === '+' && i + 1 < boardPart.length) c += boardPart[++i];
|
|
56
|
+
const square = file + rank * 9;
|
|
57
|
+
const piece = stringToPiece(c);
|
|
58
|
+
if (!piece) return Result.err(new SfenError(InvalidSfen.Board));
|
|
59
|
+
board.set(square, piece);
|
|
60
|
+
file++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (rank !== offset || file !== 9) return Result.err(new SfenError(InvalidSfen.Board));
|
|
65
|
+
return Result.ok(board);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function parseHands(handsPart: string): Result<Hands, SfenError> {
|
|
69
|
+
const hands = Hands.empty();
|
|
70
|
+
for (let i = 0; i < handsPart.length; i++) {
|
|
71
|
+
if (handsPart[i] === '-') break;
|
|
72
|
+
// max 99
|
|
73
|
+
let count: number;
|
|
74
|
+
if (parseInt(handsPart[i]) >= 0) {
|
|
75
|
+
count = parseInt(handsPart[i++], 10);
|
|
76
|
+
if (parseInt(handsPart[i]) >= 0) count = count * 10 + parseInt(handsPart[i++], 10);
|
|
77
|
+
} else count = 1;
|
|
78
|
+
const piece = stringToPiece(handsPart[i]);
|
|
79
|
+
if (!piece) return Result.err(new SfenError(InvalidSfen.Hands));
|
|
80
|
+
hands[piece.color][piece.role] += count;
|
|
81
|
+
}
|
|
82
|
+
return Result.ok(hands);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function parseSfen(sfen: string): Result<Setup, SfenError> {
|
|
86
|
+
const parts = sfen.split(' ');
|
|
87
|
+
|
|
88
|
+
// Board
|
|
89
|
+
const boardPart = parts.shift()!;
|
|
90
|
+
const board: Result<Board, SfenError> = parseBoardSfen(boardPart);
|
|
91
|
+
|
|
92
|
+
// Turn
|
|
93
|
+
const turnPart = parts.shift();
|
|
94
|
+
let turn: Color;
|
|
95
|
+
if (!defined(turnPart) || turnPart === 'b') turn = 'sente';
|
|
96
|
+
else if (turnPart === 'w') turn = 'gote';
|
|
97
|
+
else return Result.err(new SfenError(InvalidSfen.Turn));
|
|
98
|
+
|
|
99
|
+
// Hands
|
|
100
|
+
const handsPart = parts.shift();
|
|
101
|
+
let hands: Result<Hands, SfenError>;
|
|
102
|
+
if (!defined(handsPart)) hands = Result.ok(Hands.empty());
|
|
103
|
+
else hands = parseHands(handsPart);
|
|
104
|
+
|
|
105
|
+
// Turn
|
|
106
|
+
const fullmovesPart = parts.shift();
|
|
107
|
+
const fullmoves = defined(fullmovesPart) ? parseSmallUint(fullmovesPart) : 1;
|
|
108
|
+
if (!defined(fullmoves)) return Result.err(new SfenError(InvalidSfen.Fullmoves));
|
|
109
|
+
|
|
110
|
+
if (parts.length > 0) return Result.err(new SfenError(InvalidSfen.Sfen));
|
|
111
|
+
|
|
112
|
+
return board.chain(board =>
|
|
113
|
+
hands.map(hands => {
|
|
114
|
+
return {
|
|
115
|
+
board,
|
|
116
|
+
hands,
|
|
117
|
+
turn,
|
|
118
|
+
fullmoves: Math.max(1, fullmoves),
|
|
119
|
+
};
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface SfenOpts {
|
|
125
|
+
epd?: boolean;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function parsePiece(str: string): Piece | undefined {
|
|
129
|
+
if (!str) return;
|
|
130
|
+
const piece = stringToPiece(str[0]);
|
|
131
|
+
if (!piece) return;
|
|
132
|
+
else if (str.length > 1 && str[1] !== '+') return;
|
|
133
|
+
return piece;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function makePiece(piece: Piece): string {
|
|
137
|
+
let r = roleToString(piece.role);
|
|
138
|
+
if (piece.color === 'sente') r = r.toUpperCase();
|
|
139
|
+
return r;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function makeBoardSfen(board: Board): string {
|
|
143
|
+
let sfen = '';
|
|
144
|
+
let empty = 0;
|
|
145
|
+
for (let rank = 8; rank >= 9 - board.numberOfRanks; rank--) {
|
|
146
|
+
for (let file = 9 - board.numberOfFiles; file < 9; file++) {
|
|
147
|
+
const square = file + rank * 9;
|
|
148
|
+
const piece = board.get(square);
|
|
149
|
+
if (!piece) empty++;
|
|
150
|
+
else {
|
|
151
|
+
if (empty > 0) {
|
|
152
|
+
sfen += empty;
|
|
153
|
+
empty = 0;
|
|
154
|
+
}
|
|
155
|
+
sfen += makePiece(piece);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (file === 8) {
|
|
159
|
+
if (empty > 0) {
|
|
160
|
+
sfen += empty;
|
|
161
|
+
empty = 0;
|
|
162
|
+
}
|
|
163
|
+
if (rank !== 9 - board.numberOfRanks) sfen += '/';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return sfen;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function makeHand(hand: Hand): string {
|
|
171
|
+
return ROLES.map(role => {
|
|
172
|
+
const r = roleToString(role);
|
|
173
|
+
const n = hand[role];
|
|
174
|
+
return n > 1 ? n + r : n === 1 ? r : '';
|
|
175
|
+
}).join('');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function makeHands(hands: Hands): string {
|
|
179
|
+
const handsStr = makeHand(hands.sente).toUpperCase() + makeHand(hands.gote);
|
|
180
|
+
return handsStr === '' ? '-' : handsStr;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function makeSfen(setup: Setup, opts?: SfenOpts): string {
|
|
184
|
+
return [
|
|
185
|
+
makeBoardSfen(setup.board),
|
|
186
|
+
toBW(setup.turn),
|
|
187
|
+
makeHands(setup.hands),
|
|
188
|
+
...(opts?.epd ? [] : [Math.max(1, Math.min(setup.fullmoves, 9999))]),
|
|
189
|
+
].join(' ');
|
|
190
|
+
}
|