first commit
This commit is contained in:
3
components/adverts/BottomBannerAdvert.jsx
Normal file
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
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
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
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
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
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
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
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,
|
||||
};
|
||||
37
components/draughts/game/DraughtsGameContext.jsx
Normal file
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
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
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
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
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
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
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
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
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
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
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,
|
||||
};
|
||||
Reference in New Issue
Block a user