diff --git a/README.md b/README.md index 5594c21..d159549 100644 --- a/README.md +++ b/README.md @@ -24,22 +24,22 @@ #### Gameplay GIF -![gameplay](https://github.com/romantech/tic-tac-toe/assets/8604840/86e3e194-2c9a-4509-8188-4e33462f0247) +![gameplay](https://github.com/romantech/tic-tac-toe/assets/8604840/fe46607c-c8a3-4f05-9c1d-a0912f2f4577) #### Home Screen -![mobile-1](https://github.com/romantech/tic-tac-toe/assets/8604840/872c4e63-9a5e-46ff-96bb-1767e7dca5c6) +![mobile-1](https://github.com/romantech/tic-tac-toe/assets/8604840/51d8b01a-9768-4134-ad7b-2f5c52134d20) #### Game Configuration -![mobile-2](https://github.com/romantech/tic-tac-toe/assets/8604840/fb7350f2-9e04-493b-a29a-36bed9bf37b8) +![mobile-2](https://github.com/romantech/tic-tac-toe/assets/8604840/ade907fd-b16a-417e-a1f1-207a8d84e84e) #### Game Screen -![mobile-3](https://github.com/romantech/tic-tac-toe/assets/8604840/dbd5f0cb-e772-4526-9ad2-33e9488495dd) +![mobile-3](https://github.com/romantech/tic-tac-toe/assets/8604840/43bf3611-58cf-45f0-8f25-b42fc00cc126) #### Match History -![mobile-4](https://github.com/romantech/tic-tac-toe/assets/8604840/2293292b-c699-43f7-b381-01ca68f828d2) +![mobile-4](https://github.com/romantech/tic-tac-toe/assets/8604840/03a30cfe-b1d6-4195-abfb-296b11ca741e) > Source of game sound effects from [Neave Interactive](https://neave.com) diff --git a/index.html b/index.html index 9143b00..964e6a8 100644 --- a/index.html +++ b/index.html @@ -21,7 +21,7 @@ - + Tic-Tac-Toe diff --git a/public/favicon.png b/public/favicon.png index 9ab2fef..6494f5a 100644 Binary files a/public/favicon.png and b/public/favicon.png differ diff --git a/public/icon-512.png b/public/icon-512.png index 821cbbc..001b9e4 100644 Binary files a/public/icon-512.png and b/public/icon-512.png differ diff --git a/public/icon-maskable-512.png b/public/icon-maskable-512.png index ff823f5..1346f02 100644 Binary files a/public/icon-maskable-512.png and b/public/icon-maskable-512.png differ diff --git a/public/manifest.json b/public/manifest.json index 4101304..909c805 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -14,7 +14,8 @@ { "src": "/icon-512.png", "sizes": "512x512", - "type": "image/png" + "type": "image/png", + "purpose": "any" } ], "start_url": "/", diff --git a/src/assets/broom.svg b/src/assets/broom.svg new file mode 100644 index 0000000..e3bd126 --- /dev/null +++ b/src/assets/broom.svg @@ -0,0 +1 @@ + diff --git a/src/assets/index.ts b/src/assets/index.ts index c0350e0..a27a987 100644 --- a/src/assets/index.ts +++ b/src/assets/index.ts @@ -2,3 +2,4 @@ export { default as GitHubSvg } from '@/assets/github.svg?react'; export { default as HomeSvg } from '@/assets/home.svg?react'; export { default as SpearOnSvg } from '@/assets/speaker-on.svg?react'; export { default as SpearOffSvg } from '@/assets/speaker-off.svg?react'; +export { default as Broom } from '@/assets/broom.svg?react'; diff --git a/src/components/common/button.tsx b/src/components/common/button.tsx index 266c1d5..ccbaa6d 100644 --- a/src/components/common/button.tsx +++ b/src/components/common/button.tsx @@ -2,21 +2,47 @@ import { ComponentProps } from 'react'; import { clsx } from 'clsx'; +type ButtonVariant = 'solid' | 'outline'; + +interface ButtonProps extends ComponentProps<'button'> { + variant?: ButtonVariant; +} + +const getVariantClasses = (variant: ButtonVariant, disabled: boolean = false) => { + const classes = []; + + if (disabled) { + classes.push('bg-slate-600/30 text-slate-500 cursor-not-allowed'); + if (variant === 'outline') classes.push('border border-slate-500'); + return classes.join(' '); + } + + switch (variant) { + case 'solid': + classes.push('bg-slate-600/80 hover:bg-slate-600'); + break; + case 'outline': + classes.push('border border-slate-500 hover:bg-slate-700/80'); + break; + default: + throw new Error(`Invalid variant: ${variant}`); + } + + return classes.join(' '); +}; + export default function Button({ children, disabled, className, + variant = 'solid', ...buttonProps -}: ComponentProps<'button'>) { +}: ButtonProps) { return ( -
- - show order - + show order + ); } diff --git a/src/components/game/square.tsx b/src/components/game/square.tsx index e16741c..049a4a3 100644 --- a/src/components/game/square.tsx +++ b/src/components/game/square.tsx @@ -2,7 +2,7 @@ import { ComponentProps } from 'react'; import { clsx } from 'clsx'; -import { BoardSize, getSequenceTextClasses, NBSP, TMark, TSequence } from '@/lib'; +import { BoardSize, getSequenceTextClasses, TMark, TSequence } from '@/lib'; interface SquareProps extends ComponentProps<'button'> { mark: TMark; @@ -55,8 +55,7 @@ export default function Square({ className={buttonClasses} {...buttonProps} > - {/* layout shift 방지를 위해 논브레이크 스페이스를 기본값으로 지정 */} - {mark ?? NBSP} + {mark} {sequence} ); diff --git a/src/components/game/undo-status.tsx b/src/components/game/undo-status.tsx index 1f1b42e..01b2a1d 100644 --- a/src/components/game/undo-status.tsx +++ b/src/components/game/undo-status.tsx @@ -43,9 +43,9 @@ interface StatusProps { const Status = ({ mark, count, className }: StatusProps) => { return ( -
-

{mark}

-

{`Count : ${count}`}

+
+ {mark} + {`Count ${count}`}
); }; diff --git a/src/hooks/use-game.ts b/src/hooks/use-game.ts index 6527115..e12705d 100644 --- a/src/hooks/use-game.ts +++ b/src/hooks/use-game.ts @@ -112,9 +112,10 @@ export const useGame = ({ }, [board, isSinglePlay, onBoardClick, size, winCondition]); const hasMark = sequence.current.length > 0; - const isDraw = sequence.current.length === board.length; const hasWinner = Boolean(winner.current.identifier); + const hasUndoCount = undoControls.getUndoCountBy(currentPlayer.current) > 0; + const isDraw = sequence.current.length === board.length && !hasWinner; const enabledReset = hasMark || isUndoUsed; const enableUndo = !hasWinner && hasMark && hasUndoCount && !isDraw; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index f662c24..1481078 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,6 +1,5 @@ import { GameOption, PlayerConfigs, TSquare, Winner } from '@/lib/types'; -export const NBSP = '\u00A0'; export const MAX_UNDO_COUNT = 3; export const TIE_SYMBOL = '−'; @@ -16,46 +15,6 @@ export enum BoardSize { Size6 = 6, } -export type BoardConfig = ReturnType; -export const getBoardConfig = (size: BoardSize, type: BoardType = BoardType.Play) => { - const boardConfig = { - [BoardSize.Size3]: { - [BoardType.Play]: 'grid-cols-3 text-7xl sm:text-8xl', - [BoardType.View]: 'grid-cols-3 text-6xl', - }, - [BoardSize.Size4]: { - [BoardType.Play]: 'grid-cols-4 text-6xl sm:text-7xl', - [BoardType.View]: 'grid-cols-4 text-5xl', - }, - [BoardSize.Size5]: { - [BoardType.Play]: 'grid-cols-5 text-5xl sm:text-6xl', - [BoardType.View]: 'grid-cols-5 text-4xl', - }, - [BoardSize.Size6]: { - [BoardType.Play]: 'grid-cols-6 text-4xl sm:text-5xl', - [BoardType.View]: 'grid-cols-6 text-3xl', - }, - }; - - return boardConfig[size][type]; -}; - -export const getSequenceTextClasses = (size: BoardSize) => { - switch (size) { - case BoardSize.Size3: - return 'text-lg top-0.5 right-1.5'; - case BoardSize.Size4: - return 'text-base top-0 right-1'; - case BoardSize.Size5: - return 'text-sm top-0 right-0.5'; - case BoardSize.Size6: - return 'text-xs top-0 right-0.5'; - default: - console.error('Invalid size'); - return 'text-base'; - } -}; - export const boardSize = Object.values(BoardSize).filter(Number); export enum BasePlayer { @@ -115,3 +74,57 @@ export const defaultSquare: TSquare = { mark: null, }; export const defaultWinner: Winner = { identifier: null, indices: null, mark: null }; + +export const getBoardConfig = (size: BoardSize, type: BoardType = BoardType.Play) => { + const boardConfig = { + [BoardSize.Size3]: { + [BoardType.Play]: 'grid-cols-3 text-7xl sm:text-8xl', + [BoardType.View]: 'grid-cols-3 text-6xl', + }, + [BoardSize.Size4]: { + [BoardType.Play]: 'grid-cols-4 text-6xl sm:text-7xl', + [BoardType.View]: 'grid-cols-4 text-5xl', + }, + [BoardSize.Size5]: { + [BoardType.Play]: 'grid-cols-5 text-5xl sm:text-6xl', + [BoardType.View]: 'grid-cols-5 text-4xl', + }, + [BoardSize.Size6]: { + [BoardType.Play]: 'grid-cols-6 text-4xl sm:text-5xl', + [BoardType.View]: 'grid-cols-6 text-3xl', + }, + }; + + return boardConfig[size][type]; +}; + +export const getSequenceTextClasses = (size: BoardSize) => { + switch (size) { + case BoardSize.Size3: + return 'text-lg top-0.5 right-1.5'; + case BoardSize.Size4: + return 'text-base top-0 right-1'; + case BoardSize.Size5: + return 'text-sm top-0 right-0.5'; + case BoardSize.Size6: + return 'text-xs top-0 right-0.5'; + default: + throw new Error(`Invalid size: ${size}`); + } +}; + +export const getPlayModeSquareClasses = (totalSquares: number, index: number) => { + const size = Math.sqrt(totalSquares); + const rightBorder = size > 4 ? 'border-r-[3px]' : 'border-r-4'; + const bottomBorder = size > 4 ? 'border-b-[3px]' : 'border-b-4'; + + const classes: string[] = ['border-slate-200']; + + const isLastSquareInRow = (index + 1) % size === 0; + if (!isLastSquareInRow) classes.push(rightBorder); + + const isLastRowSquare = index >= totalSquares - size; + if (!isLastRowSquare) classes.push(bottomBorder); + + return classes.join(' '); +}; diff --git a/src/pages/game-history.tsx b/src/pages/game-history.tsx index 6be8b8a..7b1b933 100644 --- a/src/pages/game-history.tsx +++ b/src/pages/game-history.tsx @@ -1,21 +1,25 @@ +import { Broom } from '@/assets'; import { BoardList, Button, Divider, Empty, Fade, OrderToggle } from '@/components'; import { useDisclosure, useGameHistory } from '@/hooks'; export default function GameHistory() { const { historyList, clearHistory } = useGameHistory(); const { toggle: toggleShowOrder, isOpen: showOrder } = useDisclosure(true); + const isHistoryEmpty = historyList.length === 0; return (
- - +
- +
); } diff --git a/src/pages/home.tsx b/src/pages/home.tsx index f6d53e3..5bd8635 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -4,14 +4,16 @@ import { ScreenType } from '@/lib'; export default function Home() { const changeScreen = useSetScreen(); + const onStartClick = () => changeScreen(ScreenType.Settings); + const onHistoryClick = () => changeScreen(ScreenType.History); return ( - - - );