This commit is contained in:
ilya 2024-06-24 12:30:01 +03:00
parent a756a40572
commit 6bad98efd1
18 changed files with 540 additions and 209 deletions

View file

@ -1,2 +1,2 @@
semi: false semi: false
singleQuote: true singleQuote: true

BIN
bun.lockb

Binary file not shown.

View file

@ -14,4 +14,4 @@
"components": "@/components", "components": "@/components",
"utils": "@/lib/utils" "utils": "@/lib/utils"
} }
} }

View file

@ -7,13 +7,15 @@
"dev": "bunx --bun vite", "dev": "bunx --bun vite",
"build": "tsc -b && bunx --bun vite build", "build": "tsc -b && bunx --bun vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "bunx --bun vite preview" "preview": "bunx --bun vite preview",
"fmt": "bunx --bun prettier --write ."
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"eruda": "^3.0.1",
"immer": "^10.1.1", "immer": "^10.1.1",
"lucide-react": "^0.396.0", "lucide-react": "^0.396.0",
"react": "^18.3.1", "react": "^18.3.1",

View file

@ -1,3 +1,5 @@
html, body, #root { html,
body,
#root {
@apply h-full w-full; @apply h-full w-full;
} }

View file

@ -2,25 +2,49 @@ import { create } from 'zustand'
import './App.css' import './App.css'
import { produce } from 'immer' import { produce } from 'immer'
import doska from './assets/doska.svg' import doska from './assets/doska.svg'
import { useState } from 'react' import { ReactNode, useEffect, useState } from 'react'
import { Button } from './components/ui/button' import { Button } from './components/ui/button'
import { Input } from './components/ui/input' import { Input } from './components/ui/input'
import { Label } from './components/ui/label' import { Label } from './components/ui/label'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from './components/ui/card'
import { getPoints } from './lib/math'
type Throw =
| {
type: 'real'
pts: number
}
| {
type: 'bust'
remaining: number
}
| {
type: 'finish'
}
interface Player { interface Player {
points: number throws: Throw[]
throws: number pts: number
} }
interface Game { interface Game {
players: Player[] players: Player[]
currentPlayerIdx: number currentPlayerIdx: number
round: number round: number
maxThrows: number
started: boolean started: boolean
startedAt: number startedAt: number
o1: number
start(nplayers: number): void start(nplayers: number, mthrows: number, o1: number): void
doThrow(pts: number, double: boolean): Throw
finish(): number
} }
const useGame = create<Game>((set) => ({ const useGame = create<Game>((set) => ({
@ -29,59 +53,287 @@ const useGame = create<Game>((set) => ({
round: 0, round: 0,
started: false, started: false,
startedAt: -1, startedAt: -1,
start(nplayers) { maxThrows: 3,
set(state => produce(state, state => { o1: 501,
state.players = Array.from({ length: nplayers }, () => ({ start(nplayers, mthrows, o1) {
points: 0, console.log('start', nplayers)
throws: 0 set((state) =>
})) produce(state, (state) => {
state.currentPlayerIdx = 0 state.o1 = o1
})) state.round = 1
} state.started = true
state.startedAt = new Date().getTime()
state.maxThrows = mthrows
state.players = Array.from({ length: nplayers }, () => ({
pts: 0,
throws: [],
}))
state.currentPlayerIdx = 0
}),
)
},
doThrow(pts, double): Throw {
let th: Throw = { type: 'real', pts }
set((state) =>
produce(state, (state) => {
if (
state.players[state.currentPlayerIdx].throws.length >= state.maxThrows
) {
state.currentPlayerIdx =
(state.currentPlayerIdx + 1) % state.players.length
if (state.currentPlayerIdx === 0) {
state.round++
state.players.forEach(p => p.throws = [])
}
}
const player = state.players[state.currentPlayerIdx]
if (player.pts + pts === state.o1 && double) th = { type: 'finish' }
else if (Math.abs(player.pts + pts - state.o1) <= 1) th = { type: 'bust', remaining: player.pts + pts - state.o1 }
player.throws.push(th)
if (th.type === 'real') player.pts += th.pts
}),
)
return th
},
finish() {
let idx: number = 0
set((state) => {
state.players.forEach((player, ix) => {
console.log(player, ix)
if (player.pts > state.players[idx].pts) {
idx = ix
console.log('winner', player, ix)
}
})
return produce(state, (state) => {
state.started = false
state.currentPlayerIdx = -1
})
})
console.log('winner', idx)
return idx
},
})) }))
function fuckingGameTime(ms: number) {
const duration = ms / 1000
const hours = Math.floor(duration / 3600)
const minutes = Math.floor((duration % 3600) / 60)
const seconds = Math.floor(duration % 60)
return `${hours < 10 ? '0' + hours : hours}:${minutes < 10 ? '0' + minutes : minutes}:${seconds < 10 ? '0' + seconds : seconds}`
}
function useTimer(startedAt: number) {
const [time, setTime] = useState(0)
useEffect(() => {
const interval = setInterval(
() => setTime(new Date().getTime() - startedAt),
1000,
)
return () => clearInterval(interval)
}, [startedAt])
return time
}
export default function App() { export default function App() {
const [startedAt, started, round] = useGame(game => [game.startedAt, game.started, game.round]) const [startedAt, started, round, doThrow, finish] = useGame((game) => [
game.startedAt,
game.started,
game.round,
game.doThrow,
game.finish
])
const gameTime = useTimer(startedAt)
const [points, setPoints] = useState(0)
const [double, setDouble] = useState(false)
const [winner, setWinner] = useState<number | undefined>(undefined)
return ( return (
<div className='flex flex-col h-full w-full'> <div className="flex flex-col h-full w-full">
{/* Верхняя часть: доска */} {/* Верхняя часть: доска */}
<div className='flex flex-col h-full justify-start items-center'> <div className="flex flex-col h-full justify-between items-center">
<div className="flex flex-row justify-between items-center w-full"> <div className="flex flex-row justify-between items-center w-full">
<div className="flex flex-row justify-start"> <div className="flex flex-col justify-start">
<Label htmlFor="round">Раунд</Label> <Label htmlFor="round">Раунд</Label>
<p id="round">{started ? `#${round}` : "Игра не начата"}</p></div> <p id="round">#{round}</p>
<p>{started ? new Date(Date.now() - startedAt).toLocaleTimeString() : ""}</p> </div>
{points > 0 ? (
<Button
onClick={() => {
if (doThrow(points, double).type === 'finish')
setWinner(finish())
setPoints(0)
}}
>
{points}
</Button>
) : null}
<div className="flex flex-col justify-start">
<Label htmlFor="time">Время</Label>
<p id="time">{started ? fuckingGameTime(gameTime) : '00:00:00'}</p>
</div>
</div> </div>
<Board /> <Board setPoints={setPoints} setDouble={setDouble} />
</div> </div>
{/* Нижняя часть: управление игрой */} {/* Нижняя часть: управление игрой */}
<Control /> <Control {...{winner,setWinner}} />
</div> </div>
) )
} }
function Board() { function Board({
const [points, setPoints] = useState(0) setPoints,
setDouble,
}: {
setPoints: (w: number) => void
setDouble: (d: boolean) => void
}) {
return ( return (
<img src={doska} onMouseMove={e => {}} /> <img
src={doska}
onClick={(e) => {
const { points, double } = getPoints(e)
setPoints(points)
setDouble(double)
}}
/>
) )
} }
function Control() { function Control({ winner, setWinner }: {winner: number | undefined, setWinner: (w: number | undefined) => void}) {
const [started, round, start, players] = useGame(game => [game.started, game.round, game.start, game.players]) const [started, start, players, finish] = useGame((game) => [
game.started,
game.start,
game.players,
game.finish,
])
const [nplayers, setNplayers] = useState(2) const [nplayers, setNplayers] = useState(2)
const [mthrows, setMthrows] = useState(3)
const [o1, setO1] = useState(501)
return ( return (
<div className='flex flex-col h-full justify-start gap-2'> <div className="flex flex-col h-full justify-start gap-2">
<Label htmlFor="nplayers">Количество игроков</Label> <div className="flex flex-row w-full justify-between items-center">
<Input id="nplayers" onChange={ev => setNplayers(parseInt(ev.target.value))} type="number" value={nplayers} disabled={started} /> <div className="flex flex-col">
<Label htmlFor="nplayers">Игроки</Label>
<Input
id="nplayers"
onChange={(ev) => setNplayers(parseInt(ev.target.value))}
type="number"
value={nplayers}
disabled={started}
/>
<Button
onClick={() => {
setWinner(undefined)
start(nplayers, mthrows, o1)
}}
disabled={started}
>
{'Старт'}
</Button>
</div>
<Button onClick={() => start(nplayers)} disabled={started}> <div className="flex flex-col">
{"Начать игру"} <Label htmlFor="mthrows">Дротики</Label>
</Button> <Input
id="mthrows"
onChange={(ev) => setMthrows(parseInt(ev.target.value))}
type="number"
value={mthrows}
disabled={started}
/>
<Button
variant="secondary"
onClick={() => {
setWinner(undefined)
start(nplayers, mthrows, o1)
}}
disabled={!started}
>
{'Рестарт'}
</Button>
</div>
<div className="flex flex-col">
<Label htmlFor="o1">Макс. очков</Label>
<Input
id="o1"
onChange={(ev) => setO1(parseInt(ev.target.value))}
type="number"
value={o1}
disabled={started}
/>
<Button
onClick={() => setWinner(finish())}
disabled={!started}
variant="destructive"
>
{'Стоп'}
</Button>
</div>
</div>
{started
? players.map((player, idx) => (
<Player player={player} idx={idx} key={idx}>
{Array.from({ length: mthrows }, (_, i) => {
const th = player.throws[i]
return (
<div
key={i}
className="flex flex-row justify-start items-center gap-2"
>
<Label htmlFor={`throw-${idx}-${i}`}>Бросок #{i + 1}</Label>
<p id={`throw-${idx}-${i}`}>
{(() => {
switch (th?.type) {
case 'real':
return th.pts
case 'bust':
return `не хватило ${th.remaining === 0 ? 'двойного' : `${th.remaining} очка`}`
case 'finish':
return ''
default:
return '0'
}
})()}
</p>
</div>
)
})}
</Player>
))
: null}
{winner !== undefined ? (
<Player idx={winner} player={players[winner]}>
🎉 Игрок {winner + 1} выиграл!
</Player>
) : null}
</div> </div>
) )
} }
function Player({
player,
children,
idx,
}: {
player: Player
children: ReactNode
idx: number
}) {
const cidx = useGame(game => game.currentPlayerIdx)
return (
<Card key={idx} className={cidx === idx ? "bg-neutral-300" : ""}>
<CardHeader>
<CardTitle>Игрок #{idx + 1}</CardTitle>
<CardDescription>Всего очков: {player.pts}</CardDescription>
</CardHeader>
<CardContent>{children}</CardContent>
</Card>
)
}

View file

@ -1,26 +1,26 @@
import * as React from "react" import * as React from 'react'
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const badgeVariants = cva( const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{ {
variants: { variants: {
variant: { variant: {
default: default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary: secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive: destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: "text-foreground", outline: 'text-foreground',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
}, },
} },
) )
export interface BadgeProps export interface BadgeProps

View file

@ -1,36 +1,36 @@
import * as React from "react" import * as React from 'react'
import { Slot } from "@radix-ui/react-slot" import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90", 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground", 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80", 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: 'hover:bg-accent hover:text-accent-foreground',
link: "text-primary underline-offset-4 hover:underline", link: 'text-primary underline-offset-4 hover:underline',
}, },
size: { size: {
default: "h-10 px-4 py-2", default: 'h-10 px-4 py-2',
sm: "h-9 rounded-md px-3", sm: 'h-9 rounded-md px-3',
lg: "h-11 rounded-md px-8", lg: 'h-11 rounded-md px-8',
icon: "h-10 w-10", icon: 'h-10 w-10',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
size: "default", size: 'default',
}, },
} },
) )
export interface ButtonProps export interface ButtonProps
@ -41,7 +41,7 @@ export interface ButtonProps
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : 'button'
return ( return (
<Comp <Comp
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
@ -49,8 +49,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{...props} {...props}
/> />
) )
} },
) )
Button.displayName = "Button" Button.displayName = 'Button'
export { Button, buttonVariants } export { Button, buttonVariants }

