this repo has no description

implement t-spins #2

open opened by voigt.tngl.sh targeting main from t-spins
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:6q572hlx7omtsszji5w2fyw3/sh.tangled.repo.pull/3mgkjaripyz22
+160 -20
Diff #0
+1 -1
README.md
··· 26 26 - [x] Make Column/Row count configurable, this allows for way bigger playing fields 27 27 - [x] Add modern multiplayer, where you have to clear the game together 28 28 - [x] Config persistence 29 - - [ ] Toggle T-Spins 29 + - [x] T-Spins 30 30 - [ ] Points for perfect clears 31 31 - [ ] Scoreboard/Leaderboard 32 32 - [ ] Add classic Tetris multiplayer (deleted rows will be a penalty for the opponent)
+18 -11
block.go
··· 7 7 type I_Block interface { 8 8 Init(playerId ff.Peer) I_Block 9 9 GetId() int 10 + GetOffset() (int, int) 11 + GetRotationState() int 10 12 Draw(offsetX, offsetY int) 11 13 DrawPreview(offsetX, offsetY int) 12 14 Move(rows, columns int) 13 15 GetCellPositions() []Position 14 - TryRotateWithKicks(grid *Grid, clockwise bool) bool 16 + TryRotateWithKicks(grid *Grid, clockwise bool) (bool, int) 15 17 FitsInGrid(grid *Grid) bool 16 18 } 17 19 ··· 26 28 cellPosCache [4]Position 27 29 } 28 30 31 + func (b *Block) GetOffset() (int, int) { 32 + return b.RowOffset, b.ColOffset 33 + } 34 + 35 + func (b *Block) GetRotationState() int { 36 + return b.RotationState 37 + } 38 + 29 39 func (b *Block) GetId() int { 30 40 return b.Id 31 41 } ··· 87 97 return true 88 98 } 89 99 90 - // Returns true if rotation was successful, false otherwise 91 - // Adhering to the rules of the SRS system 92 - func (b *Block) TryRotateWithKicks(grid *Grid, clockwise bool) bool { 100 + // TryRotateWithKicks attempts to rotate the block using SRS wall kicks. 101 + // Returns (true, kickIndex) on success, (false, -1) on failure. 102 + func (b *Block) TryRotateWithKicks(grid *Grid, clockwise bool) (bool, int) { 93 103 oldState := b.RotationState 94 104 95 105 if clockwise { ··· 101 111 102 112 kickTests := GetKickTests(b.Id, oldState, newState) 103 113 104 - // Try each kick test in order 105 - for _, offset := range kickTests { 114 + for i, offset := range kickTests { 106 115 b.Move(offset.Row, offset.Column) 107 116 108 117 if b.FitsInGrid(grid) { 109 - // block is rotated and positioned correctly 110 - return true 118 + return true, i 111 119 } 112 120 113 - // kick invalid, undo the offset 114 121 b.Move(-offset.Row, -offset.Column) 115 122 } 116 123 117 - // kicks invalid, undo rotation 124 + // All kicks failed โ€” undo rotation 118 125 if clockwise { 119 126 b.rotateCounterClockwise() 120 127 } else { 121 128 b.rotateClockwise() 122 129 } 123 - return false 130 + return false, -1 124 131 } 125 132 126 133 func (b *Block) rotateClockwise() {
+124 -5
game.go
··· 211 211 } 212 212 213 213 func (g *Game) HandleLocking(player *Player) { 214 + tSpin := detectTSpin(&g.Grid, player) // must be before LockBlockToGrid replaces CurrentBlock 214 215 dist, success := player.LockBlockToGrid(&g.Grid) 215 216 if !success { 216 217 g.GameOver = true 217 218 g.Screen.SetScreen(ScreenResult) 218 219 return 219 220 } 220 - g.handleScoring(dist) 221 + g.handleScoring(dist, tSpin) 221 222 } 222 223 223 - func (g *Game) handleScoring(hardDropDistance int) { 224 - // Add hard drop points if enabled 224 + func (g *Game) handleScoring(hardDropDistance int, tSpin TSpinType) { 225 225 if CONFIG.HardDropPointsEnabled && hardDropDistance > 0 { 226 226 g.Score += hardDropDistance * 2 227 227 } 228 228 229 - // Clear rows and update score (shared for all players) 230 229 rowsCleared := g.Grid.ClearFullRows() 231 - if rowsCleared > 0 { 230 + if tSpin != TSpinNone { 231 + g.scoreTSpin(tSpin, rowsCleared) 232 + if rowsCleared > 0 { 233 + g.Lines += rowsCleared 234 + if CONFIG.Level != 0 { 235 + g.UpdateLevel() 236 + } 237 + } 238 + } else if rowsCleared > 0 { 232 239 g.Lines += rowsCleared 233 240 g.UpdateScore(rowsCleared, 0) 234 241 if CONFIG.Level != 0 { ··· 237 244 } 238 245 } 239 246 247 + // TSpinType distinguishes no T-Spin, Mini T-Spin, and full T-Spin. 248 + type TSpinType int 249 + 250 + const ( 251 + TSpinNone TSpinType = iota 252 + TSpinMini 253 + TSpinFull 254 + ) 255 + 256 + // detectTSpin checks whether the player's current block qualifies as a T-Spin 257 + // at the moment of locking. Must be called before LockBlockToGrid. 258 + // 259 + // Detection rules (Tetris Guideline): 260 + // 1. Piece must be a T-block (id 6) 261 + // 2. Last player action must have been a rotation 262 + // 3. At least 3 of the 4 diagonal corners around the T's centre are occupied 263 + // 264 + // Mini vs Full: 265 + // - If the last kick used was index 4 (the ยฑ2-row SRS kick) โ†’ Full T-Spin 266 + // - Otherwise: both "front" corners filled โ†’ Full; only one โ†’ Mini 267 + func detectTSpin(grid *Grid, player *Player) TSpinType { 268 + if !CONFIG.TSpinsEnabled { 269 + return TSpinNone 270 + } 271 + if player.CurrentBlock.GetId() != 6 { 272 + return TSpinNone 273 + } 274 + if player.lastKickIndex == -1 { 275 + return TSpinNone 276 + } 277 + 278 + row, col := player.CurrentBlock.GetOffset() 279 + centerRow, centerCol := row+1, col+1 280 + 281 + // Count filled diagonal corners (wall/floor counts as filled) 282 + corners := [4][2]int{ 283 + {centerRow - 1, centerCol - 1}, 284 + {centerRow - 1, centerCol + 1}, 285 + {centerRow + 1, centerCol - 1}, 286 + {centerRow + 1, centerCol + 1}, 287 + } 288 + filled := 0 289 + for _, c := range corners { 290 + if grid.IsCellOutside(c[0], c[1]) || !grid.IsCellEmpty(c[0], c[1]) { 291 + filled++ 292 + } 293 + } 294 + if filled < 3 { 295 + return TSpinNone 296 + } 297 + 298 + // SRS kick index 4 always upgrades to a full T-Spin 299 + if player.lastKickIndex == 4 { 300 + return TSpinFull 301 + } 302 + 303 + // Front corners by rotation state (the side the T's prong faces) 304 + rotState := player.CurrentBlock.GetRotationState() 305 + var frontCorners [2][2]int 306 + switch rotState { 307 + case 0: // prong up 308 + frontCorners = [2][2]int{{centerRow - 1, centerCol - 1}, {centerRow - 1, centerCol + 1}} 309 + case 1: // prong right 310 + frontCorners = [2][2]int{{centerRow - 1, centerCol + 1}, {centerRow + 1, centerCol + 1}} 311 + case 2: // prong down 312 + frontCorners = [2][2]int{{centerRow + 1, centerCol - 1}, {centerRow + 1, centerCol + 1}} 313 + case 3: // prong left 314 + frontCorners = [2][2]int{{centerRow - 1, centerCol - 1}, {centerRow + 1, centerCol - 1}} 315 + } 316 + 317 + filledFront := 0 318 + for _, c := range frontCorners { 319 + if grid.IsCellOutside(c[0], c[1]) || !grid.IsCellEmpty(c[0], c[1]) { 320 + filledFront++ 321 + } 322 + } 323 + 324 + if filledFront == 2 { 325 + return TSpinFull 326 + } 327 + return TSpinMini 328 + } 329 + 330 + // scoreTSpin awards points for a T-Spin according to the Tetris Guideline. 331 + // Base scores: Mini(0)=100, Mini Single=200, Full(0)=400, 332 + // Full Single=800, Full Double=1200, Full Triple=1600. All ร— (level+1). 333 + func (g *Game) scoreTSpin(tSpin TSpinType, lines int) { 334 + var base int 335 + if tSpin == TSpinMini { 336 + switch lines { 337 + case 0: 338 + base = 100 339 + case 1: 340 + base = 200 341 + case 2: 342 + base = 400 343 + } 344 + } else { // TSpinFull 345 + switch lines { 346 + case 0: 347 + base = 400 348 + case 1: 349 + base = 800 350 + case 2: 351 + base = 1200 352 + case 3: 353 + base = 1600 354 + } 355 + } 356 + g.Score += base * (g.Level + 1) 357 + } 358 + 240 359 func (g *Game) UpdateScore(linesCleared, movedDownPoints int) { 241 360 switch linesCleared { 242 361 case 1:
+17 -3
player.go
··· 29 29 hardDropDistance int // Cells moved by hard drop (for scoring) 30 30 softDropCells int // Cells moved by soft drop this frame 31 31 32 + // T-Spin detection: kick index of last successful rotation, -1 if last action was not a rotation 33 + lastKickIndex int 34 + 32 35 gravityAccum float32 33 36 34 37 // blogBag contains available blocks ··· 38 41 39 42 func newPlayer(peer ff.Peer) *Player { 40 43 return &Player{ 41 - ID: peer, 42 - canHold: true, 44 + ID: peer, 45 + canHold: true, 46 + lastKickIndex: -1, 43 47 } 44 48 } 45 49 ··· 57 61 distance := p.computeDropDistance(grid) 58 62 if distance > 0 { 59 63 p.CurrentBlock.Move(distance, 0) 64 + p.justHardDropped = true // Signal immediate locking 65 + p.hardDropDistance = distance 66 + p.lastKickIndex = -1 60 67 } 61 68 p.justHardDropped = true // Signal immediate locking (even if already at bottom) 62 69 p.hardDropDistance = distance ··· 252 259 p.justHardDropped = true 253 260 } else { 254 261 p.softDropCells++ 262 + p.lastKickIndex = -1 255 263 } 256 264 return 257 265 } ··· 274 282 p.justHardDropped = true 275 283 } else { 276 284 p.softDropCells++ 285 + p.lastKickIndex = -1 277 286 } 278 287 p.softDropARRCounter = 0 279 288 } ··· 284 293 p.CurrentBlock.Move(0, dir) 285 294 if !p.CurrentBlock.FitsInGrid(grid) { 286 295 p.CurrentBlock.Move(0, -dir) 296 + } else { 297 + p.lastKickIndex = -1 287 298 } 288 299 } 289 300 ··· 344 355 345 356 // rotate attempts to rotate the block 346 357 func (p *Player) rotate(grid *Grid, clockwise bool) { 347 - p.CurrentBlock.TryRotateWithKicks(grid, clockwise) 358 + if ok, kickIdx := p.CurrentBlock.TryRotateWithKicks(grid, clockwise); ok { 359 + p.lastKickIndex = kickIdx 360 + } 348 361 } 349 362 350 363 // holdBlock swaps current block with held block ··· 406 419 p.NextBlocks[len(p.NextBlocks)-1] = p.GetNewBlock() 407 420 p.canHold = canHold 408 421 p.gravityAccum = 0 422 + p.lastKickIndex = -1 409 423 } 410 424 411 425 // Draw renders the player's current block

History

1 round 0 comments
sign up or login to add to the discussion
voigt.tngl.sh submitted #0
1 commit
expand
implement t-spins
merge conflicts detected
expand
  • README.md:26
  • block.go:7
  • game.go:211
  • player.go:29
expand 0 comments