Compare commits

..

4 Commits

Author SHA1 Message Date
Joe Zeng
1254de15d5 Refactored the "Ligne" modes. (#21)
* Added Ligne C89, now known as Marathon C89.
* Refactored all the Ligne modes to no longer use the "Ligne" name.

Ligne -> Race 40
Ligne A1 -> Marathon AX4
Ligne C89 -> Marathon C89
2019-06-16 22:24:06 -04:00
Joe Zeng
5c5ffc6887 Added Big Mode as a piece type. (#20)
Survival A3 and Phantom Mania 2 are now in their fully complete glory! :D

Implements #13.
2019-06-16 22:16:09 -04:00
Joe Zeng
5131061e42 Added conventions for code submission / review.
Also, coding conventions didn't deserve to go first, so I reordered the sections a little bit.
2019-06-15 23:28:53 -04:00
Joe Z
209e60e82e Fixed a roll and section COOL bug in Marathon A3. 2019-06-09 18:37:23 -04:00
22 changed files with 540 additions and 145 deletions

View File

@@ -1,7 +1,61 @@
Contributor's License Agreement
-------------------------------
By contributing source code or other assets (e.g. music, artwork, graphics) to Cambridge, through a pull request or otherwise, you provide me with a non-exclusive, royalty-free, worldwide, perpetual license to use, copy, modify, distribute, sublicense, publicly perform, and create derivative works of the assets for any purpose.
You also waive all moral rights to your contributions insofar as they are used in the Cambridge repository or in any code or works deriving therefrom.
(Notwithstanding the above clause, I will still make my best effort to provide sufficient attribution to all contributions. At the very least you'll get documentation of your contributions under SOURCES, and probably a special place in the credit roll as well.)
Git / Repo conventions
----------------------
In general, use `kebab-case` for branch names. Also, make sure they're concise and descriptive - like 2 or 3 words is usually good.
```
* badbeef (badBranchName) This branch name is bad.
| * defaced (another_bad_branch_name) This branch name is also bad because it uses snake case.
|/
| * deadcab (generic) This branch name isn't very descriptive.
|/
| * bac0040 (this-long-winded-branch-name-that-could-be-its-own-commit-message) Self-explanatory.
|/
| * 600db01 (good-branch-name) This branch name is good.
|/
* 0000420 (HEAD -> master, tag: v0.6.9) This is a sexy root commit.
```
The top line of a commit message should generally be one full sentence long, without too many subordinate clauses. Don't sweat 50/72, but try not go over about 100 characters either.
* If the message starts with a verb, it should be written in the past tense, as a description of what the commit _did_ to the commit tree. (e.g. _Made_ a change, _Fixed_ a bug, _Added_ a feature)
* Alternatively, include a description (in the present tense) of what is now true thanks to this commit. (e.g. "The Puyo Puyo mode can now support up to 50 players.")
```
* 800000d (message-too-long) Made multiplayer stuff play well with the new v0.2.5 server by fixing a problem the client was having with sending multiple 4-KB packets within 2 milliseconds of each other.
| * defaced (not-descriptive-enough) Fixed stuff.
|/
| * bad0003 (present-tense) Lengthens the retry period of the server connection to 15 seconds.
|/
| * bad0004 (imperative-mood) Force the credit roll to end after 67 seconds if no input is detected.
|/
| * 600d001 (good-commit-summary) Made the Jenny Marathon mode not top out randomly at level 600.
| * 600d002 (also-good) Backgrounds don't suck anymore.
|/
* 1234567 (HEAD -> master, tag: v0.4.2) Updated docs in preparation for a new release.
```
When making pull requests, always include:
* A title that works well as a commit title, since that's what's going to appear when it's merged.
* A full description of the problem that the pull request solves or the feature that it implements.
* If the whole purpose of the pull request is to resolve a particular issue and nothing else, "Fixes #[issue number]" counts as a full description. Otherwise if there's anything else in the pull request, make a short note of "also [did this other thing]".
Coding conventions Coding conventions
------------------ ------------------
* Use tabs to indent, spaces to align. Use tabs to indent, spaces to align.
* Specifically, spaces should not appear at the beginning of a line, and tabs should not appear _except_ at the beginning of a line. * Specifically, spaces should not appear at the beginning of a line, and tabs should not appear _except_ at the beginning of a line.
* The sole exception is in a multiline `if` statement; the initial `if` should have four spaces before it to align it with an `elseif` on the next line. For example: * The sole exception is in a multiline `if` statement; the initial `if` should have four spaces before it to align it with an `elseif` on the next line. For example:
@@ -12,14 +66,14 @@ Coding conventions
else return 6 end else return 6 end
``` ```
* Comments at the end of lines of code must be one line long. Multi-line comments must appear in their own block. Comments at the end of lines of code must be one line long. Multi-line comments must appear in their own block.
```lua ```lua
if self.piece:isDropBlocked(self.grid) then if not self.piece:isDropBlocked(self.grid) then
-- this is a comment that appears in a block of its own, separate from any code -- this is a comment that appears in a block of its own, separate from any code
-- consecutive multiline comments must have the same indentation level and -- consecutive multiline comments must have the same indentation level and
-- not appear next on the same line as actual code -- not appear next on the same line as actual code
self.drop_bonus = math.min(self.drop_bonus - 1, 0) -- comments at the end of a line must stay on that line self.drop_bonus = 0 -- comments at the end of a line must stay on that line
else else
if piece_dy >= 1 then -- basically if piece_dy >= 1 then -- basically
self.drop_bonus = self.drop_bonus + piece_dy * 20 -- this sort of self.drop_bonus = self.drop_bonus + piece_dy * 20 -- this sort of
@@ -28,25 +82,17 @@ Coding conventions
end -- unacceptable end -- unacceptable
``` ```
* Use `snake_case` for variables, `camelCase` for functions. Use `snake_case` for variables, `camelCase` for functions.
```lua ```lua
function MyGameMode:on_activate_bleep_bloop() function MyGameMode:on_activate_bleep_bloop()
-- no, bad, use "onActivateBleepBloop" -- no, bad, use "onActivateBleepBloop"
local bleepBloopFrames = 240 local bleepBloopFrames = 240
-- this is also bad, use "bleep_bloop_frames" -- this is also bad, use "bleep_bloop_frames"
local bleep_bloop_bonus = self.lock_delay * 150 local bleep_bloop_bonus = self.lock_delay * 150
self.bleepBloopSubscore = self.bleepBloopSubscore + bleep_bloop_bonus self.bleepBloopSubscore = self.bleepBloopSubscore + bleep_bloop_bonus
-- member variables are also variables, this should be "bleep_bloop_subscore" -- this should be self."bleep_bloop_subscore", member variables are also variables
end end
``` ```
Contributor's License Agreement
-------------------------------
By contributing source code or other assets (e.g. music, artwork, graphics) to Cambridge, through a pull request or otherwise, you provide me with a non-exclusive, royalty-free, worldwide, perpetual license to use, copy, modify, distribute, sublicense, publicly perform, and create derivative works of the assets for any purpose.
You also waive all moral rights to your contributions insofar as they are used in the Cambridge repository or in any code or works deriving therefrom.
(Notwithstanding the above clause, I will still make my best effort to provide sufficient attribution to all contributions. At the very least you'll get documentation of your contributions under SOURCES, and probably a special place in the credit roll as well.)

View File

@@ -1,6 +1,8 @@
function love.conf(t) function love.conf(t)
t.identity = "cambridge" t.identity = "cambridge"
t.console = true
t.window.title = "Cambridge" t.window.title = "Cambridge"
t.window.width = 640 t.window.width = 640
t.window.height = 480 t.window.height = 480

View File

@@ -1,13 +1,26 @@
Game modes Game modes
========== ==========
There are several classes of game modes. There are several classes of game modes. The modes that originate from other games are organized by suffix:
* The "C" series stand for "Classic" games, games that were produced before around 1992-1993 and generally have no wallkicks or lock delay.
* C84 - The original version from the Electronika 60.
* C88 - Sega Tetris.
* C89 - Nintendo / NES Tetris.
* The "A" series stand for "Arika" games, or games in the Tetris the Grand Master series.
* A1 - Tetris The Grand Master (the original from 1998).
* A2 - Tetris The Absolute The Grand Master 2 PLUS.
* A3 - Tetris The Grand Master 3 Terror-Instinct.
* AX - Tetris The Grand Master ACE (X for Xbox).
* The "G" series stand for "Guideline" games, or games that follow the Tetris Guideline.
* GF - Tetris Friends (2007-2019)
* GJ - Tetris Online Japan (2005-2011)
* N stands for Nullpomino, only used for Phantom Mania N.
MARATHON MARATHON
-------- --------
Modes in which the goal is to play as well as possible over a limited game interval, to ultimately achieve the title of Grand Master. Modes in which the goal is to play as well as possible over a limited game interval.
* **MARATHON 2020**: 2020 levels of pure pain. Can you make it all the way? * **MARATHON 2020**: 2020 levels of pure pain. Can you make it all the way?
@@ -15,6 +28,8 @@ From other games:
* **MARATHON A1**: Tetris the Grand Master 1. * **MARATHON A1**: Tetris the Grand Master 1.
* **MARATHON A2**: Tetris the Grand Master 2 (TAP Master). * **MARATHON A2**: Tetris the Grand Master 2 (TAP Master).
* **MARATHON A3**: Tetris the Grand Master 3 (no exams). * **MARATHON A3**: Tetris the Grand Master 3 (no exams).
* **MARATHON AX4**: Another mode from TGM Ace.
* **MARATHON C89**: Nintendo NES Tetris. Can you transition and make it to the killscreen?
SURVIVAL SURVIVAL
@@ -30,6 +45,14 @@ From other games:
* **SURVIVAL A3**: Ti Shirase. * **SURVIVAL A3**: Ti Shirase.
RACE
----
Modes with no levels, just a single timed goal.
* **Race 40**: How fast can you clear 40 lines? No limits, no holds barred.
PHANTOM MANIA PHANTOM MANIA
------------- -------------
@@ -40,6 +63,7 @@ Modes where pieces turn invisible as soon as you lock them. One of Cambridge's s
* **Phantom Mania 2**: Phantom Mania but way faster! Can you face a mode where even the garbage and the next preview turn invisible? * **Phantom Mania 2**: Phantom Mania but way faster! Can you face a mode where even the garbage and the next preview turn invisible?
OTHER MODES OTHER MODES
----------- -----------
@@ -48,3 +72,5 @@ OTHER MODES
* **TetrisGram™ Pacer Test**: is a multi-stage piece-placing ability test that progressively gets more difficult as it continues. * **TetrisGram™ Pacer Test**: is a multi-stage piece-placing ability test that progressively gets more difficult as it continues.
* **Interval Training**: 30 seconds per section. 20G. 15 frames of lock delay. How long can you last? * **Interval Training**: 30 seconds per section. 20G. 15 frames of lock delay. How long can you last?
* **Demon Mode**: An original mode from Oshisaure! Can you push through the ever faster levels and not get denied?

View File

@@ -49,6 +49,12 @@ blocks = {
} }
} }
for name, blockset in pairs(blocks) do
for shape, image in pairs(blockset) do
image:setFilter("nearest")
end
end
misc_graphics = { misc_graphics = {
frame = love.graphics.newImage("res/img/frame.png"), frame = love.graphics.newImage("res/img/frame.png"),
ready = love.graphics.newImage("res/img/ready.png"), ready = love.graphics.newImage("res/img/ready.png"),

View File

@@ -15,14 +15,15 @@ game_modes = {
require 'tetris.modes.phantom_mania', require 'tetris.modes.phantom_mania',
require 'tetris.modes.phantom_mania2', require 'tetris.modes.phantom_mania2',
require 'tetris.modes.phantom_mania_n', require 'tetris.modes.phantom_mania_n',
require 'tetris.modes.ligne', require 'tetris.modes.race_40',
require 'tetris.modes.marathon_a1', require 'tetris.modes.marathon_a1',
require 'tetris.modes.marathon_a2', require 'tetris.modes.marathon_a2',
require 'tetris.modes.marathon_a3', require 'tetris.modes.marathon_a3',
require 'tetris.modes.marathon_ax4',
require 'tetris.modes.marathon_c89',
require 'tetris.modes.survival_a1', require 'tetris.modes.survival_a1',
require 'tetris.modes.survival_a2', require 'tetris.modes.survival_a2',
require 'tetris.modes.survival_a3', require 'tetris.modes.survival_a3',
require 'tetris.modes.marathon_l1',
} }
rulesets = { rulesets = {

View File

@@ -38,6 +38,10 @@ function Grid:isRowFull(row)
end end
function Grid:canPlacePiece(piece) function Grid:canPlacePiece(piece)
if piece.big then
return self:canPlaceBigPiece(piece)
end
local offsets = piece:getBlockOffsets() local offsets = piece:getBlockOffsets()
for index, offset in pairs(offsets) do for index, offset in pairs(offsets) do
local x = piece.position.x + offset.x local x = piece.position.x + offset.x
@@ -49,7 +53,29 @@ function Grid:canPlacePiece(piece)
return true return true
end end
function Grid:canPlaceBigPiece(piece)
local offsets = piece:getBlockOffsets()
for index, offset in pairs(offsets) do
local x = piece.position.x + offset.x
local y = piece.position.y + offset.y
if x >= 5 or x < 0 or y >= 12 or y < 0 or
self.grid[y * 2 + 1][x * 2 + 1] ~= empty or
self.grid[y * 2 + 1][x * 2 + 2] ~= empty or
self.grid[y * 2 + 2][x * 2 + 1] ~= empty or
self.grid[y * 2 + 2][x * 2 + 2] ~= empty
then
return false
end
end
return true
end
function Grid:canPlacePieceInVisibleGrid(piece) function Grid:canPlacePieceInVisibleGrid(piece)
if piece.big then
return self:canPlaceBigPiece(piece)
-- forget canPlaceBigPieceInVisibleGrid for now
end
local offsets = piece:getBlockOffsets() local offsets = piece:getBlockOffsets()
for index, offset in pairs(offsets) do for index, offset in pairs(offsets) do
local x = piece.position.x + offset.x local x = piece.position.x + offset.x
@@ -116,6 +142,10 @@ function Grid:copyBottomRow()
end end
function Grid:applyPiece(piece) function Grid:applyPiece(piece)
if piece.big then
self:applyBigPiece(piece)
return
end
offsets = piece:getBlockOffsets() offsets = piece:getBlockOffsets()
for index, offset in pairs(offsets) do for index, offset in pairs(offsets) do
x = piece.position.x + offset.x x = piece.position.x + offset.x
@@ -127,6 +157,22 @@ function Grid:applyPiece(piece)
end end
end end
function Grid:applyBigPiece(piece)
offsets = piece:getBlockOffsets()
for index, offset in pairs(offsets) do
x = piece.position.x + offset.x
y = piece.position.y + offset.y
for a = 1, 2 do
for b = 1, 2 do
self.grid[y*2+a][x*2+b] = {
skin = piece.skin,
colour = piece.shape
}
end
end
end
end
function Grid:update() function Grid:update()
for y = 1, 24 do for y = 1, 24 do
for x = 1, 10 do for x = 1, 10 do
@@ -148,6 +194,7 @@ function Grid:draw()
love.graphics.setColor(0.5, 0.5, 0.5, 1) love.graphics.setColor(0.5, 0.5, 0.5, 1)
love.graphics.draw(blocks[self.grid[y][x].skin][self.grid[y][x].colour], 48+x*16, y*16) love.graphics.draw(blocks[self.grid[y][x].skin][self.grid[y][x].colour], 48+x*16, y*16)
end end
if self.grid[y][x].skin ~= "bone" then
love.graphics.setColor(0.8, 0.8, 0.8, 1) love.graphics.setColor(0.8, 0.8, 0.8, 1)
love.graphics.setLineWidth(1) love.graphics.setLineWidth(1)
if y > 1 and self.grid[y-1][x] == empty then if y > 1 and self.grid[y-1][x] == empty then
@@ -166,6 +213,7 @@ function Grid:draw()
end end
end end
end end
end
function Grid:drawInvisible(opacity_function, garbage_opacity_function) function Grid:drawInvisible(opacity_function, garbage_opacity_function)
for y = 1, 24 do for y = 1, 24 do

View File

@@ -2,7 +2,7 @@ local Object = require 'libs.classic'
local Piece = Object:extend() local Piece = Object:extend()
function Piece:new(shape, rotation, position, block_offsets, gravity, lock_delay, skin) function Piece:new(shape, rotation, position, block_offsets, gravity, lock_delay, skin, big)
self.shape = shape self.shape = shape
self.rotation = rotation self.rotation = rotation
self.position = position self.position = position
@@ -12,6 +12,7 @@ function Piece:new(shape, rotation, position, block_offsets, gravity, lock_delay
self.skin = skin self.skin = skin
self.ghost = false self.ghost = false
self.locked = false self.locked = false
self.big = big
end end
-- Functions that return a new piece to test in rotation systems. -- Functions that return a new piece to test in rotation systems.
@@ -20,7 +21,7 @@ function Piece:withOffset(offset)
return Piece( return Piece(
self.shape, self.rotation, self.shape, self.rotation,
{x = self.position.x + offset.x, y = self.position.y + offset.y}, {x = self.position.x + offset.x, y = self.position.y + offset.y},
self.block_offsets, self.gravity, self.lock_delay, self.skin self.block_offsets, self.gravity, self.lock_delay, self.skin, self.big
) )
end end
@@ -30,7 +31,7 @@ function Piece:withRelativeRotation(rot)
while new_rot >= 4 do new_rot = new_rot - 4 end while new_rot >= 4 do new_rot = new_rot - 4 end
return Piece( return Piece(
self.shape, new_rot, self.position, self.shape, new_rot, self.position,
self.block_offsets, self.gravity, self.lock_delay, self.skin self.block_offsets, self.gravity, self.lock_delay, self.skin, self.big
) )
end end
@@ -138,14 +139,25 @@ function Piece:draw(opacity, brightness, grid, partial_das)
love.graphics.setColor(brightness, brightness, brightness, opacity) love.graphics.setColor(brightness, brightness, brightness, opacity)
local offsets = self:getBlockOffsets() local offsets = self:getBlockOffsets()
local gravity_offset = 0 local gravity_offset = 0
if grid ~= nil and not self:isDropBlocked(grid) then --if grid ~= nil and not self:isDropBlocked(grid) then
gravity_offset = self.gravity * 16 -- gravity_offset = self.gravity * 16
end --end
if partial_das == nil then partial_das = 0 end if partial_das == nil then partial_das = 0 end
for index, offset in pairs(offsets) do for index, offset in pairs(offsets) do
local x = self.position.x + offset.x local x = self.position.x + offset.x
local y = self.position.y + offset.y local y = self.position.y + offset.y
love.graphics.draw(blocks[self.skin][self.shape], 64+x*16+partial_das, 16+y*16+gravity_offset) if self.big then
love.graphics.draw(
blocks[self.skin][self.shape],
64+x*32+partial_das*2, 16+y*32+gravity_offset*2,
0, 2, 2
)
else
love.graphics.draw(
blocks[self.skin][self.shape],
64+x*16+partial_das, 16+y*16+gravity_offset
)
end
end end
return false return false
end end

View File

@@ -104,7 +104,7 @@ function DemonModeGame:advanceOneFrame()
if self.clear then if self.clear then
self.roll_frames = self.roll_frames + 1 self.roll_frames = self.roll_frames + 1
if self.roll_frames < 0 then if self.roll_frames < 0 then
return return false
elseif self.roll_frames >= 1337 then elseif self.roll_frames >= 1337 then
self.completed = true self.completed = true
end end

View File

@@ -36,8 +36,10 @@ function GameMode:new()
self.enable_hold = false self.enable_hold = false
self.enable_hard_drop = true self.enable_hard_drop = true
self.next_queue_length = 1 self.next_queue_length = 1
self.additive_gravity = true
self.draw_section_times = false self.draw_section_times = false
self.draw_secondary_section_times = false self.draw_secondary_section_times = false
self.big_mode = false
-- variables related to configurable parameters -- variables related to configurable parameters
self.drop_locked = false self.drop_locked = false
self.hard_drop_locked = false self.hard_drop_locked = false
@@ -119,7 +121,8 @@ function GameMode:update(inputs, ruleset)
ruleset:processPiece( ruleset:processPiece(
inputs, self.piece, self.grid, self:getGravity(), self.prev_inputs, inputs, self.piece, self.grid, self:getGravity(), self.prev_inputs,
self.move, self:getLockDelay(), self:getDropSpeed(), self.move, self:getLockDelay(), self:getDropSpeed(),
self.drop_locked, self.hard_drop_locked, self.enable_hard_drop self.drop_locked, self.hard_drop_locked,
self.enable_hard_drop, self.additive_gravity
) )
local piece_dy = self.piece.position.y - piece_y local piece_dy = self.piece.position.y - piece_y
@@ -298,7 +301,7 @@ function GameMode:initializeNextPiece(inputs, ruleset, piece_data, generate_next
inputs, piece_data, self.grid, gravity, inputs, piece_data, self.grid, gravity,
self.prev_inputs, self.move, self.prev_inputs, self.move,
self:getLockDelay(), self:getDropSpeed(), self:getLockDelay(), self:getDropSpeed(),
self.lock_drop, self.lock_hard_drop self.lock_drop, self.lock_hard_drop, self.big_mode
) )
if self.lock_drop then if self.lock_drop then
self.drop_locked = true self.drop_locked = true

View File

@@ -67,7 +67,9 @@ function MarathonA3Game:getLineClearDelay()
elseif self.speed_level < 600 then return 25 elseif self.speed_level < 600 then return 25
elseif self.speed_level < 700 then return 16 elseif self.speed_level < 700 then return 16
elseif self.speed_level < 800 then return 12 elseif self.speed_level < 800 then return 12
else return 6 end elseif self.speed_level < 1100 then return 6
elseif self.speed_level < 1200 then return 5
else return 4 end
end end
function MarathonA3Game:getLockDelay() function MarathonA3Game:getLockDelay()
@@ -117,7 +119,7 @@ function MarathonA3Game:advanceOneFrame()
if self.roll_frames + 1 == 0 then if self.roll_frames + 1 == 0 then
switchBGM("credit_roll", "gm3") switchBGM("credit_roll", "gm3")
end end
return return false
elseif self.roll_frames > 3238 then elseif self.roll_frames > 3238 then
if self:qualifiesForMRoll() then if self:qualifiesForMRoll() then
self.roll_points = self.roll_points + 160 self.roll_points = self.roll_points + 160
@@ -181,7 +183,7 @@ function MarathonA3Game:updateSectionTimes(old_level, new_level)
self.section_cool_grade = self.section_cool_grade - 1 self.section_cool_grade = self.section_cool_grade - 1
table.insert(self.section_status, "regret") table.insert(self.section_status, "regret")
elseif section <= 9 and self.section_status[section - 1] == "cool" and elseif section <= 9 and self.section_status[section - 1] == "cool" and
self.section_70_times[section] < self.section_70_times[section - 1] + 1000 then self.section_70_times[section] < self.section_70_times[section - 1] + 120 then
self.section_cool_grade = self.section_cool_grade + 1 self.section_cool_grade = self.section_cool_grade + 1
self.speed_level = self.speed_level + 100 self.speed_level = self.speed_level + 100
table.insert(self.section_status, "cool") table.insert(self.section_status, "cool")

View File

@@ -5,17 +5,15 @@ local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls' local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local MarathonL1Game = GameMode:extend() local MarathonAX4Game = GameMode:extend()
MarathonL1Game.name = "Line Attack" MarathonAX4Game.name = "Marathon AX4"
MarathonL1Game.hash = "MarathonL1" MarathonAX4Game.hash = "MarathonAX4"
MarathonL1Game.tagline = "Can you clear the time hurdles when the game goes this fast?" MarathonAX4Game.tagline = "Can you clear the time hurdles when the game goes this fast?"
function MarathonAX4Game:new()
MarathonAX4Game.super:new()
function MarathonL1Game:new()
MarathonL1Game.super:new()
self.roll_frames = 0 self.roll_frames = 0
self.randomizer = History6RollsRandomizer() self.randomizer = History6RollsRandomizer()
@@ -30,7 +28,7 @@ function MarathonL1Game:new()
self.next_queue_length = 3 self.next_queue_length = 3
end end
function MarathonL1Game:getARE() function MarathonAX4Game:getARE()
if self.lines < 10 then return 18 if self.lines < 10 then return 18
elseif self.lines < 40 then return 14 elseif self.lines < 40 then return 14
elseif self.lines < 60 then return 12 elseif self.lines < 60 then return 12
@@ -40,24 +38,24 @@ function MarathonL1Game:getARE()
else return 6 end else return 6 end
end end
function MarathonL1Game:getLineARE() function MarathonAX4Game:getLineARE()
return self:getARE() return self:getARE()
end end
function MarathonL1Game:getDasLimit() function MarathonAX4Game:getDasLimit()
if self.lines < 20 then return 10 if self.lines < 20 then return 10
elseif self.lines < 50 then return 9 elseif self.lines < 50 then return 9
elseif self.lines < 70 then return 8 elseif self.lines < 70 then return 8
else return 7 end else return 7 end
end end
function MarathonL1Game:getLineClearDelay() function MarathonAX4Game:getLineClearDelay()
if self.lines < 10 then return 14 if self.lines < 10 then return 14
elseif self.lines < 30 then return 9 elseif self.lines < 30 then return 9
else return 5 end else return 5 end
end end
function MarathonL1Game:getLockDelay() function MarathonAX4Game:getLockDelay()
if self.lines < 10 then return 28 if self.lines < 10 then return 28
elseif self.lines < 20 then return 24 elseif self.lines < 20 then return 24
elseif self.lines < 30 then return 22 elseif self.lines < 30 then return 22
@@ -67,15 +65,15 @@ function MarathonL1Game:getLockDelay()
else return 13 end else return 13 end
end end
function MarathonL1Game:getGravity() function MarathonAX4Game:getGravity()
return 20 return 20
end end
function MarathonL1Game:getSection() function MarathonAX4Game:getSection()
return math.floor(level / 100) + 1 return math.floor(level / 100) + 1
end end
function MarathonL1Game:advanceOneFrame() function MarathonAX4Game:advanceOneFrame()
if self.clear then if self.clear then
self.roll_frames = self.roll_frames + 1 self.roll_frames = self.roll_frames + 1
if self.roll_frames < 0 then if self.roll_frames < 0 then
@@ -94,7 +92,7 @@ function MarathonL1Game:advanceOneFrame()
return true return true
end end
function MarathonL1Game:onLineClear(cleared_row_count) function MarathonAX4Game:onLineClear(cleared_row_count)
if not self.clear then if not self.clear then
local new_lines = self.lines + cleared_row_count local new_lines = self.lines + cleared_row_count
self:updateSectionTimes(self.lines, new_lines) self:updateSectionTimes(self.lines, new_lines)
@@ -106,11 +104,11 @@ function MarathonL1Game:onLineClear(cleared_row_count)
end end
end end
function MarathonL1Game:getSectionTime() function MarathonAX4Game:getSectionTime()
return self.frames - self.section_start_time return self.frames - self.section_start_time
end end
function MarathonL1Game:updateSectionTimes(old_lines, new_lines) function MarathonAX4Game:updateSectionTimes(old_lines, new_lines)
if math.floor(old_lines / 10) < math.floor(new_lines / 10) then if math.floor(old_lines / 10) < math.floor(new_lines / 10) then
-- record new section -- record new section
table.insert(self.section_times, self:getSectionTime()) table.insert(self.section_times, self:getSectionTime())
@@ -119,23 +117,23 @@ function MarathonL1Game:updateSectionTimes(old_lines, new_lines)
end end
end end
function MarathonL1Game:onPieceEnter() function MarathonAX4Game:onPieceEnter()
self.section_clear = false self.section_clear = false
end end
function MarathonL1Game:drawGrid(ruleset) function MarathonAX4Game:drawGrid(ruleset)
self.grid:draw() self.grid:draw()
end end
function MarathonL1Game:getHighscoreData() function MarathonAX4Game:getHighscoreData()
return { return {
lines = self.lines, lines = self.lines,
frames = self.frames, frames = self.frames,
} }
end end
function MarathonL1Game:drawScoringInfo() function MarathonAX4Game:drawScoringInfo()
MarathonL1Game.super.drawScoringInfo(self) MarathonAX4Game.super.drawScoringInfo(self)
love.graphics.setColor(1, 1, 1, 1) love.graphics.setColor(1, 1, 1, 1)
@@ -165,12 +163,12 @@ function MarathonL1Game:drawScoringInfo()
love.graphics.setColor(1, 1, 1, 1) love.graphics.setColor(1, 1, 1, 1)
end end
function MarathonL1Game:getSectionEndLines() function MarathonAX4Game:getSectionEndLines()
return math.floor(self.lines / 10 + 1) * 10 return math.floor(self.lines / 10 + 1) * 10
end end
function MarathonL1Game:getBackground() function MarathonAX4Game:getBackground()
return math.floor(self.lines / 10) return math.floor(self.lines / 10)
end end
return MarathonL1Game return MarathonAX4Game

View File

@@ -0,0 +1,185 @@
require 'funcs'
local GameMode = require 'tetris.modes.gamemode'
local Piece = require 'tetris.components.piece'
local Randomizer = require 'tetris.randomizers.randomizer'
local MarathonC89Game = GameMode:extend()
MarathonC89Game.name = "Marathon C89"
MarathonC89Game.hash = "MarathonC89"
MarathonC89Game.tagline = "Can you play fast enough to reach the killscreen?"
function MarathonC89Game:new()
MarathonC89Game.super:new()
self.randomizer = Randomizer()
self.ready_frames = 1
self.waiting_frames = 72
self.start_level = 12
self.level = 12
self.lock_drop = true
self.enable_hard_drop = false
self.enable_hold = false
self.next_queue_length = 1
self.additive_gravity = false
end
function MarathonC89Game:getDropSpeed() return 1/2 end
function MarathonC89Game:getDasLimit() return 16 end
function MarathonC89Game:getARR() return 6 end
function MarathonC89Game:getARE() return 6 end
function MarathonC89Game:getLineARE() return 6 end
function MarathonC89Game:getLineClearDelay() return 30 end
function MarathonC89Game:getLockDelay() return 0 end
function MarathonC89Game:chargeDAS(inputs)
if inputs[self.das.direction] == true and
self.prev_inputs[self.das.direction] == true and
not inputs["down"] and
self.piece ~= nil
then
local das_frames = self.das.frames + 1
if das_frames >= self:getDasLimit() then
if self.das.direction == "left" then
self.move = (self:getARR() == 0 and "speed" or "") .. "left"
self.das.frames = self:getDasLimit() - self:getARR()
elseif self.das.direction == "right" then
self.move = (self:getARR() == 0 and "speed" or "") .. "right"
self.das.frames = self:getDasLimit() - self:getARR()
end
else
self.move = "none"
self.das.frames = das_frames
end
elseif inputs["right"] == true then
self.das.direction = "right"
if not inputs["down"] and self.piece ~= nil then
self.move = "right"
self.das.frames = 0
else
self.move = "none"
end
elseif inputs["left"] == true then
self.das.direction = "left"
if not inputs["down"] and self.piece ~= nil then
self.move = "left"
self.das.frames = 0
else
self.move = "none"
end
else
self.move = "none"
end
if self.das.direction == "left" and self.piece ~= nil and self.piece:isMoveBlocked(self.grid, {x=-1, y=0}) or
self.das.direction == "right" and self.piece ~= nil and self.piece:isMoveBlocked(self.grid, {x=1, y=0})
then
self.das.frames = self:getDasLimit()
end
if inputs["down"] == false and self.prev_inputs["down"] == true then
self.drop_bonus = 0
end
end
local gravity_table = {
[0] =
1366/65536, 1525/65536, 1725/65536, 1986/65536, 2341/65536,
2850/65536, 3641/65536, 5042/65536, 8192/65536, 10923/65536,
13108/65536, 13108/65536, 13108/65536, 16384/65536, 16384/65536,
16384/65536, 21846/65536, 21846/65536, 21846/65536
}
function MarathonC89Game:getGravity()
if self.waiting_frames > 0 then return 0 end
if self.level >= 29 then return 1
elseif self.level >= 19 then return 1/2
else return gravity_table[self.level] end
end
function MarathonC89Game:advanceOneFrame()
if self.waiting_frames > 0 then
self.waiting_frames = self.waiting_frames - 1
else
self.frames = self.frames + 1
end
return true
end
function MarathonC89Game:onPieceLock()
self.score = self.score + self.drop_bonus
self.drop_bonus = 0
end
local cleared_line_scores = { 40, 100, 300, 1200 }
function MarathonC89Game:getLevelForLines()
if self.start_level < 10 then
return math.max(self.start_level, math.floor(self.lines / 10))
elseif self.start_level < 16 then
return math.max(self.start_level, self.start_level + math.floor((self.lines - 100) / 10))
else
return math.max(self.start_level, math.floor((self.lines - 60) / 10))
end
end
function MarathonC89Game:updateScore(level, drop_bonus, cleared_lines)
if cleared_lines > 0 then
self.score = self.score + cleared_line_scores[cleared_lines] * (self.level + 1)
self.lines = self.lines + cleared_lines
self.level = self:getLevelForLines()
else
self.drop_bonus = 0
self.combo = 1
end
end
function MarathonC89Game:drawGrid()
self.grid:draw()
if self.piece ~= nil and self.level < 100 then
self:drawGhostPiece(ruleset)
end
end
function MarathonC89Game:drawScoringInfo()
MarathonC89Game.super.drawScoringInfo(self)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.setFont(font_3x5_2)
love.graphics.print(
self.das.direction .. " " ..
self.das.frames .. " " ..
st(self.prev_inputs)
)
love.graphics.printf("NEXT", 64, 40, 40, "left")
love.graphics.printf("LINES", 240, 120, 40, "left")
love.graphics.printf("SCORE", 240, 200, 40, "left")
love.graphics.setFont(font_3x5_3)
love.graphics.printf(self.lines, 240, 140, 90, "left")
love.graphics.printf(self.score, 240, 220, 90, "left")
love.graphics.setFont(font_8x11)
love.graphics.printf(formatTime(self.frames), 64, 420, 160, "center")
end
function MarathonC89Game:getBackground()
return math.min(self.level, 19)
end
function MarathonC89Game:getHighscoreData()
return {
score = self.score,
level = self.level,
}
end
return MarathonC89Game

View File

@@ -25,6 +25,8 @@ function PhantomMania2Game:new()
self.combo = 1 self.combo = 1
self.hold_age = 0 self.hold_age = 0
self.queue_age = 0 self.queue_age = 0
self.roll_points = 0
self.randomizer = History6RollsRandomizer() self.randomizer = History6RollsRandomizer()
self.lock_drop = true self.lock_drop = true
@@ -135,7 +137,7 @@ function PhantomMania2Game:onPieceEnter()
end end
local cleared_row_levels = {1, 2, 4, 6} local cleared_row_levels = {1, 2, 4, 6}
local cleared_row_points = {0.02, 0.05, 0.15, 0.6} local cleared_row_points = {2, 6, 15, 40}
function PhantomMania2Game:onLineClear(cleared_row_count) function PhantomMania2Game:onLineClear(cleared_row_count)
if not self.clear then if not self.clear then
@@ -144,6 +146,7 @@ function PhantomMania2Game:onLineClear(cleared_row_count)
if new_level >= 1300 or self:hitTorikan(self.level, new_level) then if new_level >= 1300 or self:hitTorikan(self.level, new_level) then
if new_level >= 1300 then if new_level >= 1300 then
self.level = 1300 self.level = 1300
self.big_mode = true
end end
self.clear = true self.clear = true
self.grid:clear() self.grid:clear()
@@ -152,6 +155,12 @@ function PhantomMania2Game:onLineClear(cleared_row_count)
self.level = math.min(new_level, 1300) self.level = math.min(new_level, 1300)
end end
self:advanceBottomRow(-cleared_row_count) self:advanceBottomRow(-cleared_row_count)
else
self.roll_points = self.roll_points + cleared_row_points[cleared_row_count / 2]
if self.roll_points >= 100 then
self.roll_points = self.roll_points - 100
self.grade = self.grade + 1
end
end end
end end
@@ -225,7 +234,7 @@ PhantomMania2Game.garbageOpacityFunction = function(age)
end end
function PhantomMania2Game:drawGrid() function PhantomMania2Game:drawGrid()
if not (self.game_over or self.clear) then if not (self.game_over or (self.clear and self.level < 1300)) then
self.grid:drawInvisible(self.rollOpacityFunction, self.garbageOpacityFunction) self.grid:drawInvisible(self.rollOpacityFunction, self.garbageOpacityFunction)
else else
self.grid:draw() self.grid:draw()
@@ -243,7 +252,7 @@ local function getLetterGrade(grade)
end end
function PhantomMania2Game:setNextOpacity(i) function PhantomMania2Game:setNextOpacity(i)
if self.level > 1000 then if self.level > 1000 and self.level < 1300 then
local hidden_next_pieces = math.floor(self.level / 100) - 10 local hidden_next_pieces = math.floor(self.level / 100) - 10
if i < hidden_next_pieces then if i < hidden_next_pieces then
love.graphics.setColor(1, 1, 1, 0) love.graphics.setColor(1, 1, 1, 0)
@@ -258,7 +267,7 @@ function PhantomMania2Game:setNextOpacity(i)
end end
function PhantomMania2Game:setHoldOpacity() function PhantomMania2Game:setHoldOpacity()
if self.level > 1000 then if self.level > 1000 and self.level < 1300 then
love.graphics.setColor(1, 1, 1, 1 - math.min(1, self.hold_age / 15)) love.graphics.setColor(1, 1, 1, 1 - math.min(1, self.hold_age / 15))
else else
love.graphics.setColor(1, 1, 1, 1) love.graphics.setColor(1, 1, 1, 1)

View File

@@ -5,17 +5,15 @@ local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls' local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local LigneGame = GameMode:extend() local Race40Game = GameMode:extend()
LigneGame.name = "Ligne" Race40Game.name = "Race 40"
LigneGame.hash = "Ligne" Race40Game.hash = "Race40"
LigneGame.tagline = "How fast can you clear 40 lines?" Race40Game.tagline = "How fast can you clear 40 lines?"
function Race40Game:new()
Race40Game.super:new()
function LigneGame:new()
LigneGame.super:new()
self.lines = 0 self.lines = 0
self.line_goal = 40 self.line_goal = 40
@@ -32,39 +30,39 @@ function LigneGame:new()
self.next_queue_length = 3 self.next_queue_length = 3
end end
function LigneGame:getDropSpeed() function Race40Game:getDropSpeed()
return 20 return 20
end end
function LigneGame:getARR() function Race40Game:getARR()
return 0 return 0
end end
function LigneGame:getARE() function Race40Game:getARE()
return 0 return 0
end end
function LigneGame:getLineARE() function Race40Game:getLineARE()
return self:getARE() return self:getARE()
end end
function LigneGame:getDasLimit() function Race40Game:getDasLimit()
return 6 return 6
end end
function LigneGame:getLineClearDelay() function Race40Game:getLineClearDelay()
return 0 return 0
end end
function LigneGame:getLockDelay() function Race40Game:getLockDelay()
return 15 return 15
end end
function LigneGame:getGravity() function Race40Game:getGravity()
return 1/64 return 1/64
end end
function LigneGame:advanceOneFrame() function Race40Game:advanceOneFrame()
if self.clear then if self.clear then
self.roll_frames = self.roll_frames + 1 self.roll_frames = self.roll_frames + 1
if self.roll_frames > 150 then if self.roll_frames > 150 then
@@ -77,11 +75,11 @@ function LigneGame:advanceOneFrame()
return true return true
end end
function LigneGame:onPieceLock() function Race40Game:onPieceLock()
self.pieces = self.pieces + 1 self.pieces = self.pieces + 1
end end
function LigneGame:onLineClear(cleared_row_count) function Race40Game:onLineClear(cleared_row_count)
if not self.clear then if not self.clear then
self.lines = self.lines + cleared_row_count self.lines = self.lines + cleared_row_count
if self.lines >= self.line_goal then if self.lines >= self.line_goal then
@@ -90,22 +88,22 @@ function LigneGame:onLineClear(cleared_row_count)
end end
end end
function LigneGame:drawGrid(ruleset) function Race40Game:drawGrid(ruleset)
self.grid:draw() self.grid:draw()
if self.piece ~= nil then if self.piece ~= nil then
self:drawGhostPiece(ruleset) self:drawGhostPiece(ruleset)
end end
end end
function LigneGame:getHighscoreData() function Race40Game:getHighscoreData()
return { return {
level = self.level, level = self.level,
frames = self.frames, frames = self.frames,
} }
end end
function LigneGame:drawScoringInfo() function Race40Game:drawScoringInfo()
LigneGame.super.drawScoringInfo(self) Race40Game.super.drawScoringInfo(self)
love.graphics.setColor(1, 1, 1, 1) love.graphics.setColor(1, 1, 1, 1)
local text_x = config["side_next"] and 320 or 240 local text_x = config["side_next"] and 320 or 240
@@ -124,8 +122,8 @@ function LigneGame:drawScoringInfo()
love.graphics.printf(math.max(0, self.line_goal - self.lines), text_x, 340, 40, "left") love.graphics.printf(math.max(0, self.line_goal - self.lines), text_x, 340, 40, "left")
end end
function LigneGame:getBackground() function Race40Game:getBackground()
return 2 return 2
end end
return LigneGame return Race40Game

View File

@@ -44,13 +44,14 @@ function SurvivalA3Game:getLineARE()
end end
function SurvivalA3Game:getDasLimit() function SurvivalA3Game:getDasLimit()
if self.level < 200 then return 9 if self.level < 100 then return 9
elseif self.level < 500 then return 7 elseif self.level < 500 then return 7
else return 5 end else return 5 end
end end
function SurvivalA3Game:getLineClearDelay() function SurvivalA3Game:getLineClearDelay()
return self:getLineARE() - 2 if self.level < 1300 then return self:getLineARE() - 2
else return 6 end
end end
function SurvivalA3Game:getLockDelay() function SurvivalA3Game:getLockDelay()
@@ -60,7 +61,8 @@ function SurvivalA3Game:getLockDelay()
elseif self.level < 600 then return 13 elseif self.level < 600 then return 13
elseif self.level < 1100 then return 12 elseif self.level < 1100 then return 12
elseif self.level < 1200 then return 10 elseif self.level < 1200 then return 10
else return 8 end elseif self.level < 1300 then return 8
else return 15 end
end end
function SurvivalA3Game:getGravity() function SurvivalA3Game:getGravity()
@@ -133,6 +135,7 @@ function SurvivalA3Game:onLineClear(cleared_row_count)
end end
self.clear = true self.clear = true
self.grid:clear() self.grid:clear()
self.big_mode = true
self.roll_frames = -150 self.roll_frames = -150
else else
self.level = math.min(new_level, 1300) self.level = math.min(new_level, 1300)
@@ -188,10 +191,8 @@ end
local function getLetterGrade(grade) local function getLetterGrade(grade)
if grade == 0 then if grade == 0 then
return "1" return "1"
elseif grade <= 9 then
return "S" .. tostring(grade)
else else
return "M" .. tostring(grade - 9) return "S" .. tostring(grade)
end end
end end

View File

@@ -16,6 +16,16 @@ ARS.spawn_positions = {
Z = { x=4, y=5 }, Z = { x=4, y=5 },
} }
ARS.big_spawn_positions = {
I = { x=2, y=2 },
J = { x=2, y=3 },
L = { x=2, y=3 },
O = { x=2, y=3 },
S = { x=2, y=3 },
T = { x=2, y=3 },
Z = { x=2, y=3 },
}
ARS.block_offsets = { ARS.block_offsets = {
I={ I={
{ {x=0, y=0}, {x=-1, y=0}, {x=-2, y=0}, {x=1, y=0} }, { {x=0, y=0}, {x=-1, y=0}, {x=-2, y=0}, {x=1, y=0} },

View File

@@ -16,6 +16,16 @@ ARS.spawn_positions = {
Z = { x=4, y=5 }, Z = { x=4, y=5 },
} }
ARS.big_spawn_positions = {
I = { x=2, y=2 },
J = { x=2, y=3 },
L = { x=2, y=3 },
O = { x=2, y=3 },
S = { x=2, y=3 },
T = { x=2, y=3 },
Z = { x=2, y=3 },
}
ARS.block_offsets = { ARS.block_offsets = {
I={ I={
{ {x=0, y=0}, {x=-1, y=0}, {x=-2, y=0}, {x=1, y=0} }, { {x=0, y=0}, {x=-1, y=0}, {x=-2, y=0}, {x=1, y=0} },

View File

@@ -16,6 +16,16 @@ CRS.spawn_positions = {
Z = { x=4, y=4 }, Z = { x=4, y=4 },
} }
CRS.big_spawn_positions = {
I = { x=2, y=2 },
J = { x=2, y=3 },
L = { x=2, y=3 },
O = { x=2, y=3 },
S = { x=2, y=2 },
T = { x=2, y=3 },
Z = { x=2, y=2 },
}
CRS.block_offsets = { CRS.block_offsets = {
I={ I={
{ {x=0, y=0}, {x=-1, y=0}, {x=-2, y=0}, {x=1, y=0} }, { {x=0, y=0}, {x=-1, y=0}, {x=-2, y=0}, {x=1, y=0} },

View File

@@ -72,10 +72,17 @@ function Ruleset:movePiece(piece, grid, move)
end end
end end
function Ruleset:dropPiece(inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked, hard_drop_enabled) function Ruleset:dropPiece(
inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked,
hard_drop_enabled, additive_gravity
)
local y = piece.position.y local y = piece.position.y
if inputs["down"] == true and drop_locked == false then if inputs["down"] == true and drop_locked == false then
if additive_gravity then
piece:addGravity(gravity + drop_speed, grid) piece:addGravity(gravity + drop_speed, grid)
else
piece:addGravity(math.max(gravity, drop_speed), grid)
end
elseif inputs["up"] == true and hard_drop_enabled == true then elseif inputs["up"] == true and hard_drop_enabled == true then
if hard_drop_locked == true or piece:isDropBlocked(grid) then if hard_drop_locked == true or piece:isDropBlocked(grid) then
piece:addGravity(gravity, grid) piece:addGravity(gravity, grid)
@@ -102,12 +109,19 @@ function Ruleset:getDefaultOrientation() return 1 end
function Ruleset:initializePiece( function Ruleset:initializePiece(
inputs, data, grid, gravity, prev_inputs, inputs, data, grid, gravity, prev_inputs,
move, lock_delay, drop_speed, move, lock_delay, drop_speed,
drop_locked, hard_drop_locked drop_locked, hard_drop_locked, big
) )
local spawn_positions
if big then
spawn_positions = self.big_spawn_positions
else
spawn_positions = self.spawn_positions
end
local piece = Piece(data.shape, data.orientation - 1, { local piece = Piece(data.shape, data.orientation - 1, {
x = self.spawn_positions[data.shape].x, x = spawn_positions[data.shape].x,
y = self.spawn_positions[data.shape].y y = spawn_positions[data.shape].y
}, self.block_offsets, 0, 0, data.skin) }, self.block_offsets, 0, 0, data.skin, big)
self:onPieceCreate(piece) self:onPieceCreate(piece)
self:rotatePiece(inputs, piece, grid, {}, true) self:rotatePiece(inputs, piece, grid, {}, true)
self:dropPiece(inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked) self:dropPiece(inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked)
@@ -120,11 +134,15 @@ function Ruleset:onPieceCreate(piece) end
function Ruleset:processPiece( function Ruleset:processPiece(
inputs, piece, grid, gravity, prev_inputs, inputs, piece, grid, gravity, prev_inputs,
move, lock_delay, drop_speed, move, lock_delay, drop_speed,
drop_locked, hard_drop_locked, hard_drop_enabled drop_locked, hard_drop_locked,
hard_drop_enabled, additive_gravity
) )
self:rotatePiece(inputs, piece, grid, prev_inputs, false) self:rotatePiece(inputs, piece, grid, prev_inputs, false)
self:movePiece(piece, grid, move) self:movePiece(piece, grid, move)
self:dropPiece(inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked, hard_drop_enabled) self:dropPiece(
inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked,
hard_drop_enabled, additive_gravity
)
self:lockPiece(piece, grid, lock_delay) self:lockPiece(piece, grid, lock_delay)
end end

View File

@@ -18,6 +18,16 @@ SRS.spawn_positions = {
Z = { x=4, y=5 }, Z = { x=4, y=5 },
} }
SRS.big_spawn_positions = {
I = { x=2, y=2 },
J = { x=2, y=3 },
L = { x=2, y=3 },
O = { x=2, y=3 },
S = { x=2, y=3 },
T = { x=2, y=3 },
Z = { x=2, y=3 },
}
SRS.block_offsets = { SRS.block_offsets = {
I={ I={
{ {x=0, y=0}, {x=-1, y=0}, {x=-2, y=0}, {x=1, y=0} }, { {x=0, y=0}, {x=-1, y=0}, {x=-2, y=0}, {x=1, y=0} },