View file

@ -1,6 +1,6 @@
import * as React from "react" import * as React from 'react'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const Card = React.forwardRef< const Card = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@ -9,13 +9,13 @@ const Card = React.forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm", 'rounded-lg border bg-card text-card-foreground shadow-sm',
className className,
)} )}
{...props} {...props}
/> />
)) ))
Card.displayName = "Card" Card.displayName = 'Card'
const CardHeader = React.forwardRef< const CardHeader = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@ -23,11 +23,11 @@ const CardHeader = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)} className={cn('flex flex-col space-y-1.5 p-6', className)}
{...props} {...props}
/> />
)) ))
CardHeader.displayName = "CardHeader" CardHeader.displayName = 'CardHeader'
const CardTitle = React.forwardRef< const CardTitle = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
@ -36,13 +36,13 @@ const CardTitle = React.forwardRef<
<h3 <h3
ref={ref} ref={ref}
className={cn( className={cn(
"text-2xl font-semibold leading-none tracking-tight", 'text-2xl font-semibold leading-none tracking-tight',
className className,
)} )}
{...props} {...props}
/> />
)) ))
CardTitle.displayName = "CardTitle" CardTitle.displayName = 'CardTitle'
const CardDescription = React.forwardRef< const CardDescription = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
@ -50,19 +50,19 @@ const CardDescription = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<p <p
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn('text-sm text-muted-foreground', className)}
{...props} {...props}
/> />
)) ))
CardDescription.displayName = "CardDescription" CardDescription.displayName = 'CardDescription'
const CardContent = React.forwardRef< const CardContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
)) ))
CardContent.displayName = "CardContent" CardContent.displayName = 'CardContent'
const CardFooter = React.forwardRef< const CardFooter = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@ -70,10 +70,10 @@ const CardFooter = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn("flex items-center p-6 pt-0", className)} className={cn('flex items-center p-6 pt-0', className)}
{...props} {...props}
/> />
)) ))
CardFooter.displayName = "CardFooter" CardFooter.displayName = 'CardFooter'
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View file

