+160
-20
Diff
round #0
+1
-1
README.md
+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
+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
+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
+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
voigt.tngl.sh
submitted
#0
1 commit
expand
collapse
implement t-spins
merge conflicts detected
expand
collapse
expand
collapse
- README.md:26
- block.go:7
- game.go:211
- player.go:29