first commit

This commit is contained in:
unknown
2023-01-27 20:50:01 +08:00
commit a5e17d8c5d
69 changed files with 12547 additions and 0 deletions

View 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,
};

View File

@ -0,0 +1,6 @@
import { useDraughtsBoard } from '../DraughtsBoardContext';
export function useDraughtsPlayerToMove(player) {
const { board } = useDraughtsBoard();
return player === board.playerToMove;
}

View 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 };
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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,
};