Skip to content

Commit

Permalink
Gravity; soft drop; lock reset
Browse files Browse the repository at this point in the history
  • Loading branch information
ivanjermakov committed Nov 4, 2024
1 parent 99b4da1 commit 978b8a7
Showing 1 changed file with 120 additions and 45 deletions.
165 changes: 120 additions & 45 deletions src/component/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,17 @@ export type Button = {
export type State = {
board: Board
activePiece?: ActivePiece
truePieceY?: number
frameDropped?: number
lockResets: number
queue: Queue
queueIndex: number
holdPiece?: number
holdAvailable: boolean
}

export const dt = 1 / 60

export const piecesDescription: PieceDescription[] = [
{
// I piece
Expand Down Expand Up @@ -171,7 +176,7 @@ export const input: Input = {
hold: createButton()
}

export const gameConfig = {
export const config = {
boardSize: vec(10, 20),
blockScreenSize: vec(40, 40),
colors: [
Expand All @@ -198,8 +203,17 @@ export const gameConfig = {
hard: 'Space',
hold: 'KeyI'
},
arr: 2,
das: 10
// TODO: fractional rates
handling: {
arr: 2,
das: 10,
sdf: 20
},
gameConfig: {
gravity: 2 / 60,
lockDelay: 30,
lockResetLimit: 15
}
}

export const App: Component = () => {
Expand All @@ -211,6 +225,9 @@ export const App: Component = () => {
const state: State = {
board: [],
activePiece: undefined,
truePieceY: undefined,
frameDropped: undefined,
lockResets: 0,
queue: generateQueue(piecesDescription, piecesDescription.length * 64),
queueIndex: 0,
holdAvailable: true
Expand All @@ -224,15 +241,15 @@ export const App: Component = () => {
const boardToScreen = (v: Vector): Vector => {
const screenSize = vec(canvas.width, canvas.height)
const screenCenter = screenSize.scale(0.5)
const blockSize = gameConfig.blockScreenSize
const viewSize = gameConfig.boardSize.add(vec(0, 2)).scale(blockSize)
const blockSize = config.blockScreenSize
const viewSize = config.boardSize.add(vec(0, 2)).scale(blockSize)
const viewCenter = viewSize.scale(0.5)
const res = v.add(vec(0.5, 0.5)).scale(blockSize).add(viewCenter.negate()).scale(vec(1, -1)).add(screenCenter)
return vec(Math.floor(res.x), Math.floor(res.y))
}

const drawBlock = (pos: Vector, opts: DrawOptions): void => {
ctx.rect(boardToScreen(pos), gameConfig.blockScreenSize, { ...opts, lineWidth: gameConfig.blockLineWidth })
ctx.rect(boardToScreen(pos), config.blockScreenSize, { ...opts, lineWidth: config.blockLineWidth })
}

const pieceBoardPos = (piece: ActivePiece): Piece => {
Expand All @@ -246,16 +263,16 @@ export const App: Component = () => {
}

const drawBoard = (board: Board): void => {
const stroke = gameConfig.colors[1]
const gridOpts = { fill: gameConfig.colors[0], stroke }
const stroke = config.colors[1]
const gridOpts = { fill: config.colors[0], stroke }

for (let j = 0; j < gameConfig.boardSize.x; j++) {
for (let i = 0; i < Math.max(gameConfig.boardSize.y, board.length); i++) {
for (let j = 0; j < config.boardSize.x; j++) {
for (let i = 0; i < Math.max(config.boardSize.y, board.length); i++) {
const pos = vec(j, i)
if (board.length > i) {
drawBlock(pos, { fill: gameConfig.colors[board[i][j]], stroke: gameConfig.colors[1] })
drawBlock(pos, { fill: config.colors[board[i][j]], stroke: config.colors[1] })
} else {
ctx.rect(boardToScreen(pos), gameConfig.blockScreenSize, gridOpts)
ctx.rect(boardToScreen(pos), config.blockScreenSize, gridOpts)
}
}
}
Expand All @@ -264,13 +281,13 @@ export const App: Component = () => {
const drawQueue = (state: State): void => {
const offset = vec(2.5, 0)
// TODO: won't show pieces when wrapping queue around
state.queue.slice(state.queueIndex, state.queueIndex + gameConfig.visibleQueuePieces).forEach((pieceId, i) => {
state.queue.slice(state.queueIndex, state.queueIndex + config.visibleQueuePieces).forEach(pieceId => {
const height = Math.max(...piecesDescription[pieceId].blocks.map(b => b.y)) + 1
offset.y -= height
const rotationModeOffset = vec(piecesDescription[pieceId].rotationMode === 'normal' ? 0 : -0.5, 0)
drawPiece(
{ pieceId, position: gameConfig.boardSize.add(offset).add(rotationModeOffset), orientation: 0 },
{ fill: gameConfig.colors[pieceId + 3], stroke: gameConfig.colors[1] }
{ pieceId, position: config.boardSize.add(offset).add(rotationModeOffset), orientation: 0 },
{ fill: config.colors[pieceId + 3], stroke: config.colors[1] }
)
offset.y -= 1
})
Expand All @@ -282,8 +299,8 @@ export const App: Component = () => {
const height = Math.max(...piecesDescription[pieceId].blocks.map(b => b.y)) + 1
const rotationModeOffset = piecesDescription[pieceId].rotationMode === 'normal' ? 0 : 0.5
drawPiece(
{ pieceId, position: vec(-3.5 - rotationModeOffset, gameConfig.boardSize.y - height), orientation: 0 },
{ fill: gameConfig.colors[pieceId + 3], stroke: gameConfig.colors[1] }
{ pieceId, position: vec(-3.5 - rotationModeOffset, config.boardSize.y - height), orientation: 0 },
{ fill: config.colors[pieceId + 3], stroke: config.colors[1] }
)
}
}
Expand All @@ -294,20 +311,19 @@ export const App: Component = () => {
position: piece.position,
orientation: piece.orientation
}
while (!collides(state.board, ghost)) {
while (canFell(state.board, ghost)) {
ghost.position = ghost.position.add(vec(0, -1))
}
ghost.position = ghost.position.add(vec(0, 1))
if (piece.position.y === ghost.position.y) return

drawPiece(ghost, { stroke: gameConfig.colors[piece.pieceId + 3] })
drawPiece(ghost, { stroke: config.colors[piece.pieceId + 3] })
}

const insertPiece = (board: Board, piece: ActivePiece): void => {
pieceBoardPos(piece).blocks.forEach(pos => {
const missingLines = 1 + pos.y - board.length
for (let i = 0; i < missingLines; i++) {
board.push(new Array(gameConfig.boardSize.x).fill(0))
board.push(new Array(config.boardSize.x).fill(0))
}
board[pos.y][pos.x] = piece.pieceId + 3
})
Expand All @@ -325,17 +341,24 @@ export const App: Component = () => {
const collides = (board: Board, piece: ActivePiece): boolean => {
const blocks = pieceBoardPos(piece)
return blocks.blocks.some(pos => {
if (pos.x < 0 || pos.x >= gameConfig.boardSize.x || pos.y < 0) return true
if (pos.x < 0 || pos.x >= config.boardSize.x || pos.y < 0) return true
if (pos.y >= board.length) return false
const boardBlock = board[pos.y][pos.x]
return boardBlock > 0
})
}

const canFell = (board: Board, piece: ActivePiece): boolean => {
piece.position.y--
const result = !collides(board, piece)
piece.position.y++
return result
}

const handleKeyboard = (): void => {
const handleKey = (e: KeyboardEvent): void => {
if (e.repeat) return
const result = Object.entries(gameConfig.keyMap).find(([, code]) => code === e.code)
const result = Object.entries(config.keyMap).find(([, code]) => code === e.code)
if (!result) return
const [action] = result
const button = input[action as keyof Input]
Expand Down Expand Up @@ -363,67 +386,97 @@ export const App: Component = () => {
/**
* TODO: support 0F rates
*/
const buttonActiveThisFrame = (button: Button): boolean => {
const buttonFires = (button: Button, startDelay: number, repeatRate: number): boolean => {
if (button.pressed) return true
if (button.pressedFrame) {
const framesDown = engine.frameInfo.id - button.pressedFrame
const framesRepeat = framesDown - gameConfig.das
return framesRepeat >= 0 && framesRepeat % gameConfig.arr === 0
const framesRepeat = framesDown - startDelay
return framesRepeat >= 0 && framesRepeat % repeatRate === 0
}
return false
}

const updatePiece = (): void => {
if (!state.activePiece) throw Error()
const originalPos = state.activePiece.position

if (buttonActiveThisFrame(input.right)) {
const originalPos = state.activePiece.position
const checkPos = () => {
if (collides(state.board, state.activePiece!)) {
state.activePiece!.position = originalPos
} else {
lockReset(state)
}
}
if (buttonFires(input.right, config.handling.das, config.handling.arr)) {
state.activePiece.position = state.activePiece.position.add(vec(1, 0))
checkPos()
}
if (buttonActiveThisFrame(input.left)) {
if (buttonFires(input.left, config.handling.das, config.handling.arr)) {
state.activePiece.position = state.activePiece.position.add(vec(-1, 0))
}
if (collides(state.board, state.activePiece)) {
state.activePiece.position = originalPos
checkPos()
}

const originalOrient = state.activePiece.orientation
const checkOrient = () => {
// TODO: wall kicks
if (collides(state.board, state.activePiece!)) {
state.activePiece!.orientation = originalOrient
} else {
lockReset(state)
}
}
if (input.cw.pressed) {
state.activePiece.orientation = (state.activePiece.orientation + 1) % 4
checkOrient()
}
if (input.ccw.pressed) {
state.activePiece.orientation = (state.activePiece.orientation + 3) % 4
checkOrient()
}
if (input.r180.pressed) {
state.activePiece.orientation = (state.activePiece.orientation + 2) % 4
checkOrient()
}

// TODO: wall kicks
if (collides(state.board, state.activePiece)) {
state.activePiece.orientation = originalOrient
const softDropRepeatRate = Math.floor(1 / (config.gameConfig.gravity * config.handling.sdf))
if (buttonFires(input.soft, 0, softDropRepeatRate)) {
if (canFell(state.board, state.activePiece)) {
state.activePiece.position = state.activePiece.position.add(vec(0, -1))
}
}

if (input.hard.pressed) {
while (!collides(state.board, state.activePiece)) {
while (canFell(state.board, state.activePiece)) {
state.activePiece.position = state.activePiece.position.add(vec(0, -1))
}
// TODO: will ascend if not checked for game over
state.activePiece.position = state.activePiece.position.add(vec(0, 1))
insertPiece(state.board, state.activePiece)
clearLines(state.board)
state.holdAvailable = true
state.activePiece = undefined
lockPiece(state)
}
}

const lockReset = (state: State): void => {
if (state.frameDropped === undefined) return
if (state.lockResets > config.gameConfig.lockResetLimit) return
state.lockResets++
state.frameDropped = undefined
}

const lockPiece = (state: State): void => {
insertPiece(state.board, state.activePiece!)
clearLines(state.board)
state.holdAvailable = true
state.activePiece = undefined
}

const spawnPiece = (state: State, pieceId?: number): void => {
const spawnPos = vec(Math.floor(gameConfig.boardSize.x / 2) - 1, gameConfig.boardSize.y)
const spawnPos = vec(Math.floor(config.boardSize.x / 2) - 1, config.boardSize.y + 1)
if (pieceId !== undefined) {
state.activePiece = { pieceId, position: spawnPos, orientation: 0 }
return
}
pieceId = state.queue[state.queueIndex]
state.activePiece = { pieceId, position: spawnPos, orientation: 0 }
state.truePieceY = state.activePiece.position.y
state.lockResets = 0
state.queueIndex = (state.queueIndex + 1) % state.queue.length
}

Expand All @@ -437,14 +490,36 @@ export const App: Component = () => {
engine.start()
subs.push(
engine.eventDispatcher.beforeUpdate.subscribe(() => {
const currentFrame = engine.frameInfo.id
updateInput()

if (!state.activePiece) {
spawnPiece(state)
}

state.truePieceY! -= config.gameConfig.gravity
const targetY = Math.ceil(state.truePieceY!)
while (canFell(state.board, state.activePiece!) && state.activePiece!.position.y > targetY) {
state.activePiece!.position = state.activePiece!.position.add(vec(0, -1))
}
if (state.activePiece!.position.y !== targetY) {
state.truePieceY = state.activePiece!.position.y
}

updatePiece()

if (state.activePiece) {
if (canFell(state.board, state.activePiece!)) {
state.frameDropped = undefined
} else {
state.frameDropped ??= currentFrame
const framesSinceDrop = currentFrame - state.frameDropped
if (framesSinceDrop >= config.gameConfig.lockDelay) {
lockPiece(state)
}
}
}

if (input.hold.pressed && state.holdAvailable) {
state.holdAvailable = false
if (state.holdPiece) {
Expand All @@ -466,8 +541,8 @@ export const App: Component = () => {
drawHold(state)
if (state.activePiece) {
drawPiece(state.activePiece, {
fill: gameConfig.colors[state.activePiece.pieceId + 3],
stroke: gameConfig.colors[1]
fill: config.colors[state.activePiece.pieceId + 3],
stroke: config.colors[1]
})
drawGhost(state, state.activePiece)
}
Expand Down

0 comments on commit 978b8a7

Please sign in to comment.