first commit
This commit is contained in:
51
components/draughts/board/DraughtsBoardContext.jsx
Normal file
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
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
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
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
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
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
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,
|
||||
};
|
||||
Reference in New Issue
Block a user