first commit
101
.eslintrc.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:import/recommended',
|
||||||
|
'plugin:unicorn/recommended',
|
||||||
|
'plugin:sonarjs/recommended',
|
||||||
|
'plugin:react/jsx-runtime',
|
||||||
|
'next/core-web-vitals',
|
||||||
|
],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['**/*.jsx'],
|
||||||
|
rules: {
|
||||||
|
'unicorn/filename-case': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
case: 'pascalCase',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['pages/**/*'],
|
||||||
|
rules: {
|
||||||
|
'import/no-default-export': 'off',
|
||||||
|
'unicorn/filename-case': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: '2022',
|
||||||
|
},
|
||||||
|
plugins: ['react', 'chakra-ui', 'sort-keys-fix', 'prettier'],
|
||||||
|
rules: {
|
||||||
|
'chakra-ui/props-order': 'error',
|
||||||
|
'chakra-ui/props-shorthand': 'error',
|
||||||
|
'chakra-ui/require-specific-component': 'error',
|
||||||
|
'import/no-default-export': 'error',
|
||||||
|
'import/order': 'error',
|
||||||
|
'no-console': 'warn',
|
||||||
|
'no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
destructuredArrayIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'react/boolean-prop-naming': 'error',
|
||||||
|
'react/destructuring-assignment': ['error', 'never'],
|
||||||
|
'react/function-component-definition': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
namedComponents: 'function-declaration',
|
||||||
|
unnamedComponents: 'arrow-function',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'react/hook-use-state': 'error',
|
||||||
|
'react/jsx-boolean-value': 'error',
|
||||||
|
'react/jsx-curly-brace-presence': [
|
||||||
|
'error',
|
||||||
|
{ children: 'never', propElementValues: 'always', props: 'never' },
|
||||||
|
],
|
||||||
|
'react/jsx-fragments': 'error',
|
||||||
|
'react/jsx-handler-names': 'error',
|
||||||
|
'react/jsx-no-useless-fragment': 'error',
|
||||||
|
'react/jsx-pascal-case': 'error',
|
||||||
|
'react/jsx-wrap-multilines': 'error',
|
||||||
|
'react/no-array-index-key': 'error',
|
||||||
|
'react/no-invalid-html-attribute': 'error',
|
||||||
|
'react/no-this-in-sfc': 'error',
|
||||||
|
'react/no-typos': 'error',
|
||||||
|
'react/no-unstable-nested-components': 'error',
|
||||||
|
'react/no-unused-prop-types': 'error',
|
||||||
|
'react/prefer-stateless-function': 'error',
|
||||||
|
'sort-keys-fix/sort-keys-fix': 'warn',
|
||||||
|
'unicorn/filename-case': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
case: 'kebabCase',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'unicorn/no-null': 'off',
|
||||||
|
'unicorn/no-unsafe-regex': 'error',
|
||||||
|
'unicorn/prefer-at': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
checkAllIndexAccess: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'unicorn/prefer-module': 'off',
|
||||||
|
'unicorn/prevent-abbreviations': 'off',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
alias: {
|
||||||
|
extensions: ['.js', '.jsx'],
|
||||||
|
map: [['@draughts', './packages/draughts']],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
34
.gitignore
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
1
New Text Document.bat
Normal file
@ -0,0 +1 @@
|
|||||||
|
start "" http://localhost:3000 && npm run dev
|
||||||
3
components/adverts/BottomBannerAdvert.jsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function BottomBannerAdvert() {
|
||||||
|
return <div id="ezoic-pub-ad-placeholder-130" />;
|
||||||
|
}
|
||||||
3
components/adverts/LeftSidebarAdvert.jsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function LeftSidebarAdvert() {
|
||||||
|
return <div id="ezoic-pub-ad-placeholder-133" />;
|
||||||
|
}
|
||||||
3
components/adverts/RightSidebarAdvert.jsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function RightSidebarAdvert() {
|
||||||
|
return <div id="ezoic-pub-ad-placeholder-132" />;
|
||||||
|
}
|
||||||
3
components/adverts/TopBannerAdvert.jsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function TopBannerAdvert() {
|
||||||
|
return <div id="ezoic-pub-ad-placeholder-124" />;
|
||||||
|
}
|
||||||
61
components/content/DraughtsRulesContent.jsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { Text, VStack } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
export function DraughtsRulesContent() {
|
||||||
|
return (
|
||||||
|
<VStack align="start" spacing={2}>
|
||||||
|
<Text>
|
||||||
|
Permainan draf dimainkan di papan catur 64 persegi dengan delapan
|
||||||
|
deretan kotak berwarna gelap dan terang bergantian.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Ada dua pemain dan masing-masing memulai permainan dengan masing-masing 12 draf
|
||||||
|
pemain memiliki warna mereka sendiri.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Para pemain menempatkan draf mereka di tiga baris kotak gelap yang
|
||||||
|
paling dekat dengan mereka.
|
||||||
|
</Text>
|
||||||
|
<Text>Para pemain kemudian mulai bermain, melakukan satu gerakan pada satu waktu.</Text>
|
||||||
|
<Text>
|
||||||
|
Tujuan permainan ini adalah membuat lawan tidak bisa bergerak
|
||||||
|
ketika tiba giliran mereka.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Ini dilakukan dengan mengambil semua bidak mereka sepanjang permainan, atau
|
||||||
|
memblokir mereka sehingga mereka tidak punya tempat untuk bergerak.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Draf tunggal hanya dapat bergerak dalam arah diagonal ke depan ke a
|
||||||
|
persegi tanpa bagian lain di dalamnya.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Jika bidak lawan ada di kotak berikutnya, pemain bisa melompat
|
||||||
|
di atasnya dan tangkap, lepaskan potongan itu dari papan. Mereka hanya bisa
|
||||||
|
lakukan ini jika kotak berikutnya kosong.
|
||||||
|
</Text>
|
||||||
|
<Text>Pemain tidak pernah bisa melompati bagian mereka sendiri.</Text>
|
||||||
|
<Text>
|
||||||
|
Ketika seorang pemain berjalan melintasi papan ke yang lain
|
||||||
|
sisi pemain, bidak mereka akan berubah menjadi Raja. Ketika ini terjadi,
|
||||||
|
salah satu bidak yang diambil sebelumnya akan diletakkan di atas bidak
|
||||||
|
yang membuatnya ke sisi itu.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Setelah sepotong dibuat menjadi raja, itu akan dapat bergerak maju dan
|
||||||
|
mundur, memberikan lebih banyak kesempatan untuk menangkap bagian lawan.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Seorang raja dapat melompat sebanyak mungkin sehubungan dengan
|
||||||
|
kotak yang diperlukan sedang kosong. Namun, raja tidak bisa melompati
|
||||||
|
potongan-potongan yang memiliki warna yang sama dengan mereka.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Permainan akan berakhir setelah pemain tidak bisa lagi bergerak.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Jika kedua pemain tidak bisa bergerak kemana-mana, permainan akan berakhir dengan a
|
||||||
|
mengikat, atau menggambar.
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
components/draughts/DraughtsContext.jsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
DraughtsSettingsProvider,
|
||||||
|
DraughtsSettingsProviderProps,
|
||||||
|
} from './settings/DraughtsSettingsContext';
|
||||||
|
import {
|
||||||
|
DraughtsBoardProvider,
|
||||||
|
DraughtsBoardProviderProps,
|
||||||
|
} from './board/DraughtsBoardContext';
|
||||||
|
import { DraughtsGameProvider } from './game/DraughtsGameContext';
|
||||||
|
|
||||||
|
export function DraughtsProvider(props) {
|
||||||
|
return (
|
||||||
|
<DraughtsSettingsProvider {...props.settings}>
|
||||||
|
<DraughtsBoardProvider {...props.board}>
|
||||||
|
<DraughtsGameProvider>{props.children}</DraughtsGameProvider>
|
||||||
|
</DraughtsBoardProvider>
|
||||||
|
</DraughtsSettingsProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DraughtsProvider.propTypes = {
|
||||||
|
board: PropTypes.shape(DraughtsBoardProviderProps),
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
settings: PropTypes.shape(DraughtsSettingsProviderProps),
|
||||||
|
};
|
||||||
59
components/draughts/DraughtsGameInfo.jsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { HStack, VStack, Text, keyframes } from '@chakra-ui/react';
|
||||||
|
import { useDraughtsPlayerToMove } from './board/hooks/use-draughts-player-to-move';
|
||||||
|
import { useDraughtsGame } from './game/DraughtsGameContext';
|
||||||
|
import { useDraughtsSettings } from './settings/DraughtsSettingsContext';
|
||||||
|
import { Players } from '@draughts/core';
|
||||||
|
|
||||||
|
const glowKeyframe = keyframes`
|
||||||
|
from {
|
||||||
|
background-color: rgba(144, 238, 144, 0.3)
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background-color: rgba(144, 238, 144, 0.6)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const glowAnimation = `${glowKeyframe} 1s ease 0s infinite alternate`;
|
||||||
|
|
||||||
|
function formatTimer({ seconds, minutes }) {
|
||||||
|
const formatSeconds = String(seconds).padStart(2, '0');
|
||||||
|
return `${minutes}:${formatSeconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DraughtsGameInfoView() {
|
||||||
|
const { userPlayer } = useDraughtsSettings();
|
||||||
|
const { whiteTimer, blackTimer } = useDraughtsGame();
|
||||||
|
const whiteToMove = useDraughtsPlayerToMove(Players.WHITE);
|
||||||
|
const blackToMove = useDraughtsPlayerToMove(Players.BLACK);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack justify="space-between">
|
||||||
|
<HStack
|
||||||
|
align="center"
|
||||||
|
justify="space-between"
|
||||||
|
gap={3}
|
||||||
|
px={2}
|
||||||
|
borderRadius="0.2em"
|
||||||
|
animation={whiteToMove ? glowAnimation : undefined}
|
||||||
|
>
|
||||||
|
<Text fontSize="sm">
|
||||||
|
putih {userPlayer === Players.WHITE ? '(Anda)' : '(AI)'}
|
||||||
|
</Text>
|
||||||
|
<time>{formatTimer(whiteTimer)}</time>
|
||||||
|
</HStack>
|
||||||
|
<HStack
|
||||||
|
align="center"
|
||||||
|
justify="space-between"
|
||||||
|
gap={3}
|
||||||
|
px={2}
|
||||||
|
borderRadius="0.2em"
|
||||||
|
animation={blackToMove ? glowAnimation : undefined}
|
||||||
|
>
|
||||||
|
<Text fontSize="sm">
|
||||||
|
hitam {userPlayer === Players.BLACK ? '(Anda)' : '(AI)'}
|
||||||
|
</Text>
|
||||||
|
<time>{formatTimer(blackTimer)}</time>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
components/draughts/board/DraughtsBoardContext.jsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { createContext, useCallback, useContext, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Board, Pieces, Players } from '@draughts/core';
|
||||||
|
|
||||||
|
export const DraughtsBoardContext = createContext();
|
||||||
|
|
||||||
|
export const useDraughtsBoard = () => useContext(DraughtsBoardContext);
|
||||||
|
|
||||||
|
export function DraughtsBoardProvider(props) {
|
||||||
|
const [lastMove, setLastMove] = useState(null);
|
||||||
|
const [board, setBoard] = useState(
|
||||||
|
new Board(props.position, props.playerToMove)
|
||||||
|
);
|
||||||
|
|
||||||
|
const doMove = useCallback((move) => {
|
||||||
|
setBoard((board) => {
|
||||||
|
return board.doMove(move);
|
||||||
|
});
|
||||||
|
setLastMove(move.path.at(-1));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetBoard = useCallback(() => {
|
||||||
|
setBoard(new Board(props.position, props.playerToMove));
|
||||||
|
setLastMove(null);
|
||||||
|
}, [props.position, props.playerToMove]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DraughtsBoardContext.Provider
|
||||||
|
value={{
|
||||||
|
board,
|
||||||
|
doMove,
|
||||||
|
lastMove,
|
||||||
|
resetBoard,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</DraughtsBoardContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DraughtsBoardProviderProps = {
|
||||||
|
playerToMove: PropTypes.oneOf(Object.values(Players)),
|
||||||
|
position: PropTypes.arrayOf(
|
||||||
|
PropTypes.arrayOf(PropTypes.oneOf(Object.values(Pieces)))
|
||||||
|
).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
DraughtsBoardProvider.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
...DraughtsBoardProviderProps,
|
||||||
|
};
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { useDraughtsBoard } from '../DraughtsBoardContext';
|
||||||
|
|
||||||
|
export function useDraughtsPlayerToMove(player) {
|
||||||
|
const { board } = useDraughtsBoard();
|
||||||
|
return player === board.playerToMove;
|
||||||
|
}
|
||||||
17
components/draughts/board/hooks/use-draughts-winner.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useDraughtsBoard } from '../DraughtsBoardContext';
|
||||||
|
import { useDraughtsSettings } from '../../settings/DraughtsSettingsContext';
|
||||||
|
import { Players, GameStates } from '@draughts/core';
|
||||||
|
|
||||||
|
export function useDraughtsWinner() {
|
||||||
|
const { board } = useDraughtsBoard();
|
||||||
|
const { userPlayer } = useDraughtsSettings();
|
||||||
|
|
||||||
|
const winner = useMemo(() => {
|
||||||
|
if (board.state === GameStates.WHITE_WON) return Players.WHITE;
|
||||||
|
if (board.state === GameStates.BLACK_WON) return Players.BLACK;
|
||||||
|
return Players.NONE;
|
||||||
|
}, [board.state]);
|
||||||
|
|
||||||
|
return { userWon: winner === userPlayer, winner };
|
||||||
|
}
|
||||||
65
components/draughts/board/views/DraughtsBoard.jsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
|
import { DndProvider } from 'react-dnd';
|
||||||
|
import { Grid, GridItem } from '@chakra-ui/react';
|
||||||
|
import { TouchBackend } from 'react-dnd-touch-backend';
|
||||||
|
import { useDraughtsBoard } from '../DraughtsBoardContext';
|
||||||
|
import { useDraughtsSettings } from '../../settings/DraughtsSettingsContext';
|
||||||
|
import { DraughtsCell } from './DraughtsCell';
|
||||||
|
import { DraughtsGameOverModal } from './DraughtsGameOverModal';
|
||||||
|
import { Players } from '@draughts/core';
|
||||||
|
|
||||||
|
function isTouchDevice() {
|
||||||
|
return (
|
||||||
|
'ontouchstart' in window ||
|
||||||
|
navigator.maxTouchPoints > 0 ||
|
||||||
|
navigator.msMaxTouchPoints > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DraughtsBoard() {
|
||||||
|
const { board } = useDraughtsBoard();
|
||||||
|
const { userPlayer } = useDraughtsSettings();
|
||||||
|
|
||||||
|
const backend = useMemo(() => {
|
||||||
|
if (typeof window === 'undefined') return HTML5Backend;
|
||||||
|
return isTouchDevice() ? TouchBackend : HTML5Backend;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const rows = useMemo(() => {
|
||||||
|
const entries = board.position.map((row, rowIndex) => ({
|
||||||
|
row,
|
||||||
|
rowIndex,
|
||||||
|
}));
|
||||||
|
if (userPlayer === Players.WHITE) {
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
return entries.reverse();
|
||||||
|
}, [userPlayer, board.position]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndProvider backend={backend}>
|
||||||
|
<DraughtsGameOverModal />
|
||||||
|
<Grid
|
||||||
|
templateRows="repeat(8, 1fr)"
|
||||||
|
templateColumns="repeat(8, 1fr)"
|
||||||
|
w="100%"
|
||||||
|
h="100%"
|
||||||
|
style={{ aspectRatio: 1 }}
|
||||||
|
>
|
||||||
|
{rows.map(({ row, rowIndex }) =>
|
||||||
|
row.map((piece, colIndex) => (
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
<GridItem key={`${rowIndex}:${colIndex}:${piece}`}>
|
||||||
|
<DraughtsCell
|
||||||
|
piece={piece}
|
||||||
|
rowIndex={rowIndex}
|
||||||
|
colIndex={colIndex}
|
||||||
|
/>
|
||||||
|
</GridItem>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</DndProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
78
components/draughts/board/views/DraughtsCell.jsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useDrop } from 'react-dnd';
|
||||||
|
import { Box, Square } from '@chakra-ui/react';
|
||||||
|
import { useDraughtsBoard } from '../DraughtsBoardContext';
|
||||||
|
import { DraughtsPiece } from './DraughtsPiece';
|
||||||
|
import { compareCells, Pieces } from '@draughts/core';
|
||||||
|
|
||||||
|
DraughtsCell.propTypes = {
|
||||||
|
colIndex: PropTypes.number.isRequired,
|
||||||
|
piece: PropTypes.oneOf(Object.values(Pieces)).isRequired,
|
||||||
|
rowIndex: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DraughtsCell(props) {
|
||||||
|
const { board, doMove, lastMove } = useDraughtsBoard();
|
||||||
|
const isDark = (props.rowIndex + props.colIndex) % 2 === 0;
|
||||||
|
const currentCell = { col: props.colIndex, row: props.rowIndex };
|
||||||
|
const isLastMove = compareCells(currentCell, lastMove);
|
||||||
|
|
||||||
|
const validMovePredicate = (start) => (move) => {
|
||||||
|
const startMove = move.path.at(0);
|
||||||
|
const endMove = move.path.at(-1);
|
||||||
|
return compareCells(startMove, start) && compareCells(endMove, currentCell);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [{ isOver, canDrop }, dropReference] = useDrop(
|
||||||
|
() => ({
|
||||||
|
accept: 'piece',
|
||||||
|
canDrop: (start) => board.moves.some(validMovePredicate(start)),
|
||||||
|
collect: (monitor) => ({
|
||||||
|
canDrop: !!monitor.canDrop(),
|
||||||
|
isOver: !!monitor.isOver(),
|
||||||
|
}),
|
||||||
|
drop: (start) => {
|
||||||
|
doMove(board.moves.find(validMovePredicate(start)));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[board.moves, doMove]
|
||||||
|
);
|
||||||
|
|
||||||
|
let bg = 'gray.300';
|
||||||
|
if (canDrop) {
|
||||||
|
bg = 'lightblue';
|
||||||
|
} else if ((canDrop && isOver) || isLastMove) {
|
||||||
|
bg = 'lightgreen';
|
||||||
|
} else if (isDark) {
|
||||||
|
bg = 'gray.500';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Square
|
||||||
|
ref={dropReference}
|
||||||
|
pos="relative"
|
||||||
|
h="100%"
|
||||||
|
p="0.2em"
|
||||||
|
bg={bg}
|
||||||
|
userSelect="none"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
pos="absolute"
|
||||||
|
top="0.1rem"
|
||||||
|
right="0.1rem"
|
||||||
|
fontSize="0.4rem"
|
||||||
|
opacity={0.4}
|
||||||
|
userSelect="none"
|
||||||
|
>
|
||||||
|
{props.rowIndex}, {props.colIndex}
|
||||||
|
</Box>
|
||||||
|
{props.piece !== Pieces.NONE && (
|
||||||
|
<DraughtsPiece
|
||||||
|
piece={props.piece}
|
||||||
|
rowIndex={props.rowIndex}
|
||||||
|
colIndex={props.colIndex}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Square>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
components/draughts/board/views/DraughtsCrown.jsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const { Icon } = require('@chakra-ui/react');
|
||||||
|
|
||||||
|
export function DraughtsCrown(props) {
|
||||||
|
return (
|
||||||
|
<Icon viewBox="0 0 230 200" {...props}>
|
||||||
|
<path
|
||||||
|
d="m9,51 72,21 37-64 37,64 72-21-33,99H42
|
||||||
|
m75-74a15,29 0 1,0 2,0m71,86-11,33H57l-11-33"
|
||||||
|
fill="gold"
|
||||||
|
/>
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
}
|
||||||
82
components/draughts/board/views/DraughtsGameOverModal.jsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
Button,
|
||||||
|
Text,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
useDisclosure,
|
||||||
|
Divider,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useDraughtsBoard } from '../DraughtsBoardContext';
|
||||||
|
import { useDraughtsWinner } from '../hooks/use-draughts-winner';
|
||||||
|
import { useDraughtsGame } from '../../game/DraughtsGameContext';
|
||||||
|
import { useDraughtsSettings } from '../../settings/DraughtsSettingsContext';
|
||||||
|
import { formatPlayer, Players, GameStates } from '@draughts/core';
|
||||||
|
|
||||||
|
function capitalizeFirstLetter(string) {
|
||||||
|
return string.at(0).toUpperCase() + string.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatGameOverMessage(winner) {
|
||||||
|
if (winner === Players.NONE) return 'The game was a draw.';
|
||||||
|
// const winnerFormatted = capitalizeFirstLetter(formatPlayer(winner));
|
||||||
|
// return `${winnerFormatted} won the game.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DraughtsGameOverModal() {
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const { restartGame } = useDraughtsGame();
|
||||||
|
const { board } = useDraughtsBoard();
|
||||||
|
const { settingsModal } = useDraughtsSettings();
|
||||||
|
const { userWon, winner } = useDraughtsWinner();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (board.state === GameStates.PLAYING) return;
|
||||||
|
onOpen();
|
||||||
|
return onClose;
|
||||||
|
}, [board.state, onOpen, onClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>
|
||||||
|
{userWon ? 'Wooo! Anda Menang. 🎉' : 'Semoga lain kali lebih beruntung... 💪'}
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
<Text>{formatGameOverMessage(winner)}</Text>
|
||||||
|
<Divider marginY={5} />
|
||||||
|
<Text>
|
||||||
|
Mengapa bukan kesulitan komputer yang berbeda, Main Lagi Bosku, Anda Masih Kuat, Bertenaga?
|
||||||
|
</Text>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
mr={3}
|
||||||
|
colorScheme="blue"
|
||||||
|
onClick={() => {
|
||||||
|
onClose();
|
||||||
|
settingsModal.onOpen();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Setting Game
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
restartGame();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Main Lagi
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
components/draughts/board/views/DraughtsPiece.jsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { useDrag } from 'react-dnd';
|
||||||
|
import { Center } from '@chakra-ui/react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useDraughtsBoard } from '../DraughtsBoardContext';
|
||||||
|
import { useDraughtsSettings } from '../../settings/DraughtsSettingsContext';
|
||||||
|
import { DraughtsCrown } from './DraughtsCrown';
|
||||||
|
import {
|
||||||
|
compareCells,
|
||||||
|
pieceIsPlayer,
|
||||||
|
pieceIsQueen,
|
||||||
|
Pieces,
|
||||||
|
Players,
|
||||||
|
} from '@draughts/core';
|
||||||
|
|
||||||
|
export function DraughtsPiece(props) {
|
||||||
|
const { board } = useDraughtsBoard();
|
||||||
|
const { userPlayer } = useDraughtsSettings();
|
||||||
|
|
||||||
|
const isWhite = pieceIsPlayer(props.piece, Players.WHITE);
|
||||||
|
const activePlayer =
|
||||||
|
pieceIsPlayer(props.piece, userPlayer) &&
|
||||||
|
pieceIsPlayer(props.piece, board.playerToMove);
|
||||||
|
|
||||||
|
const currentCell = { col: props.colIndex, row: props.rowIndex };
|
||||||
|
|
||||||
|
const canDrag =
|
||||||
|
activePlayer &&
|
||||||
|
board.moves.some((move) => compareCells(move.path.at(0), currentCell));
|
||||||
|
|
||||||
|
const [, dragRef] = useDrag(
|
||||||
|
() => ({
|
||||||
|
canDrag: () => canDrag,
|
||||||
|
isDragging: (monitor) => compareCells(monitor.getItem(), currentCell),
|
||||||
|
item: currentCell,
|
||||||
|
type: 'piece',
|
||||||
|
}),
|
||||||
|
[board, canDrag, currentCell]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Center
|
||||||
|
ref={dragRef}
|
||||||
|
zIndex="1"
|
||||||
|
w="100%"
|
||||||
|
h="100%"
|
||||||
|
opacity={canDrag ? 1 : 0.8}
|
||||||
|
borderWidth="0.2em"
|
||||||
|
borderStyle="solid"
|
||||||
|
borderColor={isWhite ? 'gray.400' : 'gray.600'}
|
||||||
|
borderRadius="50%"
|
||||||
|
bgColor={isWhite ? 'yellow.50' : 'gray.900'}
|
||||||
|
>
|
||||||
|
{pieceIsQueen(props.piece) && (
|
||||||
|
<DraughtsCrown w="70%" h="70%" opacity="0.6" userSelect="none" />
|
||||||
|
)}
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DraughtsPiece.propTypes = {
|
||||||
|
colIndex: PropTypes.number,
|
||||||
|
piece: PropTypes.oneOf(Object.values(Pieces)).isRequired,
|
||||||
|
rowIndex: PropTypes.number,
|
||||||
|
};
|
||||||
37
components/draughts/game/DraughtsGameContext.jsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { createContext, useCallback, useContext } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useDraughtsBoard } from '../board/DraughtsBoardContext';
|
||||||
|
import { useDraughtsTimer } from './timer/use-draughts-timer';
|
||||||
|
import { useDraughtsComputer } from './computer/use-draughts-computer';
|
||||||
|
import { Players } from '@draughts/core';
|
||||||
|
|
||||||
|
export const DraughtsGameContext = createContext();
|
||||||
|
|
||||||
|
export const useDraughtsGame = () => useContext(DraughtsGameContext);
|
||||||
|
|
||||||
|
export function DraughtsGameProvider(props) {
|
||||||
|
const { resetBoard } = useDraughtsBoard();
|
||||||
|
|
||||||
|
const [whiteTimer, resetWhiteTimer] = useDraughtsTimer(Players.WHITE);
|
||||||
|
const [blackTimer, resetBlackTimer] = useDraughtsTimer(Players.BLACK);
|
||||||
|
|
||||||
|
useDraughtsComputer();
|
||||||
|
|
||||||
|
const restartGame = useCallback(() => {
|
||||||
|
resetBoard();
|
||||||
|
resetWhiteTimer();
|
||||||
|
resetBlackTimer();
|
||||||
|
}, [resetBoard, resetWhiteTimer, resetBlackTimer]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DraughtsGameContext.Provider
|
||||||
|
value={{ blackTimer, restartGame, whiteTimer }}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</DraughtsGameContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DraughtsGameProvider.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
};
|
||||||
31
components/draughts/game/computer/use-draughts-computer.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { useDraughtsSettings } from '../../settings/DraughtsSettingsContext';
|
||||||
|
import { useDraughtsBoard } from '../../board/DraughtsBoardContext';
|
||||||
|
import { GameStates } from '@draughts/core';
|
||||||
|
|
||||||
|
export function useDraughtsComputer() {
|
||||||
|
const { board, doMove } = useDraughtsBoard();
|
||||||
|
const { userPlayer, computerDifficulty } = useDraughtsSettings();
|
||||||
|
|
||||||
|
const workerRef = useRef();
|
||||||
|
useEffect(() => {
|
||||||
|
if (board.state !== GameStates.PLAYING) return;
|
||||||
|
if (board.playerToMove === userPlayer) return;
|
||||||
|
|
||||||
|
workerRef.current = new Worker(new URL('worker.js', import.meta.url));
|
||||||
|
|
||||||
|
const handleWorkerMessage = (event) => {
|
||||||
|
doMove(event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
workerRef.current.addEventListener('message', handleWorkerMessage);
|
||||||
|
workerRef.current.postMessage({ board, computerDifficulty });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
workerRef.current.removeEventListener('message', handleWorkerMessage);
|
||||||
|
workerRef.current.terminate();
|
||||||
|
};
|
||||||
|
}, [board, userPlayer, computerDifficulty, doMove]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
40
components/draughts/game/computer/worker.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { ComputerDifficulty } from '../../settings/constants/computer-difficulty';
|
||||||
|
import { alphaBetaMove } from '@draughts/computer';
|
||||||
|
import { Board } from '@draughts/core';
|
||||||
|
|
||||||
|
const getRandomMove = (board) => {
|
||||||
|
return board.moves.at(Math.floor(Math.random() * board.moves.length));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getComputerMove = (board, difficulty) => {
|
||||||
|
if (difficulty === ComputerDifficulty.MEDIUM) {
|
||||||
|
return alphaBetaMove(board, 3);
|
||||||
|
}
|
||||||
|
if (difficulty === ComputerDifficulty.HARD) {
|
||||||
|
return alphaBetaMove(board, 6);
|
||||||
|
}
|
||||||
|
return getRandomMove(board);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getComputerTimeout = (board) => {
|
||||||
|
return 200 + board.moves.length * Math.floor(Math.random() * 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventListener('message', (event) => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const board = new Board(
|
||||||
|
event.data.board.position,
|
||||||
|
event.data.board.playerToMove,
|
||||||
|
event.data.board.firstMove
|
||||||
|
);
|
||||||
|
const move = getComputerMove(board, event.data.computerDifficulty);
|
||||||
|
|
||||||
|
const endTime = Date.now();
|
||||||
|
const elapsedTime = startTime - endTime;
|
||||||
|
const waitTime = Math.max(0, getComputerTimeout(board) - elapsedTime);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
postMessage(move);
|
||||||
|
}, waitTime);
|
||||||
|
});
|
||||||
43
components/draughts/game/timer/use-draughts-timer.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { useDraughtsBoard } from '../../board/DraughtsBoardContext';
|
||||||
|
import { GameStates } from '@draughts/core';
|
||||||
|
|
||||||
|
const TIMER_TICK = 100;
|
||||||
|
const INITIAL_TIME = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
export function useDraughtsTimer(player) {
|
||||||
|
const { board } = useDraughtsBoard();
|
||||||
|
const [timer, setTimer] = useState(INITIAL_TIME);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (board.state !== GameStates.PLAYING) return;
|
||||||
|
if (board.firstMove) return;
|
||||||
|
if (board.playerToMove !== player) return;
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setTimer((timer) => {
|
||||||
|
const updatedTime = timer - TIMER_TICK;
|
||||||
|
return updatedTime >= 0 ? updatedTime : 0;
|
||||||
|
});
|
||||||
|
}, TIMER_TICK);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
setTimer((timer) => timer + 1000);
|
||||||
|
};
|
||||||
|
}, [player, board, setTimer]);
|
||||||
|
|
||||||
|
const timerInfo = useMemo(() => {
|
||||||
|
const totalSeconds = Math.floor(timer / 1000);
|
||||||
|
return {
|
||||||
|
complete: timer === 0,
|
||||||
|
millis: timer % 1000,
|
||||||
|
minutes: Math.floor(totalSeconds / 60),
|
||||||
|
seconds: totalSeconds % 60,
|
||||||
|
};
|
||||||
|
}, [timer]);
|
||||||
|
|
||||||
|
const resetTimer = () => setTimer(INITIAL_TIME);
|
||||||
|
|
||||||
|
return [timerInfo, resetTimer];
|
||||||
|
}
|
||||||
41
components/draughts/settings/DraughtsSettingsContext.jsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { createContext, useContext, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useDisclosure } from '@chakra-ui/react';
|
||||||
|
import { ComputerDifficulty } from './constants/computer-difficulty';
|
||||||
|
import { Players } from '@draughts/core';
|
||||||
|
|
||||||
|
export const DraughtsSettingsContext = createContext();
|
||||||
|
|
||||||
|
export const useDraughtsSettings = () => useContext(DraughtsSettingsContext);
|
||||||
|
|
||||||
|
export function DraughtsSettingsProvider(props) {
|
||||||
|
const settingsModal = useDisclosure();
|
||||||
|
|
||||||
|
const [userPlayer, setUserPlayer] = useState(props.userPlayer);
|
||||||
|
const [computerDifficulty, setComputerDifficulty] = useState(
|
||||||
|
props.computerDifficulty
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateSettings = (newSettings) => {
|
||||||
|
setUserPlayer(newSettings.userPlayer);
|
||||||
|
setComputerDifficulty(newSettings.computerDifficulty);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DraughtsSettingsContext.Provider
|
||||||
|
value={{ computerDifficulty, settingsModal, updateSettings, userPlayer }}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</DraughtsSettingsContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DraughtsSettingsProviderProps = {
|
||||||
|
computerDifficulty: PropTypes.oneOf(Object.values(ComputerDifficulty)),
|
||||||
|
userPlayer: PropTypes.oneOf(Object.values(Players)),
|
||||||
|
};
|
||||||
|
|
||||||
|
DraughtsSettingsProvider.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
...DraughtsSettingsProviderProps,
|
||||||
|
};
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
export const ComputerDifficulty = {
|
||||||
|
EASY: 'e',
|
||||||
|
HARD: 'h',
|
||||||
|
MEDIUM: 'm',
|
||||||
|
};
|
||||||
30
components/draughts/settings/views/DraughtsMenu.jsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { SettingsIcon, RepeatIcon } from '@chakra-ui/icons';
|
||||||
|
import { Button, VStack } from '@chakra-ui/react';
|
||||||
|
import { useDraughtsGame } from '../../game/DraughtsGameContext';
|
||||||
|
import { useDraughtsSettings } from '../DraughtsSettingsContext';
|
||||||
|
import { DraughtsSettingsModal } from './DraughtsSettingsModal';
|
||||||
|
|
||||||
|
export function DraughtsMenuView() {
|
||||||
|
const { restartGame } = useDraughtsGame();
|
||||||
|
const { settingsModal } = useDraughtsSettings();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack>
|
||||||
|
<DraughtsSettingsModal />
|
||||||
|
<Button
|
||||||
|
onClick={() => settingsModal.onOpen()}
|
||||||
|
rightIcon={<SettingsIcon />}
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
Buka Setting
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => restartGame()}
|
||||||
|
rightIcon={<RepeatIcon />}
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
Mulai Lagi
|
||||||
|
</Button>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
94
components/draughts/settings/views/DraughtsSettingsModal.jsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
Button,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
HStack,
|
||||||
|
Select,
|
||||||
|
Divider,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useDraughtsGame } from '../../game/DraughtsGameContext';
|
||||||
|
import { useDraughtsSettings } from '../DraughtsSettingsContext';
|
||||||
|
import { ComputerDifficulty } from '../constants/computer-difficulty';
|
||||||
|
import { Players } from '@draughts/core';
|
||||||
|
|
||||||
|
export function DraughtsSettingsModal() {
|
||||||
|
const { restartGame } = useDraughtsGame();
|
||||||
|
const {
|
||||||
|
userPlayer,
|
||||||
|
updateSettings,
|
||||||
|
computerDifficulty,
|
||||||
|
settingsModal: { isOpen, onClose },
|
||||||
|
} = useDraughtsSettings();
|
||||||
|
|
||||||
|
const [computerDifficultySelection, setComputerDifficultySelection] =
|
||||||
|
useState(computerDifficulty);
|
||||||
|
const [userPlayerSelection, setUserPlayerSelection] = useState(userPlayer);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>Settings</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
<FormControl as="fieldset">
|
||||||
|
<FormLabel as="legend">Pilih Warna</FormLabel>
|
||||||
|
<RadioGroup
|
||||||
|
onChange={setUserPlayerSelection}
|
||||||
|
value={userPlayerSelection}
|
||||||
|
>
|
||||||
|
<HStack>
|
||||||
|
<Radio value={`${Players.WHITE}`}>Putih</Radio>
|
||||||
|
<Radio value={`${Players.BLACK}`}>Hitam</Radio>
|
||||||
|
</HStack>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
<Divider marginY={5} />
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel htmlFor="computerDifficulty">
|
||||||
|
Computer difficulty
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
id="computerDifficulty"
|
||||||
|
onChange={(event) => {
|
||||||
|
setComputerDifficultySelection(event.target.value);
|
||||||
|
}}
|
||||||
|
value={computerDifficultySelection}
|
||||||
|
>
|
||||||
|
<option value={ComputerDifficulty.EASY}>Easy</option>
|
||||||
|
<option value={ComputerDifficulty.MEDIUM}>Medium</option>
|
||||||
|
<option value={ComputerDifficulty.HARD}>Hard</option>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button mr={3} colorScheme="blue" onClick={onClose}>
|
||||||
|
Kembali Ke Permainan
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
restartGame();
|
||||||
|
updateSettings({
|
||||||
|
computerDifficulty: computerDifficultySelection,
|
||||||
|
userPlayer: userPlayerSelection,
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Mulai Permainan Baru
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
components/header/Header.jsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { HStack } from '@chakra-ui/react';
|
||||||
|
import { Logo } from './Logo';
|
||||||
|
import { Navigation } from './Navigation';
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
return (
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<Logo />
|
||||||
|
<Navigation />
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
components/header/Logo.jsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import NextLink from 'next/link';
|
||||||
|
import { Heading, HStack } from '@chakra-ui/react';
|
||||||
|
import { DraughtsCrown } from '../draughts/board/views/DraughtsCrown';
|
||||||
|
|
||||||
|
export function Logo() {
|
||||||
|
return (
|
||||||
|
<NextLink href="/" passHref>
|
||||||
|
<HStack as="a">
|
||||||
|
<DraughtsCrown
|
||||||
|
bg="black"
|
||||||
|
padding={1}
|
||||||
|
borderRadius="50%"
|
||||||
|
height="1.5em"
|
||||||
|
width="1.5em"
|
||||||
|
/>
|
||||||
|
<Heading fontSize="2xl">Givan Checkers</Heading>
|
||||||
|
</HStack>
|
||||||
|
</NextLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
77
components/header/Navigation.jsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { useRef } from 'react';
|
||||||
|
import {
|
||||||
|
HStack,
|
||||||
|
VStack,
|
||||||
|
IconButton,
|
||||||
|
Link,
|
||||||
|
useDisclosure,
|
||||||
|
Drawer,
|
||||||
|
DrawerBody,
|
||||||
|
DrawerOverlay,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerCloseButton,
|
||||||
|
DrawerHeader,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { HamburgerIcon } from '@chakra-ui/icons';
|
||||||
|
|
||||||
|
const pages = [
|
||||||
|
{'url':'rules','name' : 'Peraturan'},
|
||||||
|
{'url':'strategies','name': 'Strategi'},
|
||||||
|
{'url':'history','name':'Sejarah'}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Navigation() {
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const btnRef = useRef();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HStack as="nav" display={['none', 'none', 'block']} spacing={[2, 2, 4]}>
|
||||||
|
{pages.map((page) => (
|
||||||
|
<Link
|
||||||
|
key={`navlink-desktop-${page.url}`}
|
||||||
|
fontSize="lg"
|
||||||
|
fontWeight="bold"
|
||||||
|
href={`/${page.url}`}
|
||||||
|
>
|
||||||
|
{page.name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
<IconButton
|
||||||
|
ref={btnRef}
|
||||||
|
display={['block', 'block', 'none']}
|
||||||
|
aria-label="Open Menu"
|
||||||
|
icon={<HamburgerIcon />}
|
||||||
|
onClick={onOpen}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Drawer
|
||||||
|
finalFocusRef={btnRef}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
placement="right"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
<DrawerOverlay />
|
||||||
|
<DrawerContent>
|
||||||
|
<DrawerCloseButton />
|
||||||
|
<DrawerHeader>Explore</DrawerHeader>
|
||||||
|
<DrawerBody>
|
||||||
|
<VStack as="nav">
|
||||||
|
{pages.map((page) => (
|
||||||
|
<Link
|
||||||
|
key={`navlink-mobile-${page}`}
|
||||||
|
fontSize="lg"
|
||||||
|
fontWeight="bold"
|
||||||
|
href={`/${page}`}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</DrawerBody>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
components/layout/MainLayout.jsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { Grid, GridItem } from '@chakra-ui/react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Header } from '../header/Header';
|
||||||
|
import { LeftSidebarAdvert } from '../adverts/LeftSidebarAdvert';
|
||||||
|
import { RightSidebarAdvert } from '../adverts/RightSidebarAdvert';
|
||||||
|
import { TopBannerAdvert } from '../adverts/TopBannerAdvert';
|
||||||
|
import { BottomBannerAdvert } from '../adverts/BottomBannerAdvert';
|
||||||
|
|
||||||
|
export function MainLayout(props) {
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
as="main"
|
||||||
|
rowGap={[3, 4]}
|
||||||
|
columnGap={[3, 4, 5, 6]}
|
||||||
|
templateRows={['0 auto auto auto auto 0', '0 auto auto 0']}
|
||||||
|
templateColumns={[
|
||||||
|
'0 minmax(0, 1fr) 0',
|
||||||
|
'0 minmax(4em, 1fr) min(78vh, 78vw) 0',
|
||||||
|
'0 minmax(4em, 1fr) min(78vh, 78vw) minmax(4em, 1fr) 0',
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<GridItem colSpan={1} colStart={[2, 3, 3]} rowStart={2}>
|
||||||
|
<Header />
|
||||||
|
</GridItem>
|
||||||
|
<GridItem colSpan={1} colStart={[2, 3]} rowStart={[4, 3]}>
|
||||||
|
{props.children}
|
||||||
|
</GridItem>
|
||||||
|
<GridItem
|
||||||
|
display={['block', 'none']}
|
||||||
|
colSpan={2}
|
||||||
|
colStart={2}
|
||||||
|
rowStart={3}
|
||||||
|
>
|
||||||
|
<TopBannerAdvert />
|
||||||
|
</GridItem>
|
||||||
|
<GridItem
|
||||||
|
display={['block', 'none']}
|
||||||
|
colSpan={2}
|
||||||
|
colStart={2}
|
||||||
|
rowStart={6}
|
||||||
|
>
|
||||||
|
<BottomBannerAdvert />
|
||||||
|
</GridItem>
|
||||||
|
<GridItem
|
||||||
|
display={['none', 'block']}
|
||||||
|
colStart={2}
|
||||||
|
rowSpan={[2, 3, 4]}
|
||||||
|
rowStart={[3, 3, 2]}
|
||||||
|
>
|
||||||
|
<LeftSidebarAdvert />
|
||||||
|
</GridItem>
|
||||||
|
<GridItem
|
||||||
|
display={['none', 'none', 'block']}
|
||||||
|
colStart={5}
|
||||||
|
rowSpan={[2, 2, 4]}
|
||||||
|
rowStart={[3, 3, 2]}
|
||||||
|
>
|
||||||
|
<RightSidebarAdvert />
|
||||||
|
</GridItem>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MainLayout.propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
10
jsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"include": ["."],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@draughts/*": ["packages/draughts/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
48
next.config.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* @type {import('next').NextConfig}
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
images: {
|
||||||
|
formats: ['image/avif', 'image/webp'],
|
||||||
|
},
|
||||||
|
reactStrictMode: true,
|
||||||
|
async redirects() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
destination: '/',
|
||||||
|
permanent: true,
|
||||||
|
source: '/index.html',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
destination: '/',
|
||||||
|
permanent: true,
|
||||||
|
source: '/games_list.html',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
destination: '/rules',
|
||||||
|
permanent: true,
|
||||||
|
source: '/the_rules_of_draughts.html',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
destination: '/strategies',
|
||||||
|
permanent: true,
|
||||||
|
source: '/strategies_for_draughts.html',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
destination: '/history',
|
||||||
|
permanent: true,
|
||||||
|
source: '/the_evolution_of_draughts.html',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
destination: '/history',
|
||||||
|
permanent: true,
|
||||||
|
source: '/variations_for_the_game_of_draughts.html',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
destination: '/history',
|
||||||
|
permanent: true,
|
||||||
|
source: '/the_history_of_draughts.html',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
9799
package-lock.json
generated
Normal file
40
package.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "draughts-web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@chakra-ui/icons": "^2.0.2",
|
||||||
|
"@chakra-ui/react": "^2.2.1",
|
||||||
|
"@emotion/react": "^11.9.0",
|
||||||
|
"@emotion/styled": "^11.8.1",
|
||||||
|
"framer-motion": "^6.3.11",
|
||||||
|
"next": "^12.1.6",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react": "^18.1.0",
|
||||||
|
"react-dnd": "^16.0.1",
|
||||||
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
|
"react-dnd-touch-backend": "^16.0.1",
|
||||||
|
"react-dom": "^18.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^8.17.0",
|
||||||
|
"eslint-config-next": "^12.1.6",
|
||||||
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
|
"eslint-import-resolver-node": "^0.3.6",
|
||||||
|
"eslint-plugin-chakra-ui": "^0.6.4",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"eslint-plugin-react": "^7.30.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.5.0",
|
||||||
|
"eslint-plugin-sonarjs": "^0.13.0",
|
||||||
|
"eslint-plugin-sort-keys-fix": "^1.1.2",
|
||||||
|
"eslint-plugin-unicorn": "^42.0.0",
|
||||||
|
"prettier": "^2.6.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
46
packages/draughts/computer/alpha-beta-search.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { quiescenceSearch } from './quiescence-search';
|
||||||
|
import { GameStates } from '@draughts/core';
|
||||||
|
|
||||||
|
const getShuffledArray = (arr) => {
|
||||||
|
const newArr = [...arr];
|
||||||
|
for (let i = newArr.length - 1; i > 0; i--) {
|
||||||
|
const rand = Math.floor(Math.random() * (i + 1));
|
||||||
|
[newArr[i], newArr[rand]] = [newArr[rand], newArr[i]];
|
||||||
|
}
|
||||||
|
return newArr;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function alphaBetaMove(board, depth) {
|
||||||
|
let recordE = Number.NEGATIVE_INFINITY;
|
||||||
|
let recordMove = null;
|
||||||
|
|
||||||
|
for (const move of getShuffledArray(board.moves)) {
|
||||||
|
const nextBoard = board.doMove(move);
|
||||||
|
const e = -alphaBetaSearch(
|
||||||
|
nextBoard,
|
||||||
|
depth - 1,
|
||||||
|
Number.NEGATIVE_INFINITY,
|
||||||
|
Number.POSITIVE_INFINITY
|
||||||
|
);
|
||||||
|
if (e >= recordE) {
|
||||||
|
recordE = e;
|
||||||
|
recordMove = move;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return recordMove;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function alphaBetaSearch(board, depth, alpha, beta) {
|
||||||
|
if (depth === 0 || board.state !== GameStates.PLAYING)
|
||||||
|
return quiescenceSearch(board, alpha, beta);
|
||||||
|
|
||||||
|
for (const move of board.moves) {
|
||||||
|
const nextBoard = board.doMove(move);
|
||||||
|
const e = -alphaBetaSearch(nextBoard, depth - 1, -beta, -alpha);
|
||||||
|
if (e >= beta) return beta;
|
||||||
|
alpha = Math.max(e, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
return alpha;
|
||||||
|
}
|
||||||
101
packages/draughts/computer/evaluate-board.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import {
|
||||||
|
eachCell,
|
||||||
|
pieceIsPlayer,
|
||||||
|
pieceIsQueen,
|
||||||
|
BOARD_SIZE,
|
||||||
|
Players,
|
||||||
|
GameStates,
|
||||||
|
} from '@draughts/core';
|
||||||
|
|
||||||
|
const winnerMap = {
|
||||||
|
[GameStates.WHITE_WON]: Players.WHITE,
|
||||||
|
[GameStates.BLACK_WON]: Players.BLACK,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function evaluateBoard(board) {
|
||||||
|
if (board.state === GameStates.DRAW) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (board.state !== GameStates.PLAYING) {
|
||||||
|
const winner = winnerMap[board.state];
|
||||||
|
return board.playerToMove === winner
|
||||||
|
? Number.POSITIVE_INFINITY
|
||||||
|
: Number.NEGATIVE_INFINITY;
|
||||||
|
}
|
||||||
|
if (isEndgame(board)) {
|
||||||
|
return evaluateEndgamePosition(board);
|
||||||
|
}
|
||||||
|
return evaluateRegularPosition(board);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEndgame(board) {
|
||||||
|
for (const { piece } of eachCell(board.position)) {
|
||||||
|
if (!pieceIsPlayer(piece, Players.NONE) && !pieceIsQueen(piece)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateEndgamePosition(board) {
|
||||||
|
let playerPieces = 0;
|
||||||
|
let opponentPieces = 0;
|
||||||
|
let distances = 0;
|
||||||
|
for (const { cell, piece } of eachCell(board.position)) {
|
||||||
|
if (pieceIsPlayer(piece, board.playerToMove)) {
|
||||||
|
playerPieces += 1;
|
||||||
|
distances += calculateDistances(board, cell);
|
||||||
|
} else if (!pieceIsPlayer(piece, Players.NONE)) {
|
||||||
|
opponentPieces += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (playerPieces >= opponentPieces) {
|
||||||
|
return -distances;
|
||||||
|
}
|
||||||
|
return distances;
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateRegularPosition(board) {
|
||||||
|
let e = 0;
|
||||||
|
for (const { cell, piece } of eachCell(board.position)) {
|
||||||
|
const pieceEvaluation = evaluatePiece(cell, piece);
|
||||||
|
if (pieceIsPlayer(piece, board.playerToMove)) {
|
||||||
|
e += pieceEvaluation;
|
||||||
|
} else if (!pieceIsPlayer(piece, Players.NONE)) {
|
||||||
|
e -= pieceEvaluation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluatePiece(cell, piece) {
|
||||||
|
let e = 20;
|
||||||
|
if (pieceIsQueen(piece)) {
|
||||||
|
e += 40;
|
||||||
|
} else if (pieceIsPlayer(piece, Players.WHITE)) {
|
||||||
|
e += BOARD_SIZE - 1 - cell.row;
|
||||||
|
} else if (pieceIsPlayer(piece, Players.BLACK)) {
|
||||||
|
e += cell.row;
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateDistances(board, baseCell) {
|
||||||
|
let distances = 0;
|
||||||
|
for (const { cell, piece } of eachCell(board.position)) {
|
||||||
|
if (
|
||||||
|
!pieceIsPlayer(piece, board.playerToMove) &&
|
||||||
|
!pieceIsPlayer(piece, Players.NONE)
|
||||||
|
) {
|
||||||
|
distances += euclideanDistance(baseCell, cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return distances;
|
||||||
|
}
|
||||||
|
|
||||||
|
function euclideanDistance(a, b) {
|
||||||
|
const rowSquare = Math.pow(a.row - b.row, 2);
|
||||||
|
const colSquare = Math.pow(a.col - b.col, 2);
|
||||||
|
const distanceFloat = Math.sqrt(rowSquare + colSquare);
|
||||||
|
return Math.ceil(distanceFloat);
|
||||||
|
}
|
||||||
1
packages/draughts/computer/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { alphaBetaMove } from './alpha-beta-search';
|
||||||
543
packages/draughts/computer/negascout-result.js
Normal file
@ -0,0 +1,543 @@
|
|||||||
|
import {
|
||||||
|
eachCell,
|
||||||
|
pieceIsPlayer,
|
||||||
|
pieceIsQueen,
|
||||||
|
BOARD_SIZE,
|
||||||
|
Players,
|
||||||
|
GameStates,
|
||||||
|
} from '@draughts/core';
|
||||||
|
|
||||||
|
const winnerMap = {
|
||||||
|
[GameStates.WHITE_WON]: Players.WHITE,
|
||||||
|
[GameStates.BLACK_WON]: Players.BLACK,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function negascout_search(board) {
|
||||||
|
if (board.state === GameStates.DRAW) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (board.state !== GameStates.PLAYING) {
|
||||||
|
const winner = winnerMap[board.state];
|
||||||
|
return board.playerToMove === winner
|
||||||
|
? Number.POSITIVE_INFINITY
|
||||||
|
: Number.NEGATIVE_INFINITY;
|
||||||
|
}
|
||||||
|
if (isEndgame(board)) {
|
||||||
|
return evaluateEndgamePosition(board);
|
||||||
|
}
|
||||||
|
return evaluateRegularPosition(board);
|
||||||
|
}
|
||||||
|
|
||||||
|
function eval_board(Board, pieceType, restrictions) {
|
||||||
|
let score = 0;
|
||||||
|
const min_r = restrictions[0];
|
||||||
|
const min_c = restrictions[1];
|
||||||
|
const max_r = restrictions[2];
|
||||||
|
const max_c = restrictions[3];
|
||||||
|
for (let row = min_r; row < max_r + 1; row++) {
|
||||||
|
for (let column = min_c; column < max_c + 1; column++) {
|
||||||
|
if (Board[row][column] == pieceType) {
|
||||||
|
let block = 0;
|
||||||
|
let piece = 1;
|
||||||
|
// left
|
||||||
|
if (column === 0 || Board[row][column - 1] !== 0) {
|
||||||
|
block++;
|
||||||
|
}
|
||||||
|
// pieceNum
|
||||||
|
for (column++; column < Columns && Board[row][column] === pieceType; column++) {
|
||||||
|
piece++;
|
||||||
|
}
|
||||||
|
// right
|
||||||
|
if (column === Columns || Board[row][column] !== 0) {
|
||||||
|
block++;
|
||||||
|
}
|
||||||
|
score += evaluateblock(block, piece);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let column = min_c; column < max_c + 1; column++) {
|
||||||
|
for (let row = min_r; row < max_r + 1; row++) {
|
||||||
|
if (Board[row][column] == pieceType) {
|
||||||
|
let block = 0;
|
||||||
|
let piece = 1;
|
||||||
|
// left
|
||||||
|
if (row === 0 || Board[row - 1][column] !== 0) {
|
||||||
|
block++;
|
||||||
|
}
|
||||||
|
// pieceNum
|
||||||
|
for (row++; row < Rows && Board[row][column] === pieceType; row++) {
|
||||||
|
piece++;
|
||||||
|
}
|
||||||
|
// right
|
||||||
|
if (row === Rows || Board[row][column] !== 0) {
|
||||||
|
block++;
|
||||||
|
}
|
||||||
|
score += evaluateblock(block, piece);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let n = min_r; n < (max_c - min_c + max_r); n += 1) {
|
||||||
|
let r = n;
|
||||||
|
let c = min_c;
|
||||||
|
while (r >= min_r && c <= max_c) {
|
||||||
|
if (r <= max_r) {
|
||||||
|
if (Board[r][c] === pieceType) {
|
||||||
|
let block = 0;
|
||||||
|
let piece = 1;
|
||||||
|
// left
|
||||||
|
if (c === 0 || r === Rows - 1 || Board[r + 1][c - 1] !== 0) {
|
||||||
|
block++;
|
||||||
|
}
|
||||||
|
// pieceNum
|
||||||
|
r--;
|
||||||
|
c++;
|
||||||
|
for (; r >= 0 && Board[r][c] === pieceType; r--) {
|
||||||
|
piece++;
|
||||||
|
c++
|
||||||
|
}
|
||||||
|
// right
|
||||||
|
if (r < 0 || c === Columns || Board[r][c] !== 0) {
|
||||||
|
block++;
|
||||||
|
}
|
||||||
|
score += evaluateblock(block, piece);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r -= 1;
|
||||||
|
c += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let n = min_r - (max_c - min_c); n <= max_r; n++) {
|
||||||
|
let r = n;
|
||||||
|
let c = min_c;
|
||||||
|
while (r <= max_r && c <= max_c) {
|
||||||
|
if (r >= min_r && r <= max_r) {
|
||||||
|
if (Board[r][c] === pieceType) {
|
||||||
|
let block = 0;
|
||||||
|
let piece = 1;
|
||||||
|
// left
|
||||||
|
if (c === 0 || r === 0 || Board[r - 1][c - 1] !== 0) {
|
||||||
|
block++;
|
||||||
|
}
|
||||||
|
// pieceNum
|
||||||
|
r++;
|
||||||
|
c++;
|
||||||
|
for (; r < Rows && Board[r][c] == pieceType; r++) {
|
||||||
|
piece++;
|
||||||
|
c++;
|
||||||
|
}
|
||||||
|
// right
|
||||||
|
if (r === Rows || c === Columns || Board[r][c] !== 0) {
|
||||||
|
block++;
|
||||||
|
}
|
||||||
|
score += evaluateblock(block, piece);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r += 1;
|
||||||
|
c += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateblock(blocks, pieces) {
|
||||||
|
if (blocks === 0) {
|
||||||
|
switch (pieces) {
|
||||||
|
case 1:
|
||||||
|
return LiveOne;
|
||||||
|
case 2:
|
||||||
|
return LiveTwo;
|
||||||
|
case 3:
|
||||||
|
return LiveThree;
|
||||||
|
case 4:
|
||||||
|
return LiveFour;
|
||||||
|
default:
|
||||||
|
return Five;
|
||||||
|
}
|
||||||
|
} else if (blocks === 1) {
|
||||||
|
switch (pieces) {
|
||||||
|
case 1:
|
||||||
|
return DeadOne;
|
||||||
|
case 2:
|
||||||
|
return DeadTwo;
|
||||||
|
case 3:
|
||||||
|
return DeadThree;
|
||||||
|
case 4:
|
||||||
|
return DeadFour;
|
||||||
|
default:
|
||||||
|
return Five;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (pieces >= 5) {
|
||||||
|
return Five;
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_directions(arr) {
|
||||||
|
for (let i = 0; i < arr.length - 4; i++) {
|
||||||
|
if (arr[i] !== 0) {
|
||||||
|
if (arr[i] === arr[i + 1] && arr[i] === arr[i + 2] && arr[i] === arr[i + 3] && arr[i] === arr[i + 4]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_directions(Board, x, y) {
|
||||||
|
const Directions = [[],[],[],[]];
|
||||||
|
for (let i = -4; i < 5; i++) {
|
||||||
|
if (x + i >= 0 && x + i <= Rows - 1) {
|
||||||
|
Directions[0].push(Board[x + i][y])
|
||||||
|
if (y + i >= 0 && y + i <= Columns - 1) {
|
||||||
|
Directions[2].push(Board[x + i][y + i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (y + i >= 0 && y + i <= Columns - 1) {
|
||||||
|
Directions[1].push(Board[x][y + i])
|
||||||
|
if (x - i >= 0 && x - i <= Rows - 1) {
|
||||||
|
Directions[3].push(Board[x - i][y + i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return Directions
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkwin(Board, x, y) {
|
||||||
|
const Directions = get_directions(Board, x, y)
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
if (check_directions(Directions[i])) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function remoteCell(Board, r, c) {
|
||||||
|
for (let i = r - 2; i <= r + 2; i++) {
|
||||||
|
if (i < 0 || i >= Rows) continue;
|
||||||
|
for (let j = c - 2; j <= c + 2; j++) {
|
||||||
|
if (j < 0 || j >= Columns) continue;
|
||||||
|
if (Board[i][j] !== 0) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get_restrictions(Board) {
|
||||||
|
let min_r = Infinity;
|
||||||
|
let min_c = Infinity;
|
||||||
|
let max_r = -Infinity;
|
||||||
|
let max_c = -Infinity;
|
||||||
|
for (let i = 0; i < Rows; i++) {
|
||||||
|
for (let j = 0; j < Columns; j++) {
|
||||||
|
if (Board[i][j] !== 0) {
|
||||||
|
min_r = Math.min(min_r, i)
|
||||||
|
min_c = Math.min(min_c, j)
|
||||||
|
max_r = Math.max(max_r, i)
|
||||||
|
max_c = Math.max(max_c, j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (min_r - 2 < 0) {
|
||||||
|
min_r = 2;
|
||||||
|
}
|
||||||
|
if (min_c - 2 < 0) {
|
||||||
|
min_c = 2;
|
||||||
|
}
|
||||||
|
if (max_r + 2 >= Rows) {
|
||||||
|
max_r = Rows - 3;
|
||||||
|
}
|
||||||
|
if (max_c + 2 >= Columns) {
|
||||||
|
max_c = Columns - 3;
|
||||||
|
}
|
||||||
|
return [min_r, min_c, max_r, max_c]
|
||||||
|
}
|
||||||
|
|
||||||
|
function Change_restrictions(restrictions, i, j) {
|
||||||
|
let min_r = restrictions[0];
|
||||||
|
let min_c = restrictions[1];
|
||||||
|
let max_r = restrictions[2];
|
||||||
|
let max_c = restrictions[3];
|
||||||
|
if (i < min_r) {
|
||||||
|
min_r = i
|
||||||
|
} else if (i > max_r) {
|
||||||
|
max_r = i
|
||||||
|
}
|
||||||
|
if (j < min_c) {
|
||||||
|
min_c = j
|
||||||
|
} else if (j > max_c) {
|
||||||
|
max_c = j
|
||||||
|
}
|
||||||
|
if (min_r - 2 < 0) {
|
||||||
|
min_r = 2;
|
||||||
|
}
|
||||||
|
if (min_c - 2 < 0) {
|
||||||
|
min_c = 2;
|
||||||
|
}
|
||||||
|
if (max_r + 2 >= Rows) {
|
||||||
|
max_r = Rows - 3;
|
||||||
|
}
|
||||||
|
if (max_c + 2 >= Columns) {
|
||||||
|
max_c = Columns - 3;
|
||||||
|
}
|
||||||
|
return [min_r, min_c, max_r, max_c]
|
||||||
|
}
|
||||||
|
|
||||||
|
function compare(a, b) {
|
||||||
|
if (a.score < b.score)
|
||||||
|
return 1;
|
||||||
|
if (a.score > b.score)
|
||||||
|
return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BoardGenerator(restrictions, Board, player) {
|
||||||
|
const availSpots_score = []; //c is j r is i;
|
||||||
|
const min_r = restrictions[0];
|
||||||
|
const min_c = restrictions[1];
|
||||||
|
const max_r = restrictions[2];
|
||||||
|
const max_c = restrictions[3];;
|
||||||
|
for (let i = min_r - 2; i <= max_r + 2; i++) {
|
||||||
|
for (let j = min_c - 2; j <= max_c + 2; j++) {
|
||||||
|
if (Board[i][j] === 0 && !remoteCell(Board, i, j)) {
|
||||||
|
const move = {}
|
||||||
|
move.i = i;
|
||||||
|
move.j = j;
|
||||||
|
move.score = evaluate_move(Board, i, j, player)
|
||||||
|
if (move.score === WIN_DETECTED) {
|
||||||
|
return [move]
|
||||||
|
}
|
||||||
|
availSpots_score.push(move)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
availSpots_score.sort(compare);
|
||||||
|
// return availSpots_score.slice(0,20)
|
||||||
|
return availSpots_score;
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluate_direction(direction_arr, player) {
|
||||||
|
let score = 0;
|
||||||
|
for (let i = 0;(i + 4) < direction_arr.length; i++) {
|
||||||
|
let you = 0;
|
||||||
|
let enemy = 0;
|
||||||
|
for (let j = 0; j <= 4; j++) {
|
||||||
|
if (direction_arr[i + j] === player) {
|
||||||
|
you++
|
||||||
|
} else if (direction_arr[i + j] === -player) {
|
||||||
|
enemy++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
score += evalff(get_seq(you, enemy));
|
||||||
|
if ((score >= 800000)) {
|
||||||
|
return WIN_DETECTED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_seq(y, e) {
|
||||||
|
if (y + e === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (y !== 0 && e === 0) {
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
if (y === 0 && e !== 0) {
|
||||||
|
return -e
|
||||||
|
}
|
||||||
|
if (y !== 0 && e !== 0) {
|
||||||
|
return 17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluate_move(Board, x, y, player) {
|
||||||
|
let score = 0;
|
||||||
|
const Directions = get_directions(Board, x, y);
|
||||||
|
let temp_score;
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
temp_score = evaluate_direction(Directions[i], player);
|
||||||
|
if (temp_score === WIN_DETECTED) {
|
||||||
|
return WIN_DETECTED
|
||||||
|
} else {
|
||||||
|
score += temp_score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function evaluate_state(Board, player, hash, restrictions) {
|
||||||
|
const black_score = eval_board(Board, -1, restrictions);
|
||||||
|
const white_score = eval_board(Board, 1, restrictions);
|
||||||
|
let score = 0;
|
||||||
|
if (player == -1) {
|
||||||
|
score = (black_score - white_score);
|
||||||
|
} else {
|
||||||
|
score = (white_score - black_score);
|
||||||
|
}
|
||||||
|
StateCache.set(hash,score);
|
||||||
|
StateCachePuts++;
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function random32() {
|
||||||
|
let o = new Uint32Array(1);
|
||||||
|
self.crypto.getRandomValues(o);
|
||||||
|
return o[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function Table_init() {
|
||||||
|
for (let i = 0; i < Rows; i++) {
|
||||||
|
Table[i] = [];
|
||||||
|
for (let j = 0; j < Columns; j++) {
|
||||||
|
Table[i][j] = []
|
||||||
|
Table[i][j][0] = random32(); //1
|
||||||
|
Table[i][j][1] = random32(); //2
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function hash(board) {
|
||||||
|
let h = 0;
|
||||||
|
let p;
|
||||||
|
for (let i = 0; i < Rows; i++) {
|
||||||
|
for (let j = 0; j < Columns; j++) {
|
||||||
|
let Board_value = board[i][j];
|
||||||
|
if (Board_value !== 0) {
|
||||||
|
if (Board_value === -1) {
|
||||||
|
p = 0
|
||||||
|
} else {
|
||||||
|
p = 1
|
||||||
|
}
|
||||||
|
h = h ^ Table[i][j][p];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_hash(hash, player, row, col) {
|
||||||
|
if (player === -1) {
|
||||||
|
player = 0
|
||||||
|
} else {
|
||||||
|
player = 1
|
||||||
|
}
|
||||||
|
hash = hash ^ Table[row][col][player];
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function negascout(newBoard, player, depth, alpha, beta, hash, restrictions, last_i, last_j) {
|
||||||
|
const alphaOrig = alpha;
|
||||||
|
const CacheNode =Cache.get(hash)
|
||||||
|
if ((CacheNode !== undefined) && (CacheNode.depth >= depth)) {
|
||||||
|
CacheHits++;
|
||||||
|
const score = CacheNode.score;
|
||||||
|
if (CacheNode.Flag === 0) {
|
||||||
|
CacheCutoffs++;
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
if (CacheNode.Flag === -1) {
|
||||||
|
alpha = Math.max(alpha, score);
|
||||||
|
} else if (CacheNode.Flag === 1) {
|
||||||
|
beta = Math.min(beta, score);
|
||||||
|
}
|
||||||
|
if (alpha >= beta) {
|
||||||
|
CacheCutoffs++
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fc++
|
||||||
|
|
||||||
|
if (checkwin(newBoard, last_i, last_j)) {
|
||||||
|
return -2000000 + (MaximumDepth - depth)
|
||||||
|
}
|
||||||
|
if (depth === 0) {
|
||||||
|
const StateCacheNode=StateCache.get(hash);
|
||||||
|
if (StateCacheNode !== undefined) {
|
||||||
|
StateCacheHits++
|
||||||
|
return StateCacheNode
|
||||||
|
}
|
||||||
|
return evaluate_state(newBoard, player, hash, restrictions)
|
||||||
|
}
|
||||||
|
|
||||||
|
const availSpots = BoardGenerator(restrictions, newBoard, player);
|
||||||
|
if (availSpots.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let b = beta;
|
||||||
|
let bestscore = -Infinity;
|
||||||
|
const bestMove={};
|
||||||
|
for (let y = 0; y < availSpots.length; y++) {
|
||||||
|
let i = availSpots[y].i;
|
||||||
|
let j = availSpots[y].j;
|
||||||
|
const newHash = update_hash(hash, player, i, j)
|
||||||
|
newBoard[i][j] = player;
|
||||||
|
const restrictions_temp = Change_restrictions(restrictions, i, j)
|
||||||
|
let score = -negascout(newBoard, -player, depth - 1, -b, -alpha, newHash, restrictions_temp, i, j)
|
||||||
|
if (score > alpha && score < beta && y > 0) {
|
||||||
|
score = -negascout(newBoard, -player, depth - 1, -beta, -score, newHash, restrictions_temp, i, j)
|
||||||
|
}
|
||||||
|
if (score > bestscore) {
|
||||||
|
bestscore = score
|
||||||
|
if (depth === MaximumDepth) {
|
||||||
|
bestMove.i=i
|
||||||
|
bestMove.j=j
|
||||||
|
bestMove.score=score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newBoard[i][j] = 0;
|
||||||
|
alpha = Math.max(alpha, score)
|
||||||
|
if (alpha >= beta) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
b = alpha + 1;
|
||||||
|
}
|
||||||
|
CachePuts++
|
||||||
|
const obj={score: bestscore,depth:depth};
|
||||||
|
if (bestscore <= alphaOrig) {
|
||||||
|
obj.Flag = 1
|
||||||
|
} else if (bestscore >= b) {
|
||||||
|
obj.Flag = -1
|
||||||
|
} else {
|
||||||
|
obj.Flag = 0
|
||||||
|
}
|
||||||
|
Cache.set(hash,obj);
|
||||||
|
if (depth == MaximumDepth) {
|
||||||
|
return bestMove
|
||||||
|
} else {
|
||||||
|
return bestscore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function iterative_negascout(player, Board, depth) {
|
||||||
|
let bestmove;
|
||||||
|
let i = 2;
|
||||||
|
while (i !== depth + 2) {
|
||||||
|
MaximumDepth = i;
|
||||||
|
bestmove = negascout(Board, player, MaximumDepth, -Infinity, Infinity, hash(Board), Get_restrictions(Board), 0, 0)
|
||||||
|
// Set_last_best(bestmove)
|
||||||
|
console.log(MaximumDepth)
|
||||||
|
console.log(bestmove)
|
||||||
|
let t11 = performance.now();
|
||||||
|
console.log((t11 - t00) / 1000)
|
||||||
|
if (bestmove.score > 1999900) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
return bestmove
|
||||||
|
}
|
||||||
20
packages/draughts/computer/quiescence-search.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { evaluateBoard } from './evaluate-board';
|
||||||
|
|
||||||
|
export function quiescenceSearch(board, alpha, beta) {
|
||||||
|
const baseE = evaluateBoard(board);
|
||||||
|
|
||||||
|
if (baseE >= beta) return beta;
|
||||||
|
alpha = Math.max(baseE, alpha);
|
||||||
|
|
||||||
|
for (const move of board.moves) {
|
||||||
|
if (move.captures.length <= 0) continue;
|
||||||
|
|
||||||
|
const nextBoard = board.doMove(move);
|
||||||
|
const e = -quiescenceSearch(nextBoard, -beta, -alpha);
|
||||||
|
|
||||||
|
if (e >= beta) return beta;
|
||||||
|
alpha = Math.max(e, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
return alpha;
|
||||||
|
}
|
||||||
178
packages/draughts/core/board.js
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import {
|
||||||
|
cellIsEmpty,
|
||||||
|
queenPiece,
|
||||||
|
eachCell,
|
||||||
|
pieceIsPlayer,
|
||||||
|
pieceIsQueen,
|
||||||
|
shouldQueen,
|
||||||
|
clonePosition,
|
||||||
|
filterToLongestCaptures,
|
||||||
|
cellWithinBounds,
|
||||||
|
} from './utilities';
|
||||||
|
import { Players, Pieces, GameStates } from '@draughts/core';
|
||||||
|
|
||||||
|
export class Board {
|
||||||
|
constructor(position, playerToMove, firstMove = true) {
|
||||||
|
this.position = position;
|
||||||
|
this.playerToMove = playerToMove;
|
||||||
|
this.firstMove = firstMove;
|
||||||
|
|
||||||
|
this.direction = this.playerToMove === Players.WHITE ? -1 : 1;
|
||||||
|
this.moves = this._computeMoves();
|
||||||
|
this.state = this._computeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeState() {
|
||||||
|
if (this.moves.length > 0) {
|
||||||
|
return GameStates.PLAYING;
|
||||||
|
}
|
||||||
|
|
||||||
|
let blackPieces = 0;
|
||||||
|
let whitePieces = 0;
|
||||||
|
|
||||||
|
for (const { piece } of eachCell(this.position)) {
|
||||||
|
if (pieceIsPlayer(piece, Players.WHITE)) {
|
||||||
|
whitePieces = whitePieces + 1;
|
||||||
|
} else if (pieceIsPlayer(piece, Players.BLACK)) {
|
||||||
|
blackPieces = blackPieces + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playerToMove = Players.NONE;
|
||||||
|
|
||||||
|
if (blackPieces === 0) {
|
||||||
|
return GameStates.WHITE_WON;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (whitePieces === 0) {
|
||||||
|
return GameStates.BLACK_WON;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GameStates.DRAW;
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeMoves() {
|
||||||
|
const starts = this._getStarts();
|
||||||
|
const captures = this._getCaptures(starts);
|
||||||
|
if (captures.length > 0) return captures;
|
||||||
|
return this._getMoves(starts);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getStarts() {
|
||||||
|
const starts = [];
|
||||||
|
for (const { cell, piece } of eachCell(this.position)) {
|
||||||
|
if (pieceIsPlayer(piece, this.playerToMove)) {
|
||||||
|
starts.push(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return starts;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getCaptures(starts) {
|
||||||
|
let captures = [];
|
||||||
|
for (const start of starts) {
|
||||||
|
captures = [
|
||||||
|
...captures,
|
||||||
|
...this._getNextCaptures(this.position, {
|
||||||
|
captures: [],
|
||||||
|
path: [start],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return filterToLongestCaptures(captures);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
_getNextCaptures(initialPosition, move) {
|
||||||
|
const captures = [];
|
||||||
|
const start = move.path.at(-1);
|
||||||
|
const piece = initialPosition[start.row][start.col];
|
||||||
|
const possibleDRow = pieceIsQueen(piece) ? [-1, 1] : [1];
|
||||||
|
|
||||||
|
for (const dRow of possibleDRow) {
|
||||||
|
for (const dCol of [-1, 1]) {
|
||||||
|
const middle = {
|
||||||
|
col: start.col + dCol * this.direction,
|
||||||
|
row: start.row + dRow * this.direction,
|
||||||
|
};
|
||||||
|
const end = {
|
||||||
|
col: middle.col + dCol * this.direction,
|
||||||
|
row: middle.row + dRow * this.direction,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!cellWithinBounds(end) || !cellIsEmpty(initialPosition, end))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const middlePiece = initialPosition[middle.row][middle.col];
|
||||||
|
if (
|
||||||
|
pieceIsPlayer(middlePiece, Players.NONE) ||
|
||||||
|
pieceIsPlayer(middlePiece, this.playerToMove)
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const queened = shouldQueen(end, piece);
|
||||||
|
const searchPosition = clonePosition(initialPosition);
|
||||||
|
|
||||||
|
searchPosition[start.row][start.col] = Pieces.NONE;
|
||||||
|
searchPosition[middle.row][middle.col] = Pieces.NONE;
|
||||||
|
searchPosition[end.row][end.col] = queened ? queenPiece(piece) : piece;
|
||||||
|
|
||||||
|
const nextMove = {
|
||||||
|
captures: [...move.captures, middle],
|
||||||
|
path: [...move.path, end],
|
||||||
|
};
|
||||||
|
|
||||||
|
captures.push(nextMove);
|
||||||
|
if (!queened)
|
||||||
|
captures.push(...this._getNextCaptures(searchPosition, nextMove));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterToLongestCaptures(captures);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getMoves(starts) {
|
||||||
|
const moves = [];
|
||||||
|
|
||||||
|
for (const start of starts) {
|
||||||
|
const piece = this.position[start.row][start.col];
|
||||||
|
const possibleDeltaRow = pieceIsQueen(piece) ? [-1, 1] : [1];
|
||||||
|
for (const deltaRow of possibleDeltaRow) {
|
||||||
|
for (const deltaCol of [-1, 1]) {
|
||||||
|
const end = {
|
||||||
|
col: start.col + deltaCol * this.direction,
|
||||||
|
row: start.row + deltaRow * this.direction,
|
||||||
|
};
|
||||||
|
if (cellWithinBounds(end) && cellIsEmpty(this.position, end)) {
|
||||||
|
moves.push({ captures: [], path: [start, end] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return moves;
|
||||||
|
}
|
||||||
|
|
||||||
|
doMove({ path, captures }) {
|
||||||
|
const start = path.at(0);
|
||||||
|
const startPiece = this.position[start.row][start.col];
|
||||||
|
const end = path.at(-1);
|
||||||
|
|
||||||
|
const endPiece = shouldQueen(end, startPiece)
|
||||||
|
? queenPiece(startPiece)
|
||||||
|
: startPiece;
|
||||||
|
|
||||||
|
const newPosition = clonePosition(this.position);
|
||||||
|
newPosition[path.at(0).row][path.at(0).col] = Pieces.NONE;
|
||||||
|
newPosition[end.row][end.col] = endPiece;
|
||||||
|
|
||||||
|
for (const capture of captures) {
|
||||||
|
newPosition[capture.row][capture.col] = Pieces.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPlayerToMove =
|
||||||
|
this.playerToMove === Players.WHITE ? Players.BLACK : Players.WHITE;
|
||||||
|
|
||||||
|
return new Board(newPosition, newPlayerToMove, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
packages/draughts/core/constants/enums.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export const Pieces = {
|
||||||
|
BLACK: 'b',
|
||||||
|
BLACK_QUEEN: 'bq',
|
||||||
|
NONE: '',
|
||||||
|
WHITE: 'w',
|
||||||
|
WHITE_QUEEN: 'wq',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GameStates = {
|
||||||
|
BLACK_WON: 'b',
|
||||||
|
DRAW: 'd',
|
||||||
|
PLAYING: 'p',
|
||||||
|
WHITE_WON: 'w',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Players = {
|
||||||
|
BLACK: 'b',
|
||||||
|
NONE: '',
|
||||||
|
WHITE: 'w',
|
||||||
|
};
|
||||||
5
packages/draughts/core/constants/index.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export { Players, Pieces, GameStates } from './enums';
|
||||||
|
|
||||||
|
export { INITIAL_POSITION } from './positions';
|
||||||
|
|
||||||
|
export const BOARD_SIZE = 8;
|
||||||
86
packages/draughts/core/constants/positions.js
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { Pieces } from './enums';
|
||||||
|
|
||||||
|
export const INITIAL_POSITION = [
|
||||||
|
[
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.BLACK,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.BLACK,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.BLACK,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.BLACK,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
Pieces.BLACK,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.BLACK,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.BLACK,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.BLACK,
|
||||||
|
Pieces.NONE,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.BLACK,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.BLACK,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.BLACK,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.BLACK,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.NONE,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.NONE,
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
Pieces.WHITE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.WHITE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.WHITE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.WHITE,
|
||||||
|
Pieces.NONE,
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.WHITE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.WHITE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.WHITE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.WHITE,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
Pieces.WHITE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.WHITE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.WHITE,
|
||||||
|
Pieces.NONE,
|
||||||
|
Pieces.WHITE,
|
||||||
|
Pieces.NONE,
|
||||||
|
],
|
||||||
|
];
|
||||||
22
packages/draughts/core/index.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export {
|
||||||
|
Players,
|
||||||
|
GameStates,
|
||||||
|
Pieces,
|
||||||
|
INITIAL_POSITION,
|
||||||
|
BOARD_SIZE,
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
|
export { Board } from './board';
|
||||||
|
|
||||||
|
export {
|
||||||
|
cellIsEmpty,
|
||||||
|
queenPiece,
|
||||||
|
eachCell,
|
||||||
|
compareCells,
|
||||||
|
pieceIsPlayer,
|
||||||
|
pieceIsQueen,
|
||||||
|
shouldQueen,
|
||||||
|
clonePosition,
|
||||||
|
filterToLongestCaptures,
|
||||||
|
cellWithinBounds,
|
||||||
|
} from './utilities';
|
||||||
81
packages/draughts/core/utilities/index.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { BOARD_SIZE, Pieces, Players } from '@draughts/core';
|
||||||
|
|
||||||
|
const isQueenMap = {
|
||||||
|
[Pieces.NONE]: false,
|
||||||
|
[Pieces.BLACK]: false,
|
||||||
|
[Pieces.WHITE]: false,
|
||||||
|
[Pieces.BLACK_QUEEN]: true,
|
||||||
|
[Pieces.WHITE_QUEEN]: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pieceIsQueen = (piece) => isQueenMap[piece];
|
||||||
|
|
||||||
|
const isPlayerMap = {
|
||||||
|
[Players.NONE]: new Set([Pieces.NONE]),
|
||||||
|
[Players.WHITE]: new Set([Pieces.WHITE, Pieces.WHITE_QUEEN]),
|
||||||
|
[Players.BLACK]: new Set([Pieces.BLACK, Pieces.BLACK_QUEEN]),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pieceIsPlayer = (piece, player) => {
|
||||||
|
return isPlayerMap[player].has(piece);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cellWithinBounds = ({ row, col }) =>
|
||||||
|
row < BOARD_SIZE && row >= 0 && col < BOARD_SIZE && col >= 0;
|
||||||
|
|
||||||
|
export const cellIsEmpty = (position, { row, col }) =>
|
||||||
|
position[row][col] === Pieces.NONE;
|
||||||
|
|
||||||
|
export function* eachCell(position) {
|
||||||
|
for (const [row, rowArray] of position.entries()) {
|
||||||
|
for (const [col, piece] of rowArray.entries()) {
|
||||||
|
yield { cell: { col, row }, piece };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const crownQueenMap = {
|
||||||
|
[Pieces.BLACK]: Pieces.BLACK_QUEEN,
|
||||||
|
[Pieces.WHITE]: Pieces.WHITE_QUEEN,
|
||||||
|
[Pieces.BLACK_QUEEN]: Pieces.BLACK_QUEEN,
|
||||||
|
[Pieces.WHITE_QUEEN]: Pieces.WHITE_QUEEN,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const queenPiece = (piece) => crownQueenMap[piece];
|
||||||
|
|
||||||
|
export const shouldQueen = (cell, piece) => {
|
||||||
|
if (pieceIsQueen(piece)) return false;
|
||||||
|
return (
|
||||||
|
(pieceIsPlayer(piece, Players.WHITE) && cell.row === 0) ||
|
||||||
|
(pieceIsPlayer(piece, Players.BLACK) && cell.row === BOARD_SIZE - 1)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterToLongestCaptures = (moves) => {
|
||||||
|
let longestCaptures = [];
|
||||||
|
let recordLength = 0;
|
||||||
|
for (const move of moves) {
|
||||||
|
if (move.captures.length > recordLength) {
|
||||||
|
longestCaptures = [move];
|
||||||
|
recordLength = move.captures.length;
|
||||||
|
} else if (move.captures.length === recordLength) {
|
||||||
|
longestCaptures.push(move);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return longestCaptures;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clonePosition = (position) => [...position.map((row) => [...row])];
|
||||||
|
|
||||||
|
export const compareCells = (cellOne, cellTwo) => {
|
||||||
|
if (!cellOne || !cellTwo) return false;
|
||||||
|
return cellOne.row === cellTwo.row && cellOne.col === cellTwo.col;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPlayerMap = {
|
||||||
|
[Players.NONE]: 'none',
|
||||||
|
[Players.WHITE]: 'white',
|
||||||
|
[Players.BLACK]: 'black',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatPlayer = (player) => formatPlayerMap[player];
|
||||||
73
pages/_app.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { ChakraProvider } from '@chakra-ui/react';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import Script from 'next/script';
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
|
export default function App(props) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Script
|
||||||
|
strategy="lazyOnload"
|
||||||
|
src="https://www.googletagmanager.com/gtag/js?id=G-N7EVGCFBVC"
|
||||||
|
/>
|
||||||
|
<Script
|
||||||
|
id="google-analytics"
|
||||||
|
strategy="lazyOnload"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
window.dataLayer = window.dataLayer || []; function
|
||||||
|
gtag(){dataLayer.push(arguments);} gtag('js', new Date());
|
||||||
|
gtag('config', 'G-N7EVGCFBVC', { page_path:
|
||||||
|
window.location.pathname, });
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Head>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="180x180"
|
||||||
|
href="/apple-touch-icon.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="32x32"
|
||||||
|
href="/favicon-32x32.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="16x16"
|
||||||
|
href="/favicon-16x16.png"
|
||||||
|
/>
|
||||||
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#d6c420" />
|
||||||
|
<meta name="msapplication-TileColor" content="#ffc40d" />
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta
|
||||||
|
property="og:title"
|
||||||
|
content="Free Online Draughts Game - play against the computer"
|
||||||
|
/>
|
||||||
|
<meta property="og:url" content="https://draughts.org/" />
|
||||||
|
<meta property="og:site_name" content="Draughts" />
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content="Play draughts free online against the computer. Read about Rules and Strategies for Draughts"
|
||||||
|
/>
|
||||||
|
<meta name="robots" content="index, follow" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Play draughts free online against the computer. Read about Rules and Strategies for Draughts"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="keywords"
|
||||||
|
content="draughts,checkers,free draughts,free checkers,board games,board gaming online,play draughts online,play checkers online,play free games online,free games,online games,online checkers,online draughts"
|
||||||
|
/>
|
||||||
|
</Head>
|
||||||
|
<ChakraProvider>
|
||||||
|
<props.Component {...props.pageProps} />
|
||||||
|
</ChakraProvider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
pages/_document.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import NextDocument, { Html, Head, Main, NextScript } from 'next/document';
|
||||||
|
|
||||||
|
export default class Document extends NextDocument {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Html lang="en">
|
||||||
|
<Head />
|
||||||
|
<body>
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
82
pages/history.jsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import Head from 'next/head';
|
||||||
|
import { Container, Text, VStack } from '@chakra-ui/react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { MainLayout } from '../components/layout/MainLayout';
|
||||||
|
import aquerqueImage from '../public/history/aquerque.jpeg';
|
||||||
|
import draughts1700sImage from '../public/history/draughts-1700s.webp';
|
||||||
|
|
||||||
|
export default function History() {
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<Head>
|
||||||
|
<title>Givan Dam - Main Dam</title>
|
||||||
|
</Head>
|
||||||
|
<VStack p={[3, 0]} spacing={6}>
|
||||||
|
<Text>
|
||||||
|
Penggalian arkeologi di Irak menemukan bentuk paling awal yang diketahui
|
||||||
|
permainan Draf. Penanggalan karbon digunakan untuk menentukan umur
|
||||||
|
permainan kuno, dan tampaknya berasal dari sekitar 3000 SM. Itu
|
||||||
|
papan dan jumlah potongan yang digunakan berbeda dari
|
||||||
|
Papan draf dan potongan digunakan saat ini.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Sekitar 1400 SM, orang Mesir kuno menggunakan papan berukuran 5 x 5 untuk memainkan a
|
||||||
|
permainan yang disebut Aquerque. Permainan ini sangat populer selama ini dan
|
||||||
|
itu dimainkan di seluruh peradaban barat selama ribuan tahun.
|
||||||
|
</Text>
|
||||||
|
<Container maxW="md">
|
||||||
|
<Image src={aquerqueImage} alt="ancient 5x5 egyption chessboard" />
|
||||||
|
</Container>
|
||||||
|
<Text>
|
||||||
|
Sekitar tahun 1100 A.D., permainan Aquerque berubah ketika seorang Prancis memainkannya
|
||||||
|
itu di papan catur menggunakan 12 buah untuk setiap pemain. Nama dari
|
||||||
|
permainan juga berubah. Itu dikenal sebagai "Fierges."
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Evolusi Draf berikutnya terjadi ketika peraturan berubah lagi,
|
||||||
|
membuatnya wajib untuk melompati Draf untuk maju ke seluruh papan.
|
||||||
|
Versi yang lebih baru ini lebih menantang daripada yang lama. Versi lama
|
||||||
|
dianggap lebih lambat dan kurang menantang, dan menjadi sosial
|
||||||
|
permainan yang dimainkan oleh para wanita pada zaman itu dan disebut “La Jeu Pleasant De
|
||||||
|
Dames,” (permainan wanita yang menyenangkan). Yang baru, lebih agresif
|
||||||
|
bentuk permainan tersebut dikenal dengan nama “Jeu Force” (Play Force).
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Draf akhirnya diekspor dari Prancis ke Inggris, Spanyol, dan
|
||||||
|
Amerika. Di Spanyol sekitar pertengahan tahun 1500-an, buku mulai ada
|
||||||
|
ditulis tentang Draf. Pada tahun 1756, William Payne, seorang matematikawan di
|
||||||
|
Inggris, menulis bukunya sendiri tentang Draf.
|
||||||
|
</Text>
|
||||||
|
<Container maxW="md">
|
||||||
|
<Image
|
||||||
|
src={draughts1700sImage}
|
||||||
|
alt="draughts being played in the 1700s"
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
<Text>
|
||||||
|
Pada tahun 1847, kejuaraan dunia Draf pertama berlangsung. Seiring waktu
|
||||||
|
berlalu, menjadi jelas bahwa game tersebut menghadirkan bukaan yang memberi
|
||||||
|
keuntungan satu pihak atas pihak lain. Ada dua batasan gerakan
|
||||||
|
dibuat di mana permainan dimulai dengan gaya acak. Dua ini
|
||||||
|
pembatasan bergerak sebagian besar digunakan oleh pemain ahli. Di zaman modern
|
||||||
|
Turnamen draf, tiga batasan gerakan digunakan.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Seiring kemajuan teknologi, tidak lama kemudian programmer komputer
|
||||||
|
mulai mengembangkan game Draft yang sangat mendasar yang dapat dimainkan
|
||||||
|
komputer. Alan Turing membuat game Draf yang belum sempurna di atas kertas
|
||||||
|
karena komputer pada saat itu belum cukup berkembang untuk menjalankannya
|
||||||
|
Program draf. Pada tahun 1952, Arthur L. Samuel membuat Draf pertama
|
||||||
|
program yang sebenarnya bisa dimainkan di komputer. Dari titik itu
|
||||||
|
maju, game Draf komputer telah meningkat dalam kecepatan dan fungsi.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Program Draf saat ini membutuhkan perencanaan yang kurang strategis dan lebih banyak lagi
|
||||||
|
kemampuan pencarian data komputer. Program Draf menggunakan database
|
||||||
|
pencarian yang menampilkan semua kemungkinan kombinasi ketika ada beberapa bagian
|
||||||
|
tertinggal di papan.
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
pages/index.jsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import Head from 'next/head';
|
||||||
|
import { Divider, Heading, HStack, VStack } from '@chakra-ui/react';
|
||||||
|
import { DraughtsMenuView } from '../components/draughts/settings/views/DraughtsMenu';
|
||||||
|
import { DraughtsProvider } from '../components/draughts/DraughtsContext';
|
||||||
|
import { DraughtsBoard } from '../components/draughts/board/views/DraughtsBoard';
|
||||||
|
import { DraughtsGameInfoView } from '../components/draughts/DraughtsGameInfo';
|
||||||
|
import { ComputerDifficulty } from '../components/draughts/settings/constants/computer-difficulty';
|
||||||
|
import { MainLayout } from '../components/layout/MainLayout';
|
||||||
|
import { DraughtsRulesContent } from '../components/content/DraughtsRulesContent';
|
||||||
|
import { INITIAL_POSITION, Players } from '@draughts/core';
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<Head>
|
||||||
|
<title>Givan Checkers - Main Checkers Secara Gratis</title>
|
||||||
|
</Head>
|
||||||
|
<DraughtsProvider
|
||||||
|
settings={{
|
||||||
|
computerDifficulty: ComputerDifficulty.MEDIUM,
|
||||||
|
userPlayer: Players.WHITE,
|
||||||
|
}}
|
||||||
|
board={{ playerToMove: Players.WHITE, position: INITIAL_POSITION }}
|
||||||
|
>
|
||||||
|
<VStack spacing={4}>
|
||||||
|
<DraughtsBoard />
|
||||||
|
<HStack>
|
||||||
|
<DraughtsMenuView />
|
||||||
|
<DraughtsGameInfoView />
|
||||||
|
</HStack>
|
||||||
|
<Divider />
|
||||||
|
<Heading size="sm">Peraturan Main</Heading>
|
||||||
|
<DraughtsRulesContent />
|
||||||
|
</VStack>
|
||||||
|
</DraughtsProvider>
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
pages/rules.jsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/* eslint-disable unicorn/filename-case */
|
||||||
|
import { AspectRatio } from '@chakra-ui/react';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import { DraughtsRulesContent } from '../components/content/DraughtsRulesContent';
|
||||||
|
import { MainLayout } from '../components/layout/MainLayout';
|
||||||
|
|
||||||
|
export default function History() {
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<Head>
|
||||||
|
<title>Givan Checkers - Peraturan & Cara Bermain</title>
|
||||||
|
</Head>
|
||||||
|
<DraughtsRulesContent />
|
||||||
|
{/*<AspectRatio maxW="md" radio={16 / 9}>
|
||||||
|
<iframe
|
||||||
|
src="https://www.youtube.com/embed/PgNN6CdkYXs"
|
||||||
|
title="YouTube video player"
|
||||||
|
frameBorder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowFullScreen
|
||||||
|
></iframe>
|
||||||
|
</AspectRatio>*/}
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
86
pages/strategies.jsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import Head from 'next/head';
|
||||||
|
import { AspectRatio, Text, VStack } from '@chakra-ui/react';
|
||||||
|
import { MainLayout } from '../components/layout/MainLayout';
|
||||||
|
|
||||||
|
export default function History() {
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<Head>
|
||||||
|
<title>Givan Checkers - Strategi & Pembukaan</title>
|
||||||
|
</Head>
|
||||||
|
<VStack p={[3, 0]} spacing={6}>
|
||||||
|
<Text>
|
||||||
|
Draf, juga dikenal sebagai checkers, adalah permainan papan strategi yang memiliki
|
||||||
|
telah ada selama ribuan tahun. Ada banyak varian, tapi
|
||||||
|
versi paling umum dimainkan di papan kotak-kotak 8x8. Keduanya
|
||||||
|
permainan pemain terdiri dari dua belas buah (pria, catur, draft)per
|
||||||
|
samping. Potongan dimulai dari tiga baris pertama pada warna hitam/gelap
|
||||||
|
kotak saja. Pria hanya bisa maju secara diagonal "melompat" lebih
|
||||||
|
bagian itu dan mendarat di ruang kosong yang berdekatan. Setelah semua pria memiliki
|
||||||
|
ditangkap, permainan dimenangkan. Permainan juga bisa dimenangkan melalui
|
||||||
|
menghalangi kemampuan lawan untuk bergerak. Ada banyak taktik yang berguna
|
||||||
|
untuk meningkatkan peluang keberhasilan Anda.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Menobatan, atau menjadikan raja, sangat meningkatkan kekuatan dan portabilitas
|
||||||
|
laki-laki Anda. Jika Anda bisa mendapatkan bagian ke garis dasar pemain lain, itu
|
||||||
|
bisa "dimahkotai". Sepotong lain ditempatkan di atas
|
||||||
|
membedakannya dari draf biasa. Raja sekarang dapat dipindahkan keduanya
|
||||||
|
maju dan mundur, secara efektif menggandakan jangkauannya.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Karena pria yang menangkap membutuhkan kotak kosong untuk dilompati, memang begitu
|
||||||
|
bijaksana untuk memindahkan Anda potongan secara massal. Cobalah untuk tidak meninggalkan potongan individu
|
||||||
|
terpencil. Pindahkan lebih sedikit potongan dalam formasi ketat.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Cobalah untuk meninggalkan orang-orang garis dasar Anda di stasiun selama mungkin.
|
||||||
|
Kotak bebas apa pun berpotensi untuk penobatan oposisi. Mereka akan
|
||||||
|
tidak dapat membuat raja jika mereka tidak bisa mendarat di sana.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Draf, seperti permainan papan lainnya, bekerja berdasarkan prinsip umum
|
||||||
|
bertukar potongan setiap kali ada yang di depan. Keuntungan material dari memiliki
|
||||||
|
hanya satu orang tambahan menjadi lebih signifikan secara proporsional semakin sedikit
|
||||||
|
potongan tetap. Peluang mahkota akan meningkat pesat. Satu
|
||||||
|
peringatan untuk prinsip umum ini, adalah mengabaikan keunggulan posisi
|
||||||
|
untuk keuntungan materi buta. Seorang raja bisa sangat mengubah jalannya permainan
|
||||||
|
dengan cepat.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Mengorbankan draf bisa tampak sembrono atau ceroboh. Tapi ini
|
||||||
|
strategi dapat digunakan untuk menarik keuntungan posisional. Untuk menghapus a
|
||||||
|
potongan dasar dalam persiapan untuk penobatan misalnya, akan menjadi a
|
||||||
|
penggunaan taktik pengorbanan yang baik.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Aturan draf menyatakan bahwa jika lawan menawarkan bagian untuk
|
||||||
|
menangkap, itu harus diambil. "gerakan paksa" dapat
|
||||||
|
dipekerjakan untuk keuntungan besar. Jika draf lawan menghalangi Anda
|
||||||
|
cara membuat raja, Anda dapat memajukan bagian lain ke sisi lain
|
||||||
|
dari pemblokir yang menyinggung. Ini akan memaksa lawan Anda untuk menangkap
|
||||||
|
memungkinkan jalur yang jelas ke garis belakang untuk penobatan.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Memblokir digunakan untuk menggagalkan gerakan oposisi. Itu membutuhkan yang baik
|
||||||
|
banyak pemikiran ke depan oleh pemain lawan. Mencoba untuk yang kedua
|
||||||
|
menebak rencana atau rangkaian gerakan membutuhkan pengetahuan yang baik tentang
|
||||||
|
strategi. Sementara pemblokiran bersifat defensif dalam tujuannya (untuk mencegah
|
||||||
|
pemain lain dari maju) itu dapat menghasilkan posisi menang. Aku jatuh
|
||||||
|
potongan lawan diblokir dan dia tidak bisa bergerak, sesuai
|
||||||
|
dengan aturan permainan, dia kalah dalam permainan.
|
||||||
|
</Text>
|
||||||
|
{/*<Text>Here's a great video on draughts strategies:</Text>*/}
|
||||||
|
{/*<AspectRatio ratio={16 / 9}>
|
||||||
|
<iframe
|
||||||
|
src="https://www.youtube.com/embed/Lfo3yfrbUs0"
|
||||||
|
title="YouTube video player"
|
||||||
|
frameBorder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowFullScreen
|
||||||
|
/>
|
||||||
|
</AspectRatio>*/}
|
||||||
|
</VStack>
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
prettier.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
semi: true,
|
||||||
|
singleQuote: true,
|
||||||
|
tabWidth: 2,
|
||||||
|
trailingComma: 'es5',
|
||||||
|
};
|
||||||
BIN
public/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
public/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
9
public/browserconfig.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/mstile-150x150.png"/>
|
||||||
|
<TileColor>#ffc40d</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
||||||
BIN
public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 837 B |
BIN
public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/history/aquerque.jpeg
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
public/history/draughts-1700s.webp
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
public/mstile-144x144.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
public/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
public/mstile-310x150.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/mstile-310x310.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/mstile-70x70.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
27
public/safari-pinned-tab.svg
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<metadata>
|
||||||
|
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||||
|
</metadata>
|
||||||
|
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M3305 6993 c-605 -32 -1225 -238 -1730 -574 -223 -149 -354 -256
|
||||||
|
-545 -448 -194 -194 -301 -325 -449 -546 -299 -450 -491 -977 -557 -1535 -24
|
||||||
|
-198 -24 -582 0 -780 79 -671 332 -1277 752 -1805 113 -141 388 -416 529 -529
|
||||||
|
772 -615 1736 -877 2708 -735 604 87 1196 346 1682 735 142 114 417 388 529
|
||||||
|
529 419 526 672 1134 752 1805 24 198 24 582 0 780 -47 400 -154 771 -322
|
||||||
|
1120 -182 378 -385 663 -683 960 -287 286 -546 476 -891 649 -554 280 -1156
|
||||||
|
407 -1775 374z m635 -1984 c239 -419 440 -768 445 -775 8 -12 162 31 870 239
|
||||||
|
512 151 861 249 862 243 2 -6 -174 -546 -391 -1201 l-393 -1190 -1833 0 -1833
|
||||||
|
0 -393 1190 c-217 655 -393 1195 -391 1201 1 7 333 -86 862 -242 702 -207 862
|
||||||
|
-251 870 -240 6 7 206 356 445 775 239 418 437 761 440 761 3 0 201 -343 440
|
||||||
|
-761z m1290 -2987 c0 -5 -59 -187 -130 -405 l-131 -397 -1469 0 -1469 0 -131
|
||||||
|
396 c-71 218 -130 400 -130 405 0 5 744 9 1730 9 952 0 1730 -4 1730 -8z"/>
|
||||||
|
<path d="M3413 4099 c-89 -44 -176 -171 -222 -324 -141 -469 31 -1060 309
|
||||||
|
-1060 315 0 476 732 260 1182 -88 185 -223 263 -347 202z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
19
public/site.webmanifest
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"short_name": "",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
||||||