UNPKG

shogi-player

Version:

Shogi board web components has functions for replaying, manipulating, and editing

515 lines (435 loc) 14.5 kB
import _ from "lodash" import Vue from "vue" import { Board } from "./board.js" import { Place } from "./place.js" import { Piece } from "./piece.js" import { Soldier } from "./soldier.js" import { SfenParser } from "./sfen_parser.js" import { SfenSerializer } from "./sfen_serializer.js" import { PresetInfo } from "./preset_info.js" import { Location } from "./location.js" import { Xinteger } from "beetleshine/lib/xinteger" export class Xcontainer { constructor() { const data_source = new SfenParser() data_source.raw_body = "position startpos" data_source.parse() this.data_source = data_source this.current_turn = 0 this.board = null this.hold_pieces = null this.last_hand = null this.piece_box = {} this.env = process.env.NODE_ENV } run() { this.board = this.data_source.board this.hold_pieces = this.data_source.hold_pieces this.last_hand = null const move_infos = this.data_source.move_infos const num = this.turn_offset - this.turn_offset_min _(num).times((i) => { this.execute_one(move_infos[i]) }) } execute_one(m) { this.last_hand = m if (m.drop_piece) { const soldier = new Soldier({ piece: m.drop_piece, place: m.place, promoted: m.promoted, location: m.location, }) this.hold_pieces_add(m.location, soldier.piece, -1) this.board.place_on(soldier) } else { { const soldier = this.board.lookup(m.place) if (soldier) { this.hold_pieces_add(m.location, soldier.piece, 1) } } const soldier = this.board.lookup(m.origin_place) if (m.promoted_trigger) { soldier.promoted = true } soldier.place = m.place this.board.delete_at(m.origin_place) this.board.place_on(soldier) } } hold_pieces_count(location, piece) { return this.hold_pieces[location.key][piece.key] || 0 } // 持駒が空か? hold_pieces_blank_p(location) { return Object.keys(this.hold_pieces[location.key]).length === 0 } hold_pieces_add(location, piece, plus = 1) { const count = this.hold_pieces_count(location, piece) + plus const counts_hash = this.hold_pieces[location.key] // 次のように書いた場合、ハッシュの値(count)がいくら変化してもそのタイミングではトリガーが発生しない // // if (count >= 1) { // Vue.set(counts_hash, piece.key, count) // ←ここが問題 // } else { // Vue.delete(counts_hash, piece.key) // } // // そこで count を更新するときは「キーが新規で追加」されたことでトリガーを発生させるようにする // つまり count を更新するときは「キー削除」→「キー追加」とする Vue.delete(counts_hash, piece.key) if (count >= 1) { Vue.set(counts_hash, piece.key, count) } } // location の piece を count 減らしたいとき本当に減らせる数を返す hold_pieces_can_be_reduced_count(location, piece, count) { const max = this.hold_pieces_count(location, piece) if (count > max) { count = max } return count } board_safe_delete_on(place) { this.board.delete_at(place) } board_piece_fore_class(xy) { const place = Place.fetch(xy) const soldier = this.board.lookup(place) if (soldier) { return soldier.css_class_list } return [] } cell_piece_class(xy) { const place = Place.fetch(xy) const soldier = this.board.lookup(place) let list = [] if (soldier) { list.push(`location_${soldier.location.key}`) } return list } cell_view(xy) { const place = Place.fetch(xy) const soldier = this.board.lookup(place) let str = "" if (soldier) { str = soldier.name } return str } get dimension() { return Board.dimension } // ruby style array index access get turn_offset() { let index = Number(this.current_turn) if (index < 0) { index += this.turn_offset_max + 1 } return this.turn_clamp(index) } turn_clamp(index) { return _.clamp(Number(index), this.turn_offset_min, this.turn_offset_max) } turn_cycle(index) { return Xinteger.imodulo(Number(index), this.turn_offset_max + 1) } get previous_location() { return this.data_source.location_by_offset(this.turn_offset - 1) } get current_location() { return this.data_source.location_by_offset(this.turn_offset) } get current_comments() { if (this.data_source.comment_lines_hash) { return this.data_source.comment_lines_hash[this.turn_offset] } } get turn_offset_min() { return this.data_source.turn_offset_min } get turn_offset_max() { return this.data_source.turn_offset_max } get current_turn_label() { if (this.turn_offset === this.turn_offset_max) { return `まで${this.display_turn}手で${this.previous_location.name}の勝ち` } else { return `${this.display_turn}手` } } // 100手目から始まっている棋譜でオフセットが20のときは足して 120 を返す get display_turn() { return this.turn_base + this.turn_offset } // 何手目から始まっているかを返す get turn_base() { return this.data_source.turn_base } // 持駒 realized_hold_pieces_of(location) { const list = Object.entries(this.hold_pieces[location.key]) return _(list) // .filter(([key, count]) => count >= 1) .map(([key, count]) => [Piece.fetch(key), count]) .sortBy(([key, count]) => key.code) .value() } // -------------------------------------------------------------------------------- serialize // 4k4/9/4G4/9/9/9/9/9/9 b G2r2b2g4s4n4l18p 1 get to_simple_sfen() { return this.sfen_serializer.to_s } // 4k4/9/4G4/9/9/9/9/9/9 b G2r2b2g4s4n4l18p get to_sfen_without_turn() { return this.sfen_serializer.to_s_without_turn } // position sfen 4k4/9/4G4/9/9/9/9/9/9 b G2r2b2g4s4n4l18p 1 get to_short_sfen() { return `position sfen ${this.to_simple_sfen}` } get sfen_serializer() { return new SfenSerializer(this) } // -------------------------------------------------------------------------------- piece_box piece_box_count(piece) { return this.piece_box[piece.key] || 0 } piece_box_add(piece, plus = 1) { const count = this.piece_box_count(piece) + plus Vue.delete(this.piece_box, piece.key) if (count >= 1) { Vue.set(this.piece_box, piece.key, count) } } // piece を count 減らしたいとき本当に減らせる数を返す piece_box_can_be_reduced_count(piece, count) { const max = this.piece_box_count(piece) if (count > max) { count = max } return count } get piece_box_realize() { const list = Object.entries(this.piece_box) // {a: 1} => [['a', 1]] return _(list) .filter(([key, count]) => count >= 1) .map(([key, count]) => [Piece.fetch(key), count]) .sortBy(([key, count]) => key.code) .value() } // -------------------------------------------------------------------------------- Utilities // location の駒台の駒をすべて駒箱に移動する hold_pieces_to_piece_box(location) { _.forIn(this.hold_pieces[location.key], (count, piece_key) => { const piece = Piece.fetch(piece_key) this.hold_pieces_add(location, piece, -count) this.piece_box_add(piece, count) }) } // 駒箱の駒をすべて location の駒台に移動する piece_box_to_hold_pieces(location) { _.forIn(this.piece_box, (count, piece_key) => { const piece = Piece.fetch(piece_key) this.piece_box_add(piece, -count) this.hold_pieces_add(location, piece, count) }) } // プリセットに対応するように駒箱をセットする piece_box_reset_by_preset(preset_info) { this.piece_box_clear() const info = PresetInfo.fetch(preset_info) info.piece_box.forEach(([e, c]) => { this.piece_box_add(Piece.fetch(e), c) }) } // 駒箱に足りない駒だけにする piece_box_piece_counts_adjust() { this.piece_box_clear() const counts_hash = this.hold_piece_all_counts_hash // 両者の持駒の合計 const counts_hash_on_board = this.board.piece_counts_hash // 盤上の駒の合計 const info = PresetInfo.fetch("全部駒箱") info.piece_box.forEach(([e, c]) => { const piece = Piece.fetch(e) const rest = c - ((counts_hash[piece.key] || 0) + (counts_hash_on_board[piece.key] || 0)) this.piece_box_add(piece, rest) }) } // 両者の持駒を合わせたハッシュを返す get hold_piece_all_counts_hash() { const counts = {} Location.values.forEach(e => { _.forIn(this.hold_pieces[e.key], (count, piece_key) => { counts[piece_key] = (counts[piece_key] || 0) + count }) }) return counts } piece_box_clear() { this.piece_box = {} } //////////////////////////////////////////////////////////////////////////////// ランダム配置 //////////////////////////////////////////////////////////////////////////////// 指将棋用玉配置 king_formation_auto_set_on_off(v) { if (v) { return this.king_formation_auto_set() } else { return this.king_formation_auto_unset() } } // 指将棋用玉配置(自動) king_formation_auto_set() { let success = false if (!success) { success = this.king_formation_set("bottom_left") } if (!success) { success = this.king_formation_set("bottom_right") } return success } // 指将棋用玉配置解除 // 玉は駒箱へ // それ以外は相手の駒台へ king_formation_auto_unset() { let success = false if (!success) { success = this.king_formation_unset("bottom_left") } if (!success) { success = this.king_formation_unset("bottom_right") } return success } // 指将棋用玉配置 king_formation_set(position) { const soldiers = this.king_formation_soldiers(position) // 置きたいところに駒が1つでも置かれていたら何もしない if (soldiers.some(e => this.board.lookup(e.place))) { return } // 配置 soldiers.forEach(e => this.piece_search_and_place_on(e)) return true } // 指将棋用玉配置解除 // 玉は駒箱へ // それ以外は相手の駒台へ king_formation_unset(position) { const soldiers = this.king_formation_soldiers(position) // 駒がそろってないときは何もしない if (soldiers.some(e => !this.board.lookup(e.place))) { return } soldiers.forEach(e => { const soldier = this.board.lookup(e.place) if (soldier) { const piece = soldier.piece this.board.delete_at(soldier.place) if (piece.key === "K") { // 玉の場合は駒箱にとらげる this.piece_box_add(piece) } else { // 他の駒は相手の駒台へ this.hold_pieces_add(Location.fetch("white"), piece) } } }) return true } // soldier.piece に対応する駒を探してあれば -1 して soldier.place の位置に配置する piece_search_and_place_on(soldier) { // すでに何か置かれていれば何もしない if (this.board.lookup(soldier.place)) { return } // 玉の場合は初期配置の時点で存在しない場合もあるので「あれば」-1 するだけ if (soldier.piece.key === "K") { this.piece_search_and_decrement(soldier.piece) } else { // 玉以外は駒が数が増えてしまってややこしくなるのを防ぐため必ず「あったときだけ」-1 し、なければ何もしない if (!this.piece_search_and_decrement(soldier.piece)) { return } } this.board.place_on(soldier) } // 相手の駒→駒箱→自分の駒の順で駒を探してあれば -1 して true を返す piece_search_and_decrement(piece) { let found = false // 相手の駒から探す if (!found) { found = this.piece_search_on_hold_pieces_and_decrement("white", piece) } // 駒箱から探す if (!found) { if (this.piece_box_count(piece) >= 1) { this.piece_box_add(piece, -1) found = true } } if (false) { // 自分の駒から探す if (!found) { found = this.piece_search_on_hold_pieces_and_decrement("black", piece) } } return found } // location_key の持駒から piece を探してあれば -1 して true を返す piece_search_on_hold_pieces_and_decrement(location_key, piece) { const location = Location.fetch(location_key) if (this.hold_pieces_count(location, piece) >= 1) { this.hold_pieces_add(location, piece, -1) return true } } // TODO: SFENで定義する方法もあり king_formation_soldiers(position) { let bx = null let sx = null let by = null let sy = null if (position === "bottom_left") { bx = 0 sx = 1 by = Board.dimension - 1 sy = -1 } if (position === "bottom_right") { bx = Board.dimension - 1 sx = -1 by = Board.dimension - 1 sy = -1 } return [ { piece: "K", promoted: false, location: "black", place: [bx, by ] }, { piece: "P", promoted: true, location: "white", place: [bx, by + sy + sy ] }, { piece: "P", promoted: true, location: "white", place: [bx + sx, by + sy + sy ] }, { piece: "P", promoted: true, location: "white", place: [bx + sx + sx, by + sy + sy ] }, { piece: "P", promoted: true, location: "white", place: [bx + sx + sx, by + sy ] }, { piece: "P", promoted: true, location: "white", place: [bx + sx + sx, by ] }, ].map(e => { return new Soldier({ piece: Piece.fetch(e.piece), promoted: e.promoted, location: Location.fetch(e.location), place: Place.fetch(e.place), }) }) } //////////////////////////////////////////////////////////////////////////////// // 左右スライド slide_xy(x, y) { this.board = this.board.slide_xy(x, y) } shuffle_apply(size) { const new_board = this.board.shuffle_apply(size) if (new_board) { this.board = new_board return true } } }