@ -1,6 +1,6 @@
import * as React from "react" import * as React from 'react'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
export interface InputProps export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {} extends React.InputHTMLAttributes<HTMLInputElement> {}
@ -11,15 +11,15 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input <input
type={type} type={type}
className={cn( className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className className,
)} )}
ref={ref} ref={ref}
{...props} {...props}
/> />
) )
} },
) )
Input.displayName = "Input" Input.displayName = 'Input'
export { Input } export { Input }

View file

@ -1,11 +1,11 @@
import * as React from "react" import * as React from 'react'
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from '@radix-ui/react-label'
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const labelVariants = cva( const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
) )
const Label = React.forwardRef< const Label = React.forwardRef<

View file

@ -1,76 +1,76 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 222.2 84% 4.9%; --foreground: 222.2 84% 4.9%;
--card: 0 0% 100%; --card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%; --card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%; --popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%; --primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%; --primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%; --secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%; --secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%; --muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%; --muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%; --accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%; --accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%; --border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%; --ring: 222.2 84% 4.9%;
--radius: 0.5rem; --radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
} }
@layer base { .dark {
* { --background: 222.2 84% 4.9%;
@apply border-border; --foreground: 210 40% 98%;
}
body { --card: 222.2 84% 4.9%;
@apply bg-background text-foreground; --card-foreground: 210 40% 98%;
}
} --popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

77
src/lib/math.ts Normal file
View file

@ -0,0 +1,77 @@
import React from 'react'
/* 9 27 45 63 81 99 117 135 153 171 189 207 225 243 261 279 297 315 333 351 */
const ptSlice = [
[1, 153, 171],
[2, 27, 45],
[3, 351, 360],
[3, 0, 9],
[4, 117, 135],
[5, 189, 207],
[6, 81, 99],
[7, 315, 333],
[8, 279, 297],
[9, 225, 243],
[10, 63, 81],
[11, 261, 279],
[12, 207, 225],
[13, 99, 117],
[14, 243, 261],
[15, 45, 63],
[16, 297, 315],
[17, 9, 27],
[18, 135, 153],
[19, 333, 351],
[20, 171, 189],
]
/* Проверка дистанции */
/* 3% 7.5% 44% 47% 71,5 75,5 */
const prRanges = [
[0, 50, 0.0, 3.0],
[0, 25, 3.0, 7.5],
[1, 0, 7.5, 38.0],
[3, 0, 38.0, 47.3],
[1, 0, 47.3, 65.2],
[2, 0, 65.2, 75.5],
[0, 0, 75.5, 150.0],
]
export function getPoints(e: React.MouseEvent<HTMLImageElement>): {
points: number
double: boolean
} {
let points = 0
let double = false
let lims = e.currentTarget.getBoundingClientRect()
let cxval = e.clientX - lims.left - lims.width / 2
let cyval = e.clientY - lims.top - lims.height / 2
let angleval = (Math.atan2(cxval, cyval) * 180) / Math.PI
if (angleval < 0) angleval += 360
let distval = Math.sqrt(cxval * cxval + cyval * cyval)
distval = (distval / (lims.width / 2)) * 100 /* in prercent */
for (let index = 0; index < ptSlice.length; index++) {
if (angleval > ptSlice[index][1] && angleval <= ptSlice[index][2]) {
points = ptSlice[index][0]
break
}
}
for (let index = 0; index < prRanges.length; index++) {
if (distval > prRanges[index][2] && distval <= prRanges[index][3]) {
points = points * prRanges[index][0] + prRanges[index][1]
if (prRanges[index][0] == 2) {
double = true
} else {
double = false
}
break
}
}
return { points, double }
}

View file

@ -1,5 +1,5 @@
import { type ClassValue, clsx } from "clsx" import { type ClassValue, clsx } from 'clsx'
import { twMerge } from "tailwind-merge" import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))

