/// <summary> /// コンストラクタ /// </summary> public KifuObject(KifuHeader header, Board startBoard, IEnumerable<BoardMove> moveList, Exception error = null) { Error = error; SetHeader(header); SetBoard(startBoard, true); SetMoveList(moveList); }
/// <summary> /// コンストラクタ /// </summary> public KifuObject(KifuHeader header, Board startBoard, MoveNode root, Exception error = null) { Error = error; SetHeader(header); SetBoard(startBoard, true); SetRootNode(root); }
/// <summary> /// ヘッダを設定します。 /// </summary> public void SetHeader(KifuHeader header) { Header = header ?? new KifuHeader(); }
/// <summary> /// コンストラクタ /// </summary> public KifuObject(KifuHeader header, Board startBoard, Exception error = null) { MoveList = new List<BoardMove>(); RootNode = new MoveNode(); Error = error; SetHeader(header); SetBoard(startBoard, false); }
/// <summary> /// ki2形式のファイルを読み込みます。 /// </summary> private KifuObject LoadKi2(KifuHeader header, Board startBoard, KifMoveNode head) { var knodeList = ParseMoveLinesKi2().ToList(); var last = head; foreach (var knode in knodeList) { last.Next = knode; last = knode; } // KifNodeMoveのツリーをNodeMoveのツリーに直します。 // 変換時にエラーが出た場合は、それまでの指し手を変換します。 var board = startBoard.Clone(); Exception error = null; var root = head.Next.ConvertToMoveNode(board, head, out error); root.Regulalize(); return new KifuObject(header, startBoard, root, error); }
/// <summary> /// kif形式の指し手を読み込みます。 /// </summary> private KifuObject LoadKif(KifuHeader header, Board board, KifMoveNode head_) { var head = ParseNodeKif(board, false); // KifMoveNodeからMoveNodeへ変換します。 Exception error; var root = head.ConvertToMoveNode(board, head_, out error); root.Regulalize(); return new KifuObject(header, board, root, error); }
/// <summary> /// ヘッダー行をパースします。 /// </summary> private bool ParseHeaderLine(string line, BodParser parser, KifuHeader header = null, KifMoveNode head = null) { if (line == null) { // ファイルの終了を意味します。 return false; } var commentData = KifUtil.ParseCommentLine(line); if (commentData != null) { // コメントはパース結果に含めます。 if (head != null && commentData.IsMoveComment) { head.AddComment(commentData.Comment); } return true; } // 読み飛ばすべき説明行 if (line.Contains("手数----指手---------消費時間")) { this.isKif = true; // kif形式です。 return true; } // 局面の読み取りを試みます。 if (parser.TryParse(line)) { return true; } var item = KifUtil.ParseHeaderItem(line); if (item != null) { if (item.Key == "手合割" && item.Value != "その他") { this.startBoard = BoardTypeUtil.CreateBoardFromName(item.Value); } if (header != null) { // 可能ならヘッダアイテムを設定します。 var type = KifUtil.GetHeaderType(item.Key); if (type != null) { header[type.Value] = item.Value; } } return true; } // ヘッダが正しく読めない場合、 // 区切りなしに指し手行に入っている可能性があります。 if (MoveLineRegex.IsMatch(line)) { this.isKif = true; } return false; }
/// <summary> /// ヘッダー部分をまとめてパースします。 /// </summary> /// <remarks> /// 開始局面の設定も行います。 /// </remarks> private KifuHeader ParseHeader(KifMoveNode head) { var header = new KifuHeader(); var parser = new BodParser(); while (ParseHeaderLine(this.currentLine, parser, header, head)) { ReadNextLine(); } if (parser.IsBoardParsing) { throw new FileFormatException( "局面の読み取りを完了できませんでした。"); } this.startBoard = MakeStartBoard(parser); return header; }
/// <summary> /// このReaderで与えられたファイルを処理できるか調べます。 /// </summary> public bool CanHandle(TextReader reader) { try { this.reader = reader; this.currentLines = null; this.currentLineIndex = 0; this.lineNumber = 0; this.header = new KifuHeader(); this.startBoard = null; this.board = null; this.rootNode = new MoveNode(); this.lastNode = this.rootNode; // ファイルを3行読めれば、CSA形式であると判断します。 var parser = new CsaBoardParser(); for (var i = 0; i < 3; ++i) { var line = ReadNextLine(); if (line == null) { return true; } if (!ParseLine(line, parser)) { return false; } } return true; } catch (Exception) { return false; } }
/// <summary> /// ファイル内容から棋譜ファイルを読み込みます。 /// </summary> public KifuObject Load(TextReader reader) { if (reader == null) { throw new ArgumentNullException("reader"); } this.reader = reader; this.currentLines = null; this.currentLineIndex = 0; this.lineNumber = 0; this.header = new KifuHeader(); this.startBoard = null; this.board = null; this.rootNode = new MoveNode(); this.lastNode = this.rootNode; return LoadCsa(); }
/// <summary> /// ki2形式のファイルを読み込みます。 /// </summary> private KifuObject LoadKi2(KifuHeader header, Board board) { // 先にリスト化しないと、ConvertMoveでエラーがあった時に // ファイルを最後までパースしなくなります。 var moveList = ParseMoveLinesKi2().ToList(); var bmoveList = board.ConvertMove(moveList); Exception error = null; if (moveList.Count() != bmoveList.Count()) { if (moveList.Count() - 1 != bmoveList.Count()) { error = new FileFormatException( string.Format( "{0}手目の'{1}'を正しく処理できませんでした。", bmoveList.Count() + 1, moveList[bmoveList.Count()])); } else { error = new FileFormatException( "最終手が反則で終わっています。"); } } return new KifuObject(header, board, bmoveList, error); }
/// <summary> /// ヘッダー行をパースします。 /// </summary> private bool ParseHeaderLine(string line, BodParser parser, KifuHeader header = null) { if (line == null) { // ファイルの終了を意味します。 return false; } if (IsCommentLine(line)) { return true; } // 読み飛ばすべき説明行 if (line.Contains("手数----指手---------消費時間")) { this.isKif = true; // kif形式です。 return true; } // 局面の読み取りを試みます。 if (parser.TryParse(line)) { return true; } var item = KifUtil.ParseHeaderItem(line); if (item != null) { if (item.Key == "手合割" && item.Value != "その他") { this.startBoard = BoardTypeUtil.CreateBoardFromName(item.Value); } if (header != null) { // 可能ならヘッダアイテムを設定します。 var type = KifUtil.GetHeaderType(item.Key); if (type != null) { header[type.Value] = item.Value; } } return true; } return false; }
/// <summary> /// ファイル内容から棋譜ファイルを読み込みます。 /// </summary> public KifuObject Load(TextReader reader) { if (reader == null) { throw new ArgumentNullException("reader"); } this.reader = reader; this.currentLines = null; this.currentLineIndex = 0; this.lineNumber = 0; Header = new KifuHeader(); Board = null; // ヘッダーや局面の読み取り Parse(); return new KifuObject(Header, Board); }
public void Output(Position pos, KifuHeader kifuHeader) { Draw(pos, kifuHeader).ToFile(); }
Paragraph Draw(Position pos, KifuHeader kifuHeader) { Color sideToView = Color.BLACK; // どちらの手番から見た盤面を出力するか int gamePly = pos.gamePly; // 手数が1以外なら直前の指し手を出力する const float boardTopMargin = 56; const float boardLeftMargin = 56; const float boardRightMargin = 56; const float boardBottomMargin = 56; const float boardBorder = 24; // 座標の表示領域 const float blockSize = 60; // マス目 // const int boardSize = blockSize * 9 + boardBorder * 2; const float starSize = 5; const float pieceSize = 50; const float fontSize = 30; const float boardFontSize = 16; const float boardLeft = boardLeftMargin + boardBorder; const float boardRight = boardLeftMargin + boardBorder + blockSize * 9; const float boardTop = boardTopMargin + boardBorder; const float boardBottom = boardTopMargin + boardBorder + blockSize * 9; const float svgWidth = boardRight + boardBorder + boardRightMargin; const float svgHeight = boardBottom + boardBorder + boardBottomMargin; // 直線群の描画 Paragraph drawLines(IEnumerable <string> pathes, float strokeWidth = 1) => new Element("path", "", new Dictionary <string, string>() { { "d", string.Join(" ", pathes) }, { "stroke-width", float.IsNaN(strokeWidth) ? "" : $"{strokeWidth}" }, }).ToParagraph(); // 四角形の描画 Paragraph drawSquare(float x, float y, float size, float strokeWidth = 1, string fill = "none") => new Element("path", "", new Dictionary <string, string>() { { "d", $"M {x},{y} h {size} v {size} h {-size} z" }, { "stroke-width", float.IsNaN(strokeWidth) ? "" : $"{strokeWidth}" }, { "fill", fill }, }).ToParagraph(); // 円の描画 Paragraph drawCircle(float cx, float cy, float r) => new Element("circle", "", new Dictionary <string, string>() { { "cx", $"{cx}" }, { "cy", $"{cy}" }, { "r", $"{r}" }, }).ToParagraph(); // 駒形五角形の描画 // x, yは図形の上端中央を表す Paragraph drawPentagon(float x, float y, bool isBlack = true) { var x1 = x; var y1 = y; var x2 = x + fontSize * 0.37f; var y2 = y + fontSize * 0.20f; var x3 = x + fontSize * 0.46f; var y3 = y + fontSize; var x4 = x - fontSize * 0.46f; var y4 = y + fontSize; var x5 = x - fontSize * 0.37f; var y5 = y + fontSize * 0.20f; // 座標を整数に丸めて出力する return(new Element("path", "", new Dictionary <string, string>() { { "d", $"M {x1:f0},{y1:f0} L {x2:f0},{y2:f0} {x3:f0},{y3:f0} {x4:f0},{y4:f0} {x5:f0},{y5:f0} z" }, { "stroke-width", "1" }, { "fill", isBlack ? "black" : "none" }, }).ToParagraph()); } // 文字の描画 Element textElement(float x, float y, string t, float size = float.NaN) => new Element("text", t, new Dictionary <string, string>() { { "x", $"{x:f0}" }, { "y", $"{y:f0}" }, { "font-size", float.IsNaN(size) ? "" : $"{size}" }, }); Paragraph drawText(float x, float y, string t, float size = float.NaN) => textElement(x, y, t, size).ToParagraph(); // 直前の指し手情報の描画 Paragraph drawState() { var st = pos.State(); var move = st == null ? Move.NONE : st.lastMove; var moveStr = ""; if (move != Move.NONE && !move.IsSpecial()) { var lastPos = pos.Clone(); lastPos.UndoMove(); moveStr = lastPos.ToKi2(move); } Paragraph tempP = new Paragraph(); if (gamePly == 1) { } else if (move != Move.NONE && !move.IsSpecial()) { var x = boardLeft + blockSize * 4.5f; #if NeglectEdge var y = boardTopMargin * 0.5f; #else var y = boardTopMargin * 0.5f + fontSize * 0.5f; #endif var str1 = $"【図は{gamePly - 1}手目 "; var str2 = $"{moveStr} まで】"; var str = $"{str1}{str2}"; tempP.Concat(drawText(x, y, str, fontSize)); // 駒文字はフォントで出した方が簡単だけど…… Color prevSide = pos.sideToMove == Color.BLACK ? Color.WHITE : Color.BLACK; // おおよその長さ var lenStr = Converter.EastAsianWidth.legacyWidth(str); var lenStr2 = Converter.EastAsianWidth.legacyWidth(str2); var px = x + (lenStr * fontSize / 4) - (lenStr2 * fontSize / 2) - fontSize / 2; #if NeglectEdge var py = y - fontSize / 2; #else var py = y - fontSize; #endif tempP.Concat(drawPentagon(px, py, prevSide == Color.BLACK)); } else { var x = boardLeft + blockSize * 4.5f; var y = fontSize + 10f; var str = $"【図は{gamePly - 1}手目まで】"; tempP.Concat(drawText(x, y, str, fontSize)); } tempP.InsertTag("g", "class=\"state\""); return(tempP); } // 駒の描画 Paragraph drawPiece(int file, int rank, string t, bool isReverse = false, bool isBold = false) { float baseX = boardRight + blockSize / 2; float baseY = boardTop - blockSize / 2; float offsetX = baseX - file * blockSize; float offsetY = baseY + rank * blockSize; float correct; // 駒はマス目の中央よりやや下に置く補正 #if NeglectEdge correct = pieceSize * 0f; #else correct = pieceSize * 0.4f; // IEでは縦のセンタリングが効かないのを無理矢理補正 #endif var textElem = textElement(offsetX, offsetY + correct, t); if (isReverse) { textElem.attrs["transform"] = $"rotate(180,{offsetX},{offsetY})"; } if (isBold) { textElem.attrs["class"] = "pieceb"; } return(textElem.ToParagraph()); } // 名前、持駒領域の描画 // とりあえず正位置でレンダリングして盤の中央で180回転させれば逆位置に行く // 縦書きで複数文字レンダリングするとブラウザによって挙動が異なるが我慢する // - IE11/Edgeでは縦書き文字の位置が半文字分ほどずれる Paragraph drawHands() { var x = boardRight + boardBorder + boardRightMargin * 0.4f; var y = boardTop + boardTopMargin + fontSize / 2; var offsetX = boardLeft + blockSize * 4.5f; var offsetY = boardTop + blockSize * 4.5f; var playerName = new[] { kifuHeader.PlayerNameBlack, kifuHeader.PlayerNameWhite, }; var playerNameLen = new[] { Converter.EastAsianWidth.legacyWidth(playerName[0]), Converter.EastAsianWidth.legacyWidth(playerName[1]), }; var handBlack = pos.Hand(Color.BLACK); var handWhite = pos.Hand(Color.WHITE); var handStr = new[] { handBlack == Hand.ZERO ? "なし" : handBlack.Pretty2(), handWhite == Hand.ZERO ? "なし" : handWhite.Pretty2(), }; var HandStrLen = new[] { Converter.EastAsianWidth.legacyWidth(handStr[0]), Converter.EastAsianWidth.legacyWidth(handStr[1]), }; #if NeglectEdge var fixLength = 26; var justifyLength = 22; #else var fixLength = 24; #endif var handFullMaxLen = System.Math.Max(playerNameLen[0] + HandStrLen[0], playerNameLen[1] + HandStrLen[1]); var twoSided = 32 < handFullMaxLen; var tempP = new Paragraph(); foreach (var c in All.Colors()) { var cI = c.ToInt(); var sideP = drawPentagon(x, y - fontSize * 1.3f, c == Color.BLACK); if (twoSided) { var hand1 = playerName[cI]; var hand2 = $"持駒 {handStr[cI]}"; var hand1Len = playerNameLen[cI]; var hand2Len = HandStrLen[cI] + 6; // 文字数に応じて適当にフォントサイズを変更する var renderSize1 = System.Math.Min(fontSize / 1.5f, hand1Len <= fixLength ? fontSize : fontSize * fixLength / hand1Len); var renderSize2 = System.Math.Min(fontSize / 1.5f, hand2Len <= fixLength ? fontSize : fontSize * fixLength / hand2Len); var handSize = System.Math.Max(renderSize1, renderSize2); // 出力を簡潔にするため、フォントサイズがデフォルトのときはfont-sizeを省略する var hand1Elem = textElement(x + handSize * 0.6f, y, hand1, fontSize != renderSize1 ? renderSize1 : float.NaN); var hand2Elem = textElement(x - handSize * 0.6f, y, hand2, fontSize != renderSize2 ? renderSize2 : float.NaN); #if NeglectEdge // 両端揃え // textLength を設定すると IE / Edge で描画が崩壊する if (hand1Len > justifyLength) { hand1Elem.attrs["textLength"] = $"{boardBottom - y}"; hand1Elem.attrs["lengthAdjust"] = "spacingAndGlyphs"; } if (hand2Len > justifyLength) { hand2Elem.attrs["textLength"] = $"{boardBottom - y}"; hand2Elem.attrs["lengthAdjust"] = "spacingAndGlyphs"; } #endif sideP.Concat(hand1Elem.ToString()); sideP.Concat(hand2Elem.ToString()); } else { var handFull = $"{playerName[cI]} 持駒 {handStr[cI]}"; // 1段組の時は両者のフォントサイズを揃える var handLen = handFullMaxLen + 8; // 出力を簡潔にするため、フォントサイズがデフォルトのときはfont-sizeを省略する var handElem = textElement(x, y, handFull, handLen > fixLength ? fontSize * fixLength / handLen : float.NaN); #if NeglectEdge // 両端揃え // textLength を設定すると IE / Edge で描画が崩壊する if (handLen > justifyLength) { handElem.attrs["textLength"] = $"{boardBottom - y}"; handElem.attrs["lengthAdjust"] = "spacingAndGlyphs"; } #endif sideP.Concat(handElem.ToString()); } if (sideToView != c) { sideP.InsertTag("g", $"transform=\"rotate(180,{offsetX},{offsetY})\""); } tempP.Concat(sideP); } tempP.InsertTag("g", "class=\"hand\""); return(tempP); } // 将棋盤の描画 Paragraph drawBoard() { var tempP = drawSquare(boardLeft, boardTop, blockSize * 9, 4); var pathBuf = new List <string>(); for (int i = 1; i < 9; ++i) { pathBuf.Add($"M {boardLeft},{boardTop + blockSize * i} h {blockSize * 9}"); pathBuf.Add($"M {boardLeft + blockSize * i},{boardTop} v {blockSize * 9}"); } tempP.Concat(drawLines(pathBuf)); tempP.Concat(drawCircle(boardLeft + blockSize * 3, boardTop + blockSize * 3, starSize)); tempP.Concat(drawCircle(boardLeft + blockSize * 3, boardTop + blockSize * 6, starSize)); tempP.Concat(drawCircle(boardLeft + blockSize * 6, boardTop + blockSize * 3, starSize)); tempP.Concat(drawCircle(boardLeft + blockSize * 6, boardTop + blockSize * 6, starSize)); string[] fileStr = { "9", "8", "7", "6", "5", "4", "3", "2", "1" }; string[] rankStr = { "一", "二", "三", "四", "五", "六", "七", "八", "九" }; for (var i = 0; i < 9; ++i) { var xf = boardLeft + blockSize * (i + 0.5f); var xr = boardRight + boardBorder * 0.5f; #if NeglectEdge var yf = boardTopMargin + boardBorder * 0.5f; var yr = boardTop + blockSize * (i + 0.5f); #else var yf = boardTopMargin + boardBorder * 0.5f + boardFontSize * 0.4f; var yr = boardTop + blockSize * (i + 0.5f) + boardFontSize * 0.4f; #endif tempP.Concat(drawText(xf, yf, fileStr[i])); tempP.Concat(drawText(xr, yr, rankStr[i])); } tempP.InsertTag("g", "class=\"board\""); return(tempP); } // 盤上の駒の描画 Paragraph drawBoardPiece() { var tempP = new Paragraph(); var lastMove = pos.State().lastMove; for (SquareHand sqh = SquareHand.SquareZero; sqh < SquareHand.SquareNB; ++sqh) { var pi = pos.PieceOn(sqh); if (pi != Piece.NO_PIECE) { var sq = (Square)sqh; var file = sideToView == Color.BLACK ? (int)sq.ToFile() + 1 : 9 - (int)sq.ToFile(); var rank = sideToView == Color.BLACK ? (int)sq.ToRank() + 1 : 9 - (int)sq.ToRank(); char[] piChar = { pi.Pretty2() }; var piStr = new string(piChar); var isReverse = pi.PieceColor() == Color.WHITE; if (sideToView == Color.WHITE) { isReverse = !isReverse; } // 直前の指し手を強調する bool isBold = gamePly != 1 && lastMove != Move.NONE && !lastMove.IsSpecial() && sq == pos.State().lastMove.To(); if (isBold) { tempP.Concat(drawSquare(boardRight - file * blockSize, boardTop + (rank - 1) * blockSize, blockSize, float.NaN, "#ffff80")); } tempP.Concat(drawPiece(file, rank, piStr, isReverse, isBold)); } } tempP.InsertTag("g", "class=\"piece\""); return(tempP); } var p = new Paragraph(); p.Concat(MakeStyle()); p.Concat(drawState()); p.Concat(drawHands()); p.Concat(drawBoardPiece()); p.Concat(drawBoard()); // svgのヘッダーとフッターを追加 p.InsertTag("svg", $"xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" xml:lang=\"ja-JP\" viewBox=\"0 0 {svgWidth} {svgHeight}\" width=\"{svgWidth}\" height=\"{svgHeight}\""); return(p); }