UNPKG

ymir-js

Version:

This toolkit is created to make it easier for you to develop games like chess, checkers, go, match 3 puzzle and more. It is still under development.

389 lines (311 loc) 11.6 kB
import * as _ from 'lodash'; import getAvailableColumns from '../../../utils/getAvailableColumns'; import parseCoord from '../../../utils/parseCoord'; import Board from '../../core/board'; import { MovementType } from '../../core/item'; import Item, { CheckersColorType, CheckersItemType } from './item'; export type BoardConfig = { x: number; y: number; }; export type CheckersBoardType = { [key: string]: { item: CheckersItemType } }; export type AttactCoord = { coord: string; destroyItemCoord: string }; export type DefendCoord = { coord: string; inDangerItemCoord: string }; class TurkishCheckersBoard extends Board { board: CheckersBoardType; constructor(config: BoardConfig = { x: 8, y: 8 }) { super(config); return this; } init() { const whiteItemCoords = '1|0 1|1 1|2 1|3 1|4 1|5 1|6 1|7 2|0 2|1 2|2 2|3 2|4 2|5 2|6 2|7'; const blackItemCoords = '5|0 5|1 5|2 5|3 5|4 5|5 5|6 5|7 6|0 6|1 6|2 6|3 6|4 6|5 6|6 6|7'; whiteItemCoords.split(' ').forEach((coord) => { this.setItem(coord, new Item({ color: 'white' })); }); blackItemCoords.split(' ').forEach((coord) => { this.setItem(coord, new Item({ color: 'black' })); }); return this; } reset(): void { Object.keys(this.board).forEach(this.removeItem); this.init(); } getItemsBetweenTwoCoords(fromCoord: string, toCoord: string): string[] { const direction = this.getDirection(fromCoord, toCoord); const distance = this.getDistanceBetweenTwoCoords(fromCoord, toCoord); const convertDirection = { top: 'y', bottom: 'y', left: 'x', right: 'x', }; const stepCount = Math.abs(distance[convertDirection[direction]]) || 1; const movement = { stepCount }; movement[direction] = true; const betweenCoords = []; return betweenCoords .concat(...Object.values(getAvailableColumns(fromCoord, movement))) .filter((coord) => !this.isEmpty(coord)); } getAvailableCoordsByColor = ( color: CheckersColorType ): Record<string, string[]> => { const currentItems = []; const availableCoords = {}; Object.entries(this.board).forEach(([coord, col]) => { if (col.item && col.item.color === color) { currentItems.push(coord); } }); currentItems.forEach((coord) => { const item = this.getItem(coord); const currentAvailableCoords = this.getAvailableColumns( coord, item.movement ); if (currentAvailableCoords.length) { availableCoords[coord] = currentAvailableCoords; } }); return availableCoords; }; getAttackCoordsByColor = ( color: CheckersColorType ): { [coord: string]: AttactCoord[] } => { const availableCoords = this.getAvailableCoordsByColor(color); const attackCoords = {}; Object.entries(availableCoords).forEach( ([coord, currentAvailableCoords]) => { currentAvailableCoords.forEach((currentAvailableCoord) => { const [destroyItemCoord] = this.getItemsBetweenTwoCoords( coord, currentAvailableCoord ); if (destroyItemCoord) { if (attackCoords[coord]) { attackCoords[coord].push({ coord: currentAvailableCoord, destroyItemCoord, }); } else { attackCoords[coord] = [ { coord: currentAvailableCoord, destroyItemCoord }, ]; } } }); } ); return attackCoords; }; getDefendCoordsByColor = ( color: CheckersColorType ): { [coord: string]: DefendCoord[] } => { const defendCoords = {}; const enemyColor = color === 'white' ? 'black' : 'white'; const availableCoords = this.getAvailableCoordsByColor(color); const enemyAttackCoords = this.getAttackCoordsByColor(enemyColor); Object.entries(enemyAttackCoords).forEach(([, enemyAttack]) => { Object.values(enemyAttack).forEach((enemyAttackCoord) => { Object.keys(availableCoords).forEach((availableItemOrigin) => { const availableColumnCoords = availableCoords[availableItemOrigin]; if ( availableColumnCoords.includes(enemyAttackCoord.coord) && availableItemOrigin !== enemyAttackCoord.destroyItemCoord ) { const defendCoordItem = { coord: enemyAttackCoord.coord, inDangerCoord: enemyAttackCoord.destroyItemCoord, }; if (defendCoords[availableItemOrigin]) { defendCoords[availableItemOrigin].push(defendCoordItem); } else { defendCoords[availableItemOrigin] = [defendCoordItem]; } } }); }); }); return defendCoords; }; getItemsByColor = (color: CheckersColorType): CheckersItemType[] => { return Object.values(this.board) .filter(({ item }) => item?.color === color) .map(({ item }) => item); }; getAvailableColumns = (coord: string, movement: MovementType): string[] => { if (this.isEmpty(coord)) return []; const columns = getAvailableColumns(coord, movement); const availableColumns: Record<string, string[]> = {}; const captureAvailableColumns = {}; const item = this.getItem(coord); Object.keys(columns).forEach((key) => { availableColumns[key] = []; let isFoundCapture = false; for (let i = 0; i < columns[key].length; i += 1) { const currentCoord = columns[key][i]; if (!this.isExistCoord(currentCoord)) continue; if (this.isEmpty(currentCoord)) { availableColumns[key].push(currentCoord); continue; } else if (isFoundCapture) { break; } const nextCoordItem = this.getItem(currentCoord); if (nextCoordItem?.color === item.color) { break; } else if ( !isFoundCapture && nextCoordItem && nextCoordItem.color !== item.color ) { const direction = this.getDirection(coord, currentCoord); const movementRule = { stepCount: 1, [direction]: true, }; const [afterCoord] = Object.values( getAvailableColumns(currentCoord, movementRule) ) .filter((arr) => arr.length) .flat(); const afterItem = this.getItem(afterCoord); if (afterItem) break; if (this.isEmpty(afterCoord)) { availableColumns[key] = [afterCoord]; captureAvailableColumns[key] = true; isFoundCapture = true; } else { break; } } } }); const resultCoords: Record<string, string[]> = {}; const isFoundAnySuccessDirection = Object.values( captureAvailableColumns ).some((direction) => direction); Object.keys(availableColumns).forEach((key) => { if (!isFoundAnySuccessDirection) { resultCoords[key] = _.uniq(availableColumns[key]); } else if (captureAvailableColumns[key]) { resultCoords[key] = _.uniq(availableColumns[key]); } }); return Object.values(resultCoords).flat(); }; autoPlay = (color: CheckersColorType, { onSelect, onMove }): void => { // AVAILABLE SUCCESS MOVES const availableAttacks = this.getAttackCoordsByColor(color); const availableAttacksOriginn = Object.keys(availableAttacks); if (availableAttacksOriginn.length) { const [attackOriginn] = availableAttacksOriginn; const [availableAttackColumn] = availableAttacks[attackOriginn]; onSelect?.(attackOriginn); onMove?.(attackOriginn, availableAttackColumn.coord); return; } // AVAILABLE DEFEND MOVES const potentialDefendsMoves = this.getDefendCoordsByColor(color); const potentialDefendsItem = Object.keys(potentialDefendsMoves); if (potentialDefendsItem.length) { const [defendOriginn] = potentialDefendsItem; const [availableDefendColumn] = potentialDefendsMoves[defendOriginn].map( (item) => item.coord ); onSelect?.(defendOriginn); onMove?.(defendOriginn, availableDefendColumn); return; } // AVAILABLE NORMAL MOVES const availableNormalMoves = this.getAvailableCoordsByColor(color); const availableCoords = Object.keys(availableNormalMoves); const availableEnemyNormalMoves = Object.values( this.getAvailableCoordsByColor(color === 'white' ? 'black' : 'white') ).flat(); const safeMoves = {}; Object.values(availableNormalMoves).filter((moves, index) => { moves.forEach((move) => { if (!availableEnemyNormalMoves.includes(move)) { if (safeMoves[availableCoords[index]]) { safeMoves[availableCoords[index]].push(move); } else { safeMoves[availableCoords[index]] = [move]; } } }); }); let currentCoords = availableCoords; let currentMoves = availableNormalMoves; // KING POTENTIAL const kingRowId = color === 'white' ? 7 : 0; let potentialKingItemCoord; let potentialAvailableCoord; Object.keys(currentMoves).forEach((coord) => { const availableColumns = currentMoves[coord]; availableColumns.forEach((columnCoord) => { const [rowId] = parseCoord(coord); const [moveRowId] = parseCoord(columnCoord); if (rowId !== moveRowId && moveRowId === kingRowId) { potentialKingItemCoord = coord; potentialAvailableCoord = columnCoord; } }); }); if (potentialKingItemCoord && potentialAvailableCoord) { onSelect?.(potentialKingItemCoord); onMove?.(potentialKingItemCoord, potentialAvailableCoord); return; } const safeCoords = Object.keys(safeMoves); if (safeCoords.length) { currentCoords = safeCoords; currentMoves = safeMoves; } // RISK ESTIMATION currentCoords.forEach((currentCoord) => { currentMoves[currentCoord].forEach((move) => { const riskEstimationBoard = new TurkishCheckersBoard().updateBoard( JSON.parse(JSON.stringify(this.board)) ); riskEstimationBoard.moveItem(currentCoord, move); const potentialDefendColumns = riskEstimationBoard.getDefendCoordsByColor(color); if (Object.values(potentialDefendColumns).flat().length) { currentMoves[currentCoord] = currentMoves[currentCoord].filter( (mv) => mv !== move ); } }); if (!currentMoves[currentCoord].length) { currentCoords = currentCoords.filter((c) => c !== currentCoord); delete currentMoves[currentCoord]; } }); // PROPENSITY TO MOVE FORWARD currentCoords.forEach((currentCoord) => { currentMoves[currentCoord].sort((first, second) => { const [firstRowId] = parseCoord(first); const [secondRowId] = parseCoord(second); if (firstRowId > secondRowId) { return -1; } else if (firstRowId < secondRowId) { return 1; } else { return 0; } }); }); // [TODO]: No random, use foward move const randomIndex = Math.floor(Math.random() * currentCoords.length); const selectedCoord = currentCoords[randomIndex]; const moveCoord = currentMoves[selectedCoord]?.[0]; onSelect?.(selectedCoord); onMove?.(selectedCoord, moveCoord); }; } export default TurkishCheckersBoard;