View file

@ -2,7 +2,7 @@ import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App.tsx' import App from './App.tsx'
import './index.css' import './index.css'
import('eruda').then((eru) => eru.default.init())
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
<App /> <App />

View file

@ -1,80 +1,80 @@
import type { Config } from "tailwindcss" import type { Config } from 'tailwindcss'
const config = { const config = {
darkMode: ["class"], darkMode: ['class'],
content: [ content: [
'./pages/**/*.{ts,tsx}', './pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}', './components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}', './app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}', './src/**/*.{ts,tsx}',
], ],
prefix: "", prefix: '',
theme: { theme: {
container: { container: {
center: true, center: true,
padding: "2rem", padding: '2rem',
screens: { screens: {
"2xl": "1400px", '2xl': '1400px',
}, },
}, },
extend: { extend: {
colors: { colors: {
border: "hsl(var(--border))", border: 'hsl(var(--border))',
input: "hsl(var(--input))", input: 'hsl(var(--input))',
ring: "hsl(var(--ring))", ring: 'hsl(var(--ring))',
background: "hsl(var(--background))", background: 'hsl(var(--background))',
foreground: "hsl(var(--foreground))", foreground: 'hsl(var(--foreground))',
primary: { primary: {
DEFAULT: "hsl(var(--primary))", DEFAULT: 'hsl(var(--primary))',
foreground: "hsl(var(--primary-foreground))", foreground: 'hsl(var(--primary-foreground))',
}, },
secondary: { secondary: {
DEFAULT: "hsl(var(--secondary))", DEFAULT: 'hsl(var(--secondary))',
foreground: "hsl(var(--secondary-foreground))", foreground: 'hsl(var(--secondary-foreground))',
}, },
destructive: { destructive: {
DEFAULT: "hsl(var(--destructive))", DEFAULT: 'hsl(var(--destructive))',
foreground: "hsl(var(--destructive-foreground))", foreground: 'hsl(var(--destructive-foreground))',
}, },
muted: { muted: {
DEFAULT: "hsl(var(--muted))", DEFAULT: 'hsl(var(--muted))',
foreground: "hsl(var(--muted-foreground))", foreground: 'hsl(var(--muted-foreground))',
}, },
accent: { accent: {
DEFAULT: "hsl(var(--accent))", DEFAULT: 'hsl(var(--accent))',
foreground: "hsl(var(--accent-foreground))", foreground: 'hsl(var(--accent-foreground))',
}, },
popover: { popover: {
DEFAULT: "hsl(var(--popover))", DEFAULT: 'hsl(var(--popover))',
foreground: "hsl(var(--popover-foreground))", foreground: 'hsl(var(--popover-foreground))',
}, },
card: { card: {
DEFAULT: "hsl(var(--card))", DEFAULT: 'hsl(var(--card))',
foreground: "hsl(var(--card-foreground))", foreground: 'hsl(var(--card-foreground))',
}, },
}, },
borderRadius: { borderRadius: {
lg: "var(--radius)", lg: 'var(--radius)',
md: "calc(var(--radius) - 2px)", md: 'calc(var(--radius) - 2px)',
sm: "calc(var(--radius) - 4px)", sm: 'calc(var(--radius) - 4px)',
}, },
keyframes: { keyframes: {
"accordion-down": { 'accordion-down': {
from: { height: "0" }, from: { height: '0' },
to: { height: "var(--radix-accordion-content-height)" }, to: { height: 'var(--radix-accordion-content-height)' },
}, },
"accordion-up": { 'accordion-up': {
from: { height: "var(--radix-accordion-content-height)" }, from: { height: 'var(--radix-accordion-content-height)' },
to: { height: "0" }, to: { height: '0' },
}, },
}, },
animation: { animation: {
"accordion-down": "accordion-down 0.2s ease-out", 'accordion-down': 'accordion-down 0.2s ease-out',
"accordion-up": "accordion-up 0.2s ease-out", 'accordion-up': 'accordion-up 0.2s ease-out',
}, },
}, },
}, },
plugins: [require("tailwindcss-animate")], plugins: [require('tailwindcss-animate')],
} satisfies Config } satisfies Config
export default config export default config

View file

@ -1,12 +1,10 @@
{ {
"files": [], "files": [],
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": [ "@/*": ["./src/*"]
"./src/*" }
]
},
}, },
"references": [ "references": [
{ {

View file

@ -9,7 +9,7 @@ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
// @ts-ignore // @ts-ignore
"@": path.resolve(__dirname, "./src"), '@': path.resolve(__dirname, './src'),
}, },
}, },
}) })