First bundled release.

This commit is contained in:
Joe Z 2019-05-22 23:57:34 -04:00
commit c973929e0c
120 changed files with 8709 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*.sav
*.love
dist/*.zip
dist/**/cambridge.exe

18
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,18 @@
Coding conventions
------------------
* 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.
* 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.
* Comments with lines at the end of code must be one line long. Multi-line comments must appeare in their own block.
* Use `snake_case` for variables, `camelCase` for functions.
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.
(Regarding 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.)

29
LICENSE Normal file
View File

@ -0,0 +1,29 @@
The code in Cambridge is licensed under the MIT license.
Copyright (c) 2018 Joe Zeng
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-------------------------
Some code and assets in this repository are contributed by members of the
community, as well as borrowed from other places, either with licensing
or as placeholders until suitable material can be found that is properly
licensed. Their original sources, and copyright notices if applicable, are
listed in the file SOURCES.

108
SOURCES.md Normal file
View File

@ -0,0 +1,108 @@
Asset Sources
======
The assets in Cambridge are generally modified or formatted versions of externally gathered assets. Their original sources, if applicable, are listed below.
Some of the assets are used without proper licenses. We aim to have fully licensed or public-domain assets by version 1.0.
Backgrounds
-----------
1. Title: "Motus Glacies." Contributed by Daniel "Explo" McCarthy.
1. *Gameplay level 0: "Quantum foam." Alex Sukontsev. https://www.flickr.com/photos/control9/14957509814/
2. *Gameplay level 1: No name. http://www.onekind.tv/univision-mqb/q5mqh5brlvuuj2nhdx7ch7eum183uu
3. Gameplay level 2: "M81 Galaxy is Pretty in Pink." Jet Propulsion Lab, NASA. https://www.jpl.nasa.gov/spaceimages/details.php?id=pia09579
4. Gameplay level 3: "Formation of the Solar System." NASA. https://www.nasa.gov/images/content/149890main_BetaPictDiskbMac.jpg
5. *Gameplay level 4: No name. Dana Berry. https://news.nationalgeographic.com/news/2014/06/140605-earth-moon-theia-evidence-space-science/
6. *Gameplay level 5: "Fauna of the Burgess Shale." Carel Brest van Kempen. https://www.science-art.com/image/?id=7200&pagename=Fauna_of_the_Burgess_Shale
7. *Gameplay level 6: No name. National Geographic. http://images.nationalgeographic.com/wpf/media-live/photos/000/009/cache/cretaceous-collection_907_990x742.jpg
8. *Gameplay level 7: [original URL not found.]
9. *Gameplay level 8: [original URL not found.]
10. *Gameplay level 9: [original URL not found.]
11. Gameplay level 10: "Leif Eriksson Discovers America." Christian Krohg (1893). https://commons.wikimedia.org/wiki/File:Christian-krohg-leiv-eriksson.jpg
12. Gameplay level 11: "Taking of Jerusalem by the Crusaders, 15th July 1099." Émile Signol (1847). https://commons.wikimedia.org/wiki/File:Counquest_of_Jeusalem_(1099).jpg
13. *Gameplay level 12: [original URL not found.]
14. *Gameplay level 13: "Black Death Village." Jonas Hassibi, deviantART. https://jonashassibi.deviantart.com/art/Black-Death-Village-617757443
15. Gameplay level 14: "Landing of Columbus." Architect of the Capitol, Flickr. https://commons.wikimedia.org/wiki/File:Flickr_-_USCapitol_-_Landing_of_Columbus_(1).jpg
16. *Gameplay level 15: [original URL not found.]
17. Gameplay level 16: "Galileo Donato." H. J. Detouche. https://commons.wikimedia.org/wiki/File:Galileo_Donato.jpg
18. Gameplay level 17: "The Death of General Mercer at the Battle of Princeton, January 3, 1777." John Trumbull. https://commons.wikimedia.org/wiki/File:The_Death_of_General_Mercer_at_the_Battle_of_Princeton_January_3_1777.jpeg
19. Gameplay level 18: "Царскосельская железная дорога". Unknown. https://commons.wikimedia.org/wiki/File:%D0%A6%D0%B0%D1%80%D1%81%D0%BA%D0%BE%D1%81%D0%B5%D0%BB%D1%8C%D1%81%D0%BA%D0%B0%D1%8F_%D0%B6%D0%B5%D0%BB%D0%B5%D0%B7%D0%BD%D0%B0%D1%8F_%D0%B4%D0%BE%D1%80%D0%BE%D0%B3%D0%B0.jpg
20. *Gameplay level 19: [original URL not found.]
Backgrounds marked with a * are placeholders that will be replaced in later versions due to incompatible licenses. We are generally aiming for public domain background images, but will also accept backgrounds given proper licenses to be included within Cambridge.
Music
-----
1. TGM3 credit roll music.
2. The FitnessGram™ Pacer Test.
All background music is (currently) only unofficially included. In later releases they may be replaced with specifically licensed music as applicable.
Fonts
-----
1. System/general font: Kilo-4 3x5. Created by Joe Zeng.
2. Timer font: Big Number 8x12. Created by Joe Zeng.
All fonts are original works, licensed under the MIT license as applicable.
Libraries
=========
Cambridge makes use of the following libraries and third-party programs:
LÖVE (https://love2d.org/)
----------------
Copyright © 2006-2016 LÖVE Development Team
This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
classic (https://github.com/rxi/classic)
----------------------
Copyright (c) 2014, rxi
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
binser (https://github.com/bakpakin/binser)
--------------------
Copyright (c) 2015 Calvin Rose
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

8
conf.lua Normal file
View File

@ -0,0 +1,8 @@
function love.conf(t)
t.identity = "cambridge"
t.window.title = "Cambridge"
t.window.width = 640
t.window.height = 480
t.window.vsync = false
end

BIN
dist/win32/love.exe vendored Executable file

Binary file not shown.

BIN
dist/windows/love.exe vendored Executable file

Binary file not shown.

43
docs/gamemodes.md Normal file
View File

@ -0,0 +1,43 @@
Game modes
==========
There are several classes of game modes.
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.
* **MARATHON**: Get to level 999!
* **MARATHON 2020**: 2020 levels of pure pain.
From other games:
* **MARATHON A1**: Tetris the Grand Master 1.
* **MARATHON A2**: Tetris the Grand Master 2 (TAP Master).
* **MARATHON A3**: Tetris the Grand Master 3 (no exams).
* **MARATHON N1**: NES Tetris A-type.
SURVIVAL
--------
Modes that concentrate on how long you can survive an increasingly fast and difficult game.
* **SURVIVAL**:
From other games:
* **SURVIVAL A2**: T.A. Death.
* **SURVIVAL A3**: Ti Shirase.
RACE
----
Modes in which the goal is to achieve a fixed goal in the shortest time.
* **RACE 40L**: Clear 40 lines as fast as possible.
* **RACE 100L**: Clear 100 lines as fast as possible.
* **RACE 5K**: Clear 5,000 lines as fast as possible. Don't worry about topping out!
* **RACE 10K**: Clear 10,000 lines as fast as possible. Don't worry about topping out!

View File

@ -0,0 +1,207 @@
Marathon 2020
=============
To celebrate the coming of the year 2020, I've created a new "extended Tetris the Grand Master" mode where the level counter goes up twice as far as normal, all the way up to 2020.
Gameplay
--------
The goal of this game is to reach the end at level 2020.
Every piece placed increases the level by 1, and every line cleared also increases the level by 1, with bonuses for large numbers of lines:
| Lines cleared | Levels advanced |
|---------------|-----------------|
| 1 | 1 |
| 2 | 2 |
| 3 | 4 |
| 4 | 6 |
When the current level reaches one less than the level at the bottom of the display (usually a multiple of 100), the level will not advance until a line is cleared.
Levels
------
Each section is 100 levels long, except for the last section, whose levels go from 1900 all the way to 2020.
However, it is possible to be stopped early on if you do not play fast enough.
### Torikans
There are certain checkpoints at which your current time will be checked and you will be stopped if your time is over a set objective time.
| Level | Time limit |
|-------|------------|
| 500 | 6:00.00 |
| 900 | 8:30.00 |
| 1000 | 8:45.00 |
| 1500 | 11:30.00 |
| 1900 | 13:15.00 |
At levels 500, 1000, and 1500, you will be stopped immediately if your time is not under the objective.
At levels 900 and 1900, the next section will be capped at 999 or 1999 respectively, and you will get a short credit roll when the section is over.
Speed
-----
Marathon 2020 gets faster in two different, independent ways, the gravity curve and the delay curve.
The gravity curve is always the same at a particular level, while the delay curve can vary based on your previous section time.
### Gravity Curve
The gravity curve is the same as it is in the original TGM and TAP.
### Delay Curve
The delay curve is shown as in the following table. Line ARE is always equal to ARE.
If your time in a particular section from 0 to 70 is smaller than the "cool" requirement at that level, your delay curve will be bumped up an extra level at the end of the section.
The delay curve always advances at least 1 level past level 500, and if you get a section cool when the level is past 500, it will advance 2 levels instead.
| Level | ARE | Lock | DAS | Line | Cool |
|-------|------|------|------|------|------|
|0|27|30|15|40|45.00|
|100|24|30|12|25|41.50|
|200|21|30|12|25|38.50|
|300|18|30|9|20|35.00|
|400|16|30|9|15|32.50|
|500|14|30|8|12|29.20|
|600|12|26|8|12|27.20|
|700|10|22|8|8|24.80|
|800|8|19|7|8|22.80|
|900|6|17|7|6|20.60|
|1000|6|15|6|6|19.60|
|1100|6|15|6|4|19.40|
|1200|6|15|6|4|19.40|
|1300|5|15|5|4|18.40|
|1400|5|15|5|2|18.20|
|1500|4|15|4|2|16.20|
|1600|4|13|4|2|16.20|
|1700|4|11|4|2|16.20|
|1800|4|10|4|2|16.20|
|1900|4|9|4|2|16.20|
|2000|4|8|3|2|15.20|
In order to get a section cool, your 0-70 section time must be below the cutoff *and* no more than 2 seconds slower than your previous 0-70 time.
Grading
-------
### Basic grades
Internally, the grade counter is a number that can range from 0 to 30.
At the beginning of the game, it starts at 0. To increase it, you must bring an internal grade point counter past a certain threshold.
The threshold is set at 50 points for the first grade, then 100 more points for the next grade, then 150 points for the grade after that, and so on. You reach the maximum level of 30 at a total of 23,250 points.
A table of the thresholds in grade points required to reach each level is provided below:
Grade|Threshold
-|-
0|0
1|50
2|150
3|300
4|500
5|750
6|1050
7|1400
8|1800
9|2250
10|2750
11|3300
12|3900
13|4550
14|5250
15|6000
16|6800
17|7650
18|8550
19|9500
20|10500
21|11550
22|12650
23|13800
24|15000
25|16250
26|17550
27|18900
28|20300
29|21750
**30**|**23250**
Points are given according to a different scale, the point level. The point level is calculated by taking your current actual level, and adding (100 * the delay level) to it.
The points given for clearing certain amounts of lines is given as follows:
Level| x1 | x2 | x3 | x4
-|-|-|-|-
0|10|20|30|40
100|10|20|30|40
200|10|20|30|48
300|10|20|30|60
400|10|20|36|72
500|10|21|42|84
600|10|24|48|96
700|10|27|54|108
800|10|30|60|120
900|11|33|66|140
1000|12|36|72|160
1100|13|39|81|180
1200|14|42|90|200
1300|15|45|99|220
1400|16|48|108|240
1500|17|52|117|260
1600|18|56|126|280
1700|19|60|135|300
1800|20|64|144|320
1900|21|68|153|340
2000|22|72|162|360
2100|23|76|171|380
2200|24|80|180|400
2300|25|84|189|420
2400|26|88|198|440
2500|27|92|207|460
2600|28|96|216|480
2700|29|100|225|500
2800|30|104|234|520
2900|31|108|243|540
3000|32|112|252|560
3100|33|116|261|580
3200|34|120|270|600
3300|35|124|279|620
3400|36|128|288|640
3500|37|132|297|660
3600|38|136|306|680
3700|39|140|315|700
3800|40|144|324|720
3900|41|148|333|740
Past level 1000, a 4-line clear will always give (30 * current grade), regardless of point level.
The remaining 20 grades come from section cools. Every section cool you get boosts your score by one grade. There are no regrets.
Points are also taken away with the time it takes to lock down a piece. The delay counter starts at 0, and increases by (current grade + 2) every frame. When the counter reaches or exceeds 240, it resets to 0, and 1 grade point is taken away.
Grades are based on the maximum grade points achieved. Once a grade has been attained, it cannot be lost even if grade points drop below the threshold for that grade.
Stats
-----
* Fewest number of lines/pieces to reach 2020: 1263 pieces / 505 lines [all Tetrises]
* Most number of lines/pieces to reach 2020: 1448 pieces / 572 lines [all singles / doubles, full board]

View File

@ -0,0 +1,104 @@
Phantom Mania 2
===============
The Phantom Mania mode in Nullpomino is based on T.A. Death (Speed Mania), but where everything is invisible. The obvious sequel to such a mode is Phantom Mania 2, which is based on Shirase (Speed Mania 2), but again where everything is invisible.
Gameplay
--------
The goal of this game is to reach the end at level 1300, and then score as many grades as possible in the ending credit roll.
Each piece disappears as soon as it is placed down. Only the active piece is visible at any given time.
Every piece placed increases the level by 1, and every line cleared also increases the level by 1, with bonuses for large numbers of lines:
| Lines cleared | Levels advanced |
|---------------|-----------------|
| 1 | 1 |
| 2 | 2 |
| 3 | 4 |
| 4 | 6 |
When the current level reaches one less than the level at the bottom of the display (usually a multiple of 100), the level will not advance until a line is cleared.
Levels
------
Each section is 100 levels long. At certain levels, the speed may get faster, or the difficulty may increase some other way.
| Level | What happens |
|-------|------------|
| 500 | Rows of garbage start to advance on the board. The bottom row is copied, appearing for a brief flash before disappearing again. |
| 1000 | The garbage stops, but the hold queue turns invisible. |
| 1100 | The first next preview turns invisible. |
| 1200 | The second next preview turns invisible. |
Unlike in Shirase mode, the speed of the game does not get faster past level 1000.
### Torikans
There are certain checkpoints at which your current time will be checked and you will be stopped if your time is over a set objective time.
| Level | Time limit |
|-------|------------|
| 300 | 2:02.00 |
| 500 | 3:03.00 |
| 800 | 4:45.00 |
| 1000 | 5:38.00 |
At each torikan, you will get a 54-second invisible roll which is worth grade points.
Only the 1300 roll is in Big Mode.
Grading
-------
Your grade starts at 1, and goes on from S1 to S9, and then M1 upwards indefinitely.
Your grade advances each section based on your section time. If you achieve a section COOL, you get 2 grade points. If you instead incur a section REGRET, you get no grade points. The normal section time (in between COOL and REGRET) is worth one grade point.
| Level | Section COOL | Section REGRET |
|-------|--------------|----------------|
| 0-4 | 36.00 | 48.00 |
| 5-9 | 30.00 | 39.00 |
| 10-12 | 27.00 | 35.00 |
Then, the invisible roll is also worth grade points.
| Lines cleared | Grade points |
|---------------|--------------|
| 1 | 0.02 |
| 2 | 0.06 |
| 3 | 0.15 |
| 4 | 0.40 |
| Clear | 1.00 |
The number of grade points you earn correspond to your grade as follows:
| Points | Grade |
|--------|--------|
| 0 | 1 |
| 1 | S1 |
| 2 | S2 |
| 3 | S3 |
| 4 | S4 |
| 5 | S5 |
| 6 | S6 |
| 7 | S7 |
| 8 | S8 |
| 9 | S9 |
| 10 | m1 |
| 11 | m2 |
| 12 | m3 |
| 13 | m4 |
| 14 | m5 |
And so on. For *x* > 9 grade points, your grade is m(*x*-9).
If you achieve a section COOL on each section, your rank will be m17 entering the credit roll.

View File

@ -0,0 +1,32 @@
Phantom Mania comparison
========================
## Features
| | PM | PM2 |
|------------|-----|------|
| Max level | 999 | 1300 |
| Piece preview | 1 | 3 |
| Hold | No | Yes |
| Graded roll | No | Yes |
## Torikans
| Level | PM | PM2 |
|------------|-------|-------|
| 300 | 2'28" | 2'02" |
| 500 | 3'38" | 3'03" |
| 800 | 5'23" | 4'40" |
| 1000 | --- | 5'38" |
## Speed Curve
| Level | PM | PM2 |
|------------|-------|-------|
| 0 | 16/30 | 10/18 |
| 100 | 12/26 | 10/18 |
| 200 | 6/22 | 10/17 |
| 300 | 6/18 | 4/15 |
| 400 | 5/15 | 4/15 |
| 500 | 4/15 | 4/13 |
| 600+ | 4/15 | 4/12 |

33
docs/rulesets.md Normal file
View File

@ -0,0 +1,33 @@
Rulesets
========
A **ruleset** is a set of rules that apply to any game mode.
A ruleset consists of the following things:
* A *rotation system*, which defines how pieces move and rotate.
* A *lock delay reset system*, which defines how pieces lock when they can no longer move or rotate.
If you're used to Nullpomino, you may notice a few things missing from that definition. For example, piece previews, hold queues, and randomizers have been moved to being game-specific rules, rather than rules that are changeable with the ruleset you use. Soft and hard drop behaviour is also game-specific now, so that times can be more plausibly compared across rulesets.
Rotation system
---------------
A rotation system defines the following things:
* The block offsets of each piece orientation.
* The wall or floor kicks that will be attempted for each type of rotation.
There are three main classes/families of rotation systems:
* **ARIKA**, commonly known as ARS.
* **ARIKA-CLASSIC**, commonly known as Classic ARS.
* **ARIKA-TI**, commonly known as Ti-ARS, or "ARS with floorkicks".
* **STANDARD**, commonly known as SRS.
* **STANDARD**, or normal SRS.
* **STANDARD-EXP**, known as SRS-X in its original Heboris incarnation.
* **STANDARD-WORLD**, known as World Rule in TGM3.
* **CLASSIC**, commonly known as ORS or NRS (Nintendo). Also houses some traditional rotation systems.
* **CLASSIC-1989**, the no-wallkick system used by NES Tetris.
* **CLASSIC-1984**, the Electonika-60 system, where the I piece is one space higher than in CLASSIC-1989.
* **CLASSIC-SEGA**, the original Sega rotation system that spawned Arika.
* **CLASSIC-TENGEN**, the weird one with orientation problems.

55
funcs.lua Normal file
View File

@ -0,0 +1,55 @@
function copy(t)
if type(t) ~= "table" then return t end
local meta = getmetatable(t)
local target = {}
for k, v in pairs(t) do target[k] = v end
setmetatable(target, meta)
return target
end
function st(tbl)
str = ""
for k, v in pairs(tbl) do
if v == true then
str = str .. k .. " "
end
end
return str
end
function sp(m, s, f)
if m == nil then m = 0 end
if s == nil then s = 0 end
if f == nil then f = 0 end
return m*3600 + s*60 + math.ceil(f * 0.6)
end
function vAdd(v1, v2)
return {
x = v1.x + v2.x,
y = v1.y + v2.y
}
end
function vNeg(v)
return {
x = -v.x,
y = -v.y
}
end
function formatTime(frames)
if frames < 0 then return formatTime(0) end
str = string.format("%02d", math.floor(frames / 3600)) .. ":"
.. string.format("%02d", math.floor(frames / 60) % 60) .. "."
.. string.format("%02d", math.floor(frames / 0.6) % 100)
return str
end
function formatBigNum(number)
local s = string.format("%d", number)
local pos = string.len(s) % 3
if pos == 0 then pos = 3 end
return string.sub(s, 1, pos)
.. string.gsub(string.sub(s, pos+1), "(...)", ",%1")
end

689
libs/binser.lua Normal file
View File

@ -0,0 +1,689 @@
-- binser.lua
--[[
Copyright (c) 2016 Calvin Rose
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
local assert = assert
local error = error
local select = select
local pairs = pairs
local getmetatable = getmetatable
local setmetatable = setmetatable
local tonumber = tonumber
local type = type
local loadstring = loadstring or load
local concat = table.concat
local char = string.char
local byte = string.byte
local format = string.format
local sub = string.sub
local dump = string.dump
local floor = math.floor
local frexp = math.frexp
local unpack = unpack or table.unpack
-- Lua 5.3 frexp polyfill
-- From https://github.com/excessive/cpml/blob/master/modules/utils.lua
if not frexp then
local log, abs, floor = math.log, math.abs, math.floor
local log2 = log(2)
frexp = function(x)
if x == 0 then return 0, 0 end
local e = floor(log(abs(x)) / log2 + 1)
return x / 2 ^ e, e
end
end
-- NIL = 202
-- FLOAT = 203
-- TRUE = 204
-- FALSE = 205
-- STRING = 206
-- TABLE = 207
-- REFERENCE = 208
-- CONSTRUCTOR = 209
-- FUNCTION = 210
-- RESOURCE = 211
-- INT64 = 212
local mts = {}
local ids = {}
local serializers = {}
local deserializers = {}
local resources = {}
local resources_by_name = {}
local function pack(...)
return {...}, select("#", ...)
end
local function not_array_index(x, len)
return type(x) ~= "number" or x < 1 or x > len or x ~= floor(x)
end
local function type_check(x, tp, name)
assert(type(x) == tp,
format("Expected parameter %q to be of type %q.", name, tp))
end
local bigIntSupport = false
local isInteger
if math.type then -- Detect Lua 5.3
local mtype = math.type
bigIntSupport = loadstring[[
local char = string.char
return function(n)
local nn = n < 0 and -(n + 1) or n
local b1 = nn // 0x100000000000000
local b2 = nn // 0x1000000000000 % 0x100
local b3 = nn // 0x10000000000 % 0x100
local b4 = nn // 0x100000000 % 0x100
local b5 = nn // 0x1000000 % 0x100
local b6 = nn // 0x10000 % 0x100
local b7 = nn // 0x100 % 0x100
local b8 = nn % 0x100
if n < 0 then
b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4
b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8
end
return char(212, b1, b2, b3, b4, b5, b6, b7, b8)
end]]()
isInteger = function(x)
return mtype(x) == 'integer'
end
else
isInteger = function(x)
return floor(x) == x
end
end
-- Copyright (C) 2012-2015 Francois Perrad.
-- number serialization code modified from https://github.com/fperrad/lua-MessagePack
-- Encode a number as a big-endian ieee-754 double, big-endian signed 64 bit integer, or a small integer
local function number_to_str(n)
if isInteger(n) then -- int
if n <= 100 and n >= -27 then -- 1 byte, 7 bits of data
return char(n + 27)
elseif n <= 8191 and n >= -8192 then -- 2 bytes, 14 bits of data
n = n + 8192
return char(128 + (floor(n / 0x100) % 0x100), n % 0x100)
elseif bigIntSupport then
return bigIntSupport(n)
end
end
local sign = 0
if n < 0.0 then
sign = 0x80
n = -n
end
local m, e = frexp(n) -- mantissa, exponent
if m ~= m then
return char(203, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
elseif m == 1/0 then
if sign == 0 then
return char(203, 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
else
return char(203, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
end
end
e = e + 0x3FE
if e < 1 then -- denormalized numbers
m = m * 2 ^ (52 + e)
e = 0
else
m = (m * 2 - 1) * 2 ^ 52
end
return char(203,
sign + floor(e / 0x10),
(e % 0x10) * 0x10 + floor(m / 0x1000000000000),
floor(m / 0x10000000000) % 0x100,
floor(m / 0x100000000) % 0x100,
floor(m / 0x1000000) % 0x100,
floor(m / 0x10000) % 0x100,
floor(m / 0x100) % 0x100,
m % 0x100)
end
-- Copyright (C) 2012-2015 Francois Perrad.
-- number deserialization code also modified from https://github.com/fperrad/lua-MessagePack
local function number_from_str(str, index)
local b = byte(str, index)
if b < 128 then
return b - 27, index + 1
elseif b < 192 then
return byte(str, index + 1) + 0x100 * (b - 128) - 8192, index + 2
end
local b1, b2, b3, b4, b5, b6, b7, b8 = byte(str, index + 1, index + 8)
if b == 212 then
local flip = b1 >= 128
if flip then -- negative
b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4
b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8
end
local n = ((((((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8
if flip then
return (-n) - 1, index + 9
else
return n, index + 9
end
end
local sign = b1 > 0x7F and -1 or 1
local e = (b1 % 0x80) * 0x10 + floor(b2 / 0x10)
local m = ((((((b2 % 0x10) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8
local n
if e == 0 then
if m == 0 then
n = sign * 0.0
else
n = sign * (m / 2 ^ 52) * 2 ^ -1022
end
elseif e == 0x7FF then
if m == 0 then
n = sign * (1/0)
else
n = 0.0/0.0
end
else
n = sign * (1.0 + m / 2 ^ 52) * 2 ^ (e - 0x3FF)
end
return n, index + 9
end
local types = {}
types["nil"] = function(x, visited, accum)
accum[#accum + 1] = "\202"
end
function types.number(x, visited, accum)
accum[#accum + 1] = number_to_str(x)
end
function types.boolean(x, visited, accum)
accum[#accum + 1] = x and "\204" or "\205"
end
function types.string(x, visited, accum)
local alen = #accum
if visited[x] then
accum[alen + 1] = "\208"
accum[alen + 2] = number_to_str(visited[x])
else
visited[x] = visited.next
visited.next = visited.next + 1
accum[alen + 1] = "\206"
accum[alen + 2] = number_to_str(#x)
accum[alen + 3] = x
end
end
local function check_custom_type(x, visited, accum)
local res = resources[x]
if res then
accum[#accum + 1] = "\211"
types[type(res)](res, visited, accum)
return true
end
local mt = getmetatable(x)
local id = mt and ids[mt]
if id then
if x == visited.temp then
error("Infinite loop in constructor.")
end
visited.temp = x
accum[#accum + 1] = "\209"
types[type(id)](id, visited, accum)
local args, len = pack(serializers[id](x))
accum[#accum + 1] = number_to_str(len)
for i = 1, len do
local arg = args[i]
types[type(arg)](arg, visited, accum)
end
visited[x] = visited.next
visited.next = visited.next + 1
return true
end
end
function types.userdata(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, accum) then return end
error("Cannot serialize this userdata.")
end
end
function types.table(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, accum) then return end
visited[x] = visited.next
visited.next = visited.next + 1
local xlen = #x
accum[#accum + 1] = "\207"
accum[#accum + 1] = number_to_str(xlen)
for i = 1, xlen do
local v = x[i]
types[type(v)](v, visited, accum)
end
local key_count = 0
for k in pairs(x) do
if not_array_index(k, xlen) then
key_count = key_count + 1
end
end
accum[#accum + 1] = number_to_str(key_count)
for k, v in pairs(x) do
if not_array_index(k, xlen) then
types[type(k)](k, visited, accum)
types[type(v)](v, visited, accum)
end
end
end
end
types["function"] = function(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, accum) then return end
visited[x] = visited.next
visited.next = visited.next + 1
local str = dump(x)
accum[#accum + 1] = "\210"
accum[#accum + 1] = number_to_str(#str)
accum[#accum + 1] = str
end
end
types.cdata = function(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, #accum) then return end
error("Cannot serialize this cdata.")
end
end
types.thread = function() error("Cannot serialize threads.") end
local function deserialize_value(str, index, visited)
local t = byte(str, index)
if not t then return end
if t < 128 then
return t - 27, index + 1
elseif t < 192 then
return byte(str, index + 1) + 0x100 * (t - 128) - 8192, index + 2
elseif t == 202 then
return nil, index + 1
elseif t == 203 then
return number_from_str(str, index)
elseif t == 204 then
return true, index + 1
elseif t == 205 then
return false, index + 1
elseif t == 206 then
local length, dataindex = deserialize_value(str, index + 1, visited)
local nextindex = dataindex + length
local substr = sub(str, dataindex, nextindex - 1)
visited[#visited + 1] = substr
return substr, nextindex
elseif t == 207 then
local count, nextindex = number_from_str(str, index + 1)
local ret = {}
visited[#visited + 1] = ret
for i = 1, count do
ret[i], nextindex = deserialize_value(str, nextindex, visited)
end
count, nextindex = number_from_str(str, nextindex)
for i = 1, count do
local k, v
k, nextindex = deserialize_value(str, nextindex, visited)
v, nextindex = deserialize_value(str, nextindex, visited)
ret[k] = v
end
return ret, nextindex
elseif t == 208 then
local ref, nextindex = number_from_str(str, index + 1)
return visited[ref], nextindex
elseif t == 209 then
local count
local name, nextindex = deserialize_value(str, index + 1, visited)
count, nextindex = number_from_str(str, nextindex)
local args = {}
for i = 1, count do
args[i], nextindex = deserialize_value(str, nextindex, visited)
end
local ret = deserializers[name](unpack(args))
visited[#visited + 1] = ret
return ret, nextindex
elseif t == 210 then
local length, dataindex = deserialize_value(str, index + 1, visited)
local nextindex = dataindex + length
local ret = loadstring(sub(str, dataindex, nextindex - 1))
visited[#visited + 1] = ret
return ret, nextindex
elseif t == 211 then
local res, nextindex = deserialize_value(str, index + 1, visited)
return resources_by_name[res], nextindex
elseif t == 212 then
return number_from_str(str, index)
else
error("Could not deserialize type byte " .. t .. ".")
end
end
local function serialize(...)
local visited = {next = 1}
local accum = {}
for i = 1, select("#", ...) do
local x = select(i, ...)
types[type(x)](x, visited, accum)
end
return concat(accum)
end
local function make_file_writer(file)
return setmetatable({}, {
__newindex = function(_, _, v)
file:write(v)
end
})
end
local function serialize_to_file(path, mode, ...)
local file, err = io.open(path, mode)
assert(file, err)
local visited = {next = 1}
local accum = make_file_writer(file)
for i = 1, select("#", ...) do
local x = select(i, ...)
types[type(x)](x, visited, accum)
end
-- flush the writer
file:flush()
file:close()
end
local function writeFile(path, ...)
return serialize_to_file(path, "wb", ...)
end
local function appendFile(path, ...)
return serialize_to_file(path, "ab", ...)
end
local function deserialize(str, index)
assert(type(str) == "string", "Expected string to deserialize.")
local vals = {}
index = index or 1
local visited = {}
local len = 0
local val
while index do
val, index = deserialize_value(str, index, visited)
if index then
len = len + 1
vals[len] = val
end
end
return vals, len
end
local function deserializeN(str, n, index)
assert(type(str) == "string", "Expected string to deserialize.")
n = n or 1
assert(type(n) == "number", "Expected a number for parameter n.")
assert(n > 0 and floor(n) == n, "N must be a poitive integer.")
local vals = {}
index = index or 1
local visited = {}
local len = 0
local val
while index and len < n do
val, index = deserialize_value(str, index, visited)
if index then
len = len + 1
vals[len] = val
end
end
vals[len + 1] = index
return unpack(vals, 1, n + 1)
end
local function readFile(path)
local file, err = io.open(path, "rb")
if file == nil then
return nil, 0
end
local str = file:read("*all")
file:close()
return deserialize(str)
end
local function default_deserialize(metatable)
return function(...)
local ret = {}
for i = 1, select("#", ...), 2 do
ret[select(i, ...)] = select(i + 1, ...)
end
return setmetatable(ret, metatable)
end
end
local function default_serialize(x)
assert(type(x) == "table",
"Default serialization for custom types only works for tables.")
local args = {}
local len = 0
for k, v in pairs(x) do
args[len + 1], args[len + 2] = k, v
len = len + 2
end
return unpack(args, 1, len)
end
-- Templating
local function normalize_template(template)
local ret = {}
for i = 1, #template do
ret[i] = template[i]
end
local non_array_part = {}
-- The non-array part of the template (nested templates) have to be deterministic, so they are sorted.
-- This means that inherently non deterministicly sortable keys (tables, functions) should NOT be used
-- in templates. Looking for way around this.
for k in pairs(template) do
if not_array_index(k, #template) then
non_array_part[#non_array_part + 1] = k
end
end
table.sort(non_array_part)
for i = 1, #non_array_part do
local name = non_array_part[i]
ret[#ret + 1] = {name, normalize_template(template[name])}
end
return ret
end
local function templatepart_serialize(part, argaccum, x, len)
local extras = {}
local extracount = 0
for k, v in pairs(x) do
extras[k] = v
extracount = extracount + 1
end
for i = 1, #part do
extracount = extracount - 1
if type(part[i]) == "table" then
extras[part[i][1]] = nil
len = templatepart_serialize(part[i][2], argaccum, x[part[i][1]], len)
else
extras[part[i]] = nil
len = len + 1
argaccum[len] = x[part[i]]
end
end
if extracount > 0 then
argaccum[len + 1] = extras
else
argaccum[len + 1] = nil
end
return len + 1
end
local function templatepart_deserialize(ret, part, values, vindex)
for i = 1, #part do
local name = part[i]
if type(name) == "table" then
local newret = {}
ret[name[1]] = newret
vindex = templatepart_deserialize(newret, name[2], values, vindex)
else
ret[name] = values[vindex]
vindex = vindex + 1
end
end
local extras = values[vindex]
if extras then
for k, v in pairs(extras) do
ret[k] = v
end
end
return vindex + 1
end
local function template_serializer_and_deserializer(metatable, template)
return function(x)
argaccum = {}
local len = templatepart_serialize(template, argaccum, x, 0)
return unpack(argaccum, 1, len)
end, function(...)
local ret = {}
local len = select("#", ...)
local args = {...}
templatepart_deserialize(ret, template, args, 1)
return setmetatable(ret, metatable)
end
end
local function register(metatable, name, serialize, deserialize)
name = name or metatable.name
serialize = serialize or metatable._serialize
deserialize = deserialize or metatable._deserialize
if not serialize then
if metatable._template then
local t = normalize_template(metatable._template)
serialize, deserialize = template_serializer_and_deserializer(metatable, t)
elseif not deserialize then
serialize = default_serialize
deserialize = default_deserialize(metatable)
else
serialize = metatable
end
end
type_check(metatable, "table", "metatable")
type_check(name, "string", "name")
type_check(serialize, "function", "serialize")
type_check(deserialize, "function", "deserialize")
assert(not ids[metatable], "Metatable already registered.")
assert(not mts[name], ("Name %q already registered."):format(name))
mts[name] = metatable
ids[metatable] = name
serializers[name] = serialize
deserializers[name] = deserialize
return metatable
end
local function unregister(item)
local name, metatable
if type(item) == "string" then -- assume name
name, metatable = item, mts[item]
else -- assume metatable
name, metatable = ids[item], item
end
type_check(name, "string", "name")
type_check(metatable, "table", "metatable")
mts[name] = nil
ids[metatable] = nil
serializers[name] = nil
deserializers[name] = nil
return metatable
end
local function registerClass(class, name)
name = name or class.name
if class.__instanceDict then -- middleclass
register(class.__instanceDict, name)
else -- assume 30log or similar library
register(class, name)
end
return class
end
local function registerResource(resource, name)
type_check(name, "string", "name")
assert(not resources[resource],
"Resource already registered.")
assert(not resources_by_name[name],
format("Resource %q already exists.", name))
resources_by_name[name] = resource
resources[resource] = name
return resource
end
local function unregisterResource(name)
type_check(name, "string", "name")
assert(resources_by_name[name], format("Resource %q does not exist.", name))
local resource = resources_by_name[name]
resources_by_name[name] = nil
resources[resource] = nil
return resource
end
return {
-- aliases
s = serialize,
d = deserialize,
dn = deserializeN,
r = readFile,
w = writeFile,
a = appendFile,
serialize = serialize,
deserialize = deserialize,
deserializeN = deserializeN,
readFile = readFile,
writeFile = writeFile,
appendFile = appendFile,
register = register,
unregister = unregister,
registerResource = registerResource,
unregisterResource = unregisterResource,
registerClass = registerClass
}

68
libs/classic.lua Normal file
View File

@ -0,0 +1,68 @@
--
-- classic
--
-- Copyright (c) 2014, rxi
--
-- This module is free software; you can redistribute it and/or modify it under
-- the terms of the MIT license. See LICENSE for details.
--
local Object = {}
Object.__index = Object
function Object:new()
end
function Object:extend()
local cls = {}
for k, v in pairs(self) do
if k:find("__") == 1 then
cls[k] = v
end
end
cls.__index = cls
cls.super = self
setmetatable(cls, self)
return cls
end
function Object:implement(...)
for _, cls in pairs({...}) do
for k, v in pairs(cls) do
if self[k] == nil and type(v) == "function" then
self[k] = v
end
end
end
end
function Object:is(T)
local mt = getmetatable(self)
while mt do
if mt == T then
return true
end
mt = getmetatable(mt)
end
return false
end
function Object:__tostring()
return "Object"
end
function Object:__call(...)
local obj = setmetatable({}, self)
obj:new(...)
return obj
end
return Object

76
load/bgm.lua Normal file
View File

@ -0,0 +1,76 @@
bgm = {
credit_roll = {
gm3 = love.audio.newSource("res/bgm/tgm_credit_roll.mp3", "stream"),
},
pacer_test = love.audio.newSource("res/bgm/pacer_test.mp3", "stream"),
}
local current_bgm = nil
local bgm_locked = false
function switchBGM(sound, subsound)
if bgm_locked then return end
if current_bgm ~= nil then
current_bgm:stop()
end
if subsound ~= nil then
current_bgm = bgm[sound][subsound]
resetBGMFadeout()
elseif sound ~= nil then
current_bgm = bgm[sound]
resetBGMFadeout()
else
current_bgm = nil
end
end
function switchBGMLoop(sound, subsound)
if bgm_locked then return end
switchBGM(sound, subsound)
current_bgm:setLooping(true)
end
function lockBGM()
bgm_locked = true
end
local fading_bgm = false
local fadeout_time = 0
local total_fadeout_time = 0
function fadeoutBGM(time)
if fading_bgm == false then
fading_bgm = true
fadeout_time = time
total_fadeout_time = time
end
end
function resetBGMFadeout(time)
current_bgm:setVolume(1)
fading_bgm = false
current_bgm:play()
end
function processBGMFadeout(dt)
if fading_bgm then
fadeout_time = fadeout_time - dt
if fadeout_time < 0 then
fadeout_time = 0
fading_bgm = false
end
current_bgm:setVolume(fadeout_time / total_fadeout_time)
end
end
function pauseBGM()
if current_bgm ~= nil then
current_bgm:pause()
end
end
function resumeBGM()
if current_bgm ~= nil then
current_bgm:play()
end
end

33
load/fonts.lua Normal file
View File

@ -0,0 +1,33 @@
font_3x5 = love.graphics.newImageFont(
"res/fonts/3x5.png",
" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" ..
"`abcdefghijklmnopqrstuvwxyz{|}~™",
-1
)
font_3x5_2 = love.graphics.newImageFont(
"res/fonts/3x5_double.png",
" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" ..
"`abcdefghijklmnopqrstuvwxyz{|}~™",
-2
)
font_3x5_3 = love.graphics.newImageFont(
"res/fonts/3x5_medium.png",
" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" ..
"`abcdefghijklmnopqrstuvwxyz{|}~",
-3
)
font_3x5_4 = love.graphics.newImageFont(
"res/fonts/3x5_large.png",
" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" ..
"`abcdefghijklmnopqrstuvwxyz{|}~",
-4
)
font_8x11 = love.graphics.newImageFont(
"res/fonts/8x11_medium.png",
"0123456789:.",
1
)

58
load/graphics.lua Normal file
View File

@ -0,0 +1,58 @@
backgrounds = {
[0] = love.graphics.newImage("res/backgrounds/0-quantum-foam.png"),
love.graphics.newImage("res/backgrounds/100-big-bang.png"),
love.graphics.newImage("res/backgrounds/200-spiral-galaxy.png"),
love.graphics.newImage("res/backgrounds/300-sun-and-dust.png"),
love.graphics.newImage("res/backgrounds/400-earth-and-moon.png"),
love.graphics.newImage("res/backgrounds/500-cambrian-explosion.png"),
love.graphics.newImage("res/backgrounds/600-dinosaurs.png"),
love.graphics.newImage("res/backgrounds/700-asteroid.png"),
love.graphics.newImage("res/backgrounds/800-human-fire.png"),
love.graphics.newImage("res/backgrounds/900-early-civilization.png"),
love.graphics.newImage("res/backgrounds/1000-vikings.png"),
love.graphics.newImage("res/backgrounds/1100-crusades.png"),
love.graphics.newImage("res/backgrounds/1200-genghis-khan.png"),
love.graphics.newImage("res/backgrounds/1300-black-death.png"),
love.graphics.newImage("res/backgrounds/1400-columbus-discovery.png"),
love.graphics.newImage("res/backgrounds/1500-aztecas.png"),
love.graphics.newImage("res/backgrounds/1600-telescope.png"),
love.graphics.newImage("res/backgrounds/1700-american-revolution.png"),
love.graphics.newImage("res/backgrounds/1800-railways.png"),
love.graphics.newImage("res/backgrounds/1900-world-wide-web.png"),
title = love.graphics.newImage("res/backgrounds/title_v0.1.png"),
}
blocks = {
["2tie"] = {
I = love.graphics.newImage("res/img/s1.png"),
J = love.graphics.newImage("res/img/s4.png"),
L = love.graphics.newImage("res/img/s3.png"),
O = love.graphics.newImage("res/img/s7.png"),
S = love.graphics.newImage("res/img/s5.png"),
T = love.graphics.newImage("res/img/s2.png"),
Z = love.graphics.newImage("res/img/s6.png"),
F = love.graphics.newImage("res/img/s9.png"),
G = love.graphics.newImage("res/img/s9.png"),
X = love.graphics.newImage("res/img/s9.png"),
},
["bone"] = {
I = love.graphics.newImage("res/img/bone.png"),
J = love.graphics.newImage("res/img/bone.png"),
L = love.graphics.newImage("res/img/bone.png"),
O = love.graphics.newImage("res/img/bone.png"),
S = love.graphics.newImage("res/img/bone.png"),
T = love.graphics.newImage("res/img/bone.png"),
Z = love.graphics.newImage("res/img/bone.png"),
F = love.graphics.newImage("res/img/bone.png"),
G = love.graphics.newImage("res/img/bone.png"),
X = love.graphics.newImage("res/img/bone.png"),
}
}
misc_graphics = {
frame = love.graphics.newImage("res/img/frame.png"),
ready = love.graphics.newImage("res/img/ready.png"),
go = love.graphics.newImage("res/img/go.png"),
select_mode = love.graphics.newImage("res/img/select_mode.png"),
strike = love.graphics.newImage("res/img/strike.png"),
}

24
load/save.lua Normal file
View File

@ -0,0 +1,24 @@
local binser = require 'libs.binser'
function loadSave()
config = loadFromFile('config.sav')
highscores = loadFromFile('highscores.sav')
end
function loadFromFile(filename)
local save_data, len = binser.readFile(filename)
if save_data == nil then
return {} -- new object
end
return save_data[1]
end
function saveConfig()
binser.writeFile('config.sav', config)
end
function saveHighscores()
binser.writeFile('highscores.sav', highscores)
end

29
load/sounds.lua Normal file
View File

@ -0,0 +1,29 @@
sounds = {
blocks = {
I = love.audio.newSource("res/se/piece_i.wav", "static"),
J = love.audio.newSource("res/se/piece_j.wav", "static"),
L = love.audio.newSource("res/se/piece_l.wav", "static"),
O = love.audio.newSource("res/se/piece_o.wav", "static"),
S = love.audio.newSource("res/se/piece_s.wav", "static"),
T = love.audio.newSource("res/se/piece_t.wav", "static"),
Z = love.audio.newSource("res/se/piece_z.wav", "static")
},
move = love.audio.newSource("res/se/move.wav", "static"),
bottom = love.audio.newSource("res/se/bottom.wav", "static"),
}
function playSE(sound, subsound)
if subsound == nil then
sounds[sound]:setVolume(0.1)
if sounds[sound]:isPlaying() then
sounds[sound]:stop()
end
sounds[sound]:play()
else
sounds[sound][subsound]:setVolume(0.1)
if sounds[sound][subsound]:isPlaying() then
sounds[sound][subsound]:stop()
end
sounds[sound][subsound]:play()
end
end

96
main.lua Normal file
View File

@ -0,0 +1,96 @@
function love.load()
math.randomseed(os.time())
highscores = {}
require "load.graphics"
require "load.fonts"
require "load.sounds"
require "load.bgm"
require "load.save"
loadSave()
require "scene"
config["side_next"] = false
config["reverse_rotate"] = true
config["fullscreen"] = false
if not config.input then
config.input = {}
scene = InputConfigScene()
else
if config.current_mode then current_mode = config.current_mode end
if config.current_ruleset then current_ruleset = config.current_ruleset end
scene = TitleScene()
end
end
local TARGET_FPS = 60
local SAMPLE_SIZE = 60
local rolling_samples = {}
local rolling_total = 0
local average_n = 0
local frame = 0
function getSmoothedDt(dt)
rolling_total = rolling_total + dt
frame = frame + 1
if frame > SAMPLE_SIZE then frame = frame - SAMPLE_SIZE end
if average_n == SAMPLE_SIZE then
rolling_total = rolling_total - rolling_samples[frame]
else
average_n = average_n + 1
end
rolling_samples[frame] = dt
return rolling_total / average_n
end
local update_time = 0.52
function love.update(dt)
processBGMFadeout(dt)
local old_update_time = update_time
update_time = update_time + getSmoothedDt(dt) * TARGET_FPS
updates = 0
while (update_time >= 1.02) do
scene:update()
updates = updates + 1
update_time = update_time - 1
end
if math.abs(update_time - old_update_time) < 0.02 then
update_time = old_update_time
end
end
function love.draw()
love.graphics.push()
if love.window.getFullscreen() then
-- get offset matrix
love.graphics.setDefaultFilter("linear", "nearest")
local width = love.graphics.getWidth()
local height = love.graphics.getHeight()
local scale_factor = math.min(width / 640, height / 480)
love.graphics.translate(
(width - scale_factor * 640) / 2,
(height - scale_factor * 480) / 2
)
love.graphics.scale(scale_factor)
end
scene:render()
love.graphics.pop()
end
function love.keypressed(key, scancode, isrepeat)
-- global hotkeys
if scancode == "f4" then
config["fullscreen"] = not config["fullscreen"]
love.window.setFullscreen(config["fullscreen"])
else
scene:onKeyPress({key=key, scancode=scancode, isRepeat=isrepeat})
end
end
function love.focus(f)
if f then
resumeBGM()
else
pauseBGM()
end
end

1
package Executable file
View File

@ -0,0 +1 @@
zip -r cambridge.love libs load res scene tetris conf.lua main.lua scene.lua funcs.lua

9
release Executable file
View File

@ -0,0 +1,9 @@
./package
mkdir dist
mkdir dist/windows
mkdir dist/win32
cp cambridge.love dist/
cat dist/windows/love.exe cambridge.love > dist/windows/cambridge.exe
zip dist/cambridge-windows.zip dist/windows/* SOURCES.md LICENSE
cat dist/win32/love.exe cambridge.love > dist/win32/cambridge.exe
zip dist/cambridge-win32.zip dist/win32/* SOURCES.md LICENSE

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
res/backgrounds/100-big-bang.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
res/backgrounds/1000-vikings.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
res/backgrounds/1100-crusades.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

BIN
res/backgrounds/1500-aztecas.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
res/backgrounds/1800-railways.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
res/backgrounds/600-dinosaurs.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
res/backgrounds/700-asteroid.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
res/bgm/highscores.wav Normal file

Binary file not shown.

BIN
res/bgm/pacer_test.mp3 Normal file

Binary file not shown.

BIN
res/bgm/tgm_credit_roll.mp3 Normal file

Binary file not shown.

BIN
res/fonts/3x5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
res/fonts/3x5.xcf Normal file

Binary file not shown.

BIN
res/fonts/3x5_double.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
res/fonts/3x5_large.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
res/fonts/3x5_medium.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
res/fonts/8x11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

BIN
res/fonts/8x11_medium.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

BIN
res/fonts/8x12.xcf Normal file

Binary file not shown.

BIN
res/img/bone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

BIN
res/img/frame.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 B

BIN
res/img/go.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

BIN
res/img/ready.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 B

BIN
res/img/s1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

BIN
res/img/s2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

BIN
res/img/s3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

BIN
res/img/s4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

BIN
res/img/s5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 B

BIN
res/img/s6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

BIN
res/img/s7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

BIN
res/img/s9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

BIN
res/img/select_mode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

BIN
res/img/strike.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

BIN
res/img/torikan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 B

BIN
res/se/bottom.wav Normal file

Binary file not shown.

BIN
res/se/move.wav Normal file

Binary file not shown.

BIN
res/se/piece_i.wav Normal file

Binary file not shown.

BIN
res/se/piece_j.wav Normal file

Binary file not shown.

BIN
res/se/piece_l.wav Normal file

Binary file not shown.

BIN
res/se/piece_o.wav Normal file

Binary file not shown.

BIN
res/se/piece_s.wav Normal file

Binary file not shown.

BIN
res/se/piece_t.wav Normal file

Binary file not shown.

BIN
res/se/piece_z.wav Normal file

Binary file not shown.

14
scene.lua Normal file
View File

@ -0,0 +1,14 @@
local Object = require "libs.classic"
Scene = Object:extend()
function Scene:new() end
function Scene:update() end
function Scene:render() end
function Scene:onKeyPress() end
GameScene = require "scene.game"
ModeSelectScene = require "scene.mode_select"
InputConfigScene = require "scene.input_config"
ConfigScene = require "scene.config"
TitleScene = require "scene.title"

24
scene/config.lua Normal file
View File

@ -0,0 +1,24 @@
local ConfigScene = Scene:extend()
require 'load.save'
function ConfigScene:new()
end
function ConfigScene:update()
end
function ConfigScene:render()
end
function ConfigScene:changeOption(rel)
local len = table.getn(main_menu_screens)
self.main_menu_state = (self.main_menu_state + len + rel - 1) % len + 1
end
function ConfigScene:onKeyPress(e)
end
return ConfigScene

74
scene/game.lua Normal file
View File

@ -0,0 +1,74 @@
local GameScene = Scene:extend()
require 'load.save'
function GameScene:new(game_mode, ruleset)
self.game = game_mode()
self.ruleset = ruleset()
self.game:initialize(self.ruleset)
end
function GameScene:update()
if love.window.hasFocus() then
self.game:update({
left = love.keyboard.isScancodeDown(config.input.left),
right = love.keyboard.isScancodeDown(config.input.right),
up = love.keyboard.isScancodeDown(config.input.up),
down = love.keyboard.isScancodeDown(config.input.down),
rotate_left = love.keyboard.isScancodeDown(config.input.rotate_left),
rotate_left2 = love.keyboard.isScancodeDown(config.input.rotate_left2),
rotate_right = love.keyboard.isScancodeDown(config.input.rotate_right),
rotate_right2 = love.keyboard.isScancodeDown(config.input.rotate_right2),
rotate_180 = love.keyboard.isScancodeDown(config.input.rotate_180),
hold = love.keyboard.isScancodeDown(config.input.hold),
}, self.ruleset)
end
self.game.grid:update()
end
function GameScene:render()
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(
backgrounds[self.game:getBackground()],
0, 0, 0,
0.5, 0.5
)
-- game frame
love.graphics.draw(misc_graphics["frame"], 48, 64)
love.graphics.setColor(0, 0, 0, 200)
love.graphics.rectangle("fill", 64, 80, 160, 320)
self.game:drawGrid()
self.game:drawPiece()
self.game:drawNextQueue(self.ruleset)
self.game:drawScoringInfo()
-- ready/go graphics
if self.game.ready_frames <= 100 and self.game.ready_frames > 52 then
love.graphics.draw(misc_graphics["ready"], 144 - 50, 240 - 14)
elseif self.game.ready_frames <= 50 and self.game.ready_frames > 2 then
love.graphics.draw(misc_graphics["go"], 144 - 27, 240 - 14)
end
self.game:drawCustom()
end
function GameScene:onKeyPress(e)
if (self.game.completed) and
e.scancode == "return" and e.isRepeat == false then
highscore_entry = self.game:getHighscoreData()
highscore_hash = self.game.hash .. "-" .. self.ruleset.hash
submitHighscore(highscore_hash, highscore_entry)
scene = ModeSelectScene()
end
end
function submitHighscore(hash, data)
if not highscores[hash] then highscores[hash] = {} end
table.insert(highscores[hash], data)
saveHighscores()
end
return GameScene

62
scene/input_config.lua Normal file
View File

@ -0,0 +1,62 @@
local ConfigScene = Scene:extend()
ConfigScene.title = "Input Config"
require 'load.save'
local configurable_inputs = {
"left",
"right",
"up",
"down",
"rotate_left",
"rotate_left2",
"rotate_right",
"rotate_right2",
"rotate_180",
"hold",
}
function ConfigScene:new()
-- load current config
self.config = config.input
self.input_state = 1
end
function ConfigScene:update()
end
function ConfigScene:render()
love.graphics.setFont(font_3x5_2)
for i, input in pairs(configurable_inputs) do
if config.input[input] then
love.graphics.printf(input, 40, 50 + i * 20, 200, "left")
love.graphics.printf(
love.keyboard.getKeyFromScancode(config.input[input]) .. " (" .. config.input[input] .. ")",
240, 50 + i * 20, 200, "left"
)
end
end
if self.input_state > table.getn(configurable_inputs) then
love.graphics.print("press enter to confirm, delete to retry")
else
love.graphics.print("press key for " .. configurable_inputs[self.input_state])
end
end
function ConfigScene:onKeyPress(e)
if self.input_state > table.getn(configurable_inputs) then
if e.scancode == "return" then
-- save, then load next scene
saveConfig()
scene = TitleScene()
elseif e.scancode == "delete" or e.scancode == "backspace" then
self.input_state = 1
end
else
config.input[configurable_inputs[self.input_state]] = e.scancode
self.input_state = self.input_state + 1
end
end
return ConfigScene

127
scene/mode_select.lua Normal file
View File

@ -0,0 +1,127 @@
local ModeSelectScene = Scene:extend()
ModeSelectScene.title = "Game Start"
current_mode = 1
current_ruleset = 1
game_modes = {
require 'tetris.modes.marathon_2020',
require 'tetris.modes.survival_2020',
require 'tetris.modes.demon_mode',
require 'tetris.modes.strategy',
require 'tetris.modes.interval_training',
require 'tetris.modes.pacer_test',
require 'tetris.modes.phantom_mania',
require 'tetris.modes.phantom_mania2',
require 'tetris.modes.phantom_mania_n',
require 'tetris.modes.ligne',
require 'tetris.modes.marathon_a1',
require 'tetris.modes.marathon_a2',
require 'tetris.modes.marathon_a3',
require 'tetris.modes.survival_a1',
require 'tetris.modes.survival_a2',
require 'tetris.modes.survival_a3',
require 'tetris.modes.marathon_l1',
}
rulesets = {
require 'tetris.rulesets.cambridge',
require 'tetris.rulesets.arika',
require 'tetris.rulesets.arika_ti',
require 'tetris.rulesets.standard_exp',
--require 'tetris.rulesets.bonkers',
--require 'tetris.rulesets.shirase',
--require 'tetris.rulesets.super302',
}
function ModeSelectScene:new()
self.menu_state = {
mode = current_mode,
ruleset = current_ruleset,
select = "mode",
}
end
function ModeSelectScene:update()
end
function ModeSelectScene:render()
love.graphics.draw(
backgrounds[0],
0, 0, 0,
0.5, 0.5
)
if self.menu_state.select == "mode" then
love.graphics.setColor(1, 1, 1, 0.5)
elseif self.menu_state.select == "ruleset" then
love.graphics.setColor(1, 1, 1, 0.25)
end
love.graphics.rectangle("fill", 20, 78 + 20 * self.menu_state.mode, 240, 22)
if self.menu_state.select == "mode" then
love.graphics.setColor(1, 1, 1, 0.25)
elseif self.menu_state.select == "ruleset" then
love.graphics.setColor(1, 1, 1, 0.5)
end
love.graphics.rectangle("fill", 340, 78 + 20 * self.menu_state.ruleset, 200, 22)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(misc_graphics["select_mode"], 20, 40)
love.graphics.setFont(font_3x5_2)
for idx, mode in pairs(game_modes) do
love.graphics.printf(mode.name, 40, 80 + 20 * idx, 200, "left")
end
for idx, ruleset in pairs(rulesets) do
love.graphics.printf(ruleset.name, 360, 80 + 20 * idx, 160, "left")
end
end
function ModeSelectScene:onKeyPress(e)
if e.scancode == "return" and e.isRepeat == false then
current_mode = self.menu_state.mode
current_ruleset = self.menu_state.ruleset
config.current_mode = current_mode
config.current_ruleset = current_ruleset
saveConfig()
scene = GameScene(game_modes[self.menu_state.mode], rulesets[self.menu_state.ruleset])
elseif (e.scancode == config.input["up"] or e.scancode == "up") and e.isRepeat == false then
self:changeOption(-1)
elseif (e.scancode == config.input["down"] or e.scancode == "down") and e.isRepeat == false then
self:changeOption(1)
elseif (e.scancode == config.input["left"] or e.scancode == "left") or
(e.scancode == config.input["right"] or e.scancode == "right") then
self:switchSelect()
end
end
function ModeSelectScene:changeOption(rel)
if self.menu_state.select == "mode" then
self:changeMode(rel)
elseif self.menu_state.select == "ruleset" then
self:changeRuleset(rel)
end
end
function ModeSelectScene:switchSelect(rel)
if self.menu_state.select == "mode" then
self.menu_state.select = "ruleset"
elseif self.menu_state.select == "ruleset" then
self.menu_state.select = "mode"
end
end
function ModeSelectScene:changeMode(rel)
local len = table.getn(game_modes)
self.menu_state.mode = (self.menu_state.mode + len + rel - 1) % len + 1
end
function ModeSelectScene:changeRuleset(rel)
local len = table.getn(rulesets)
self.menu_state.ruleset = (self.menu_state.ruleset + len + rel - 1) % len + 1
end
return ModeSelectScene

49
scene/title.lua Normal file
View File

@ -0,0 +1,49 @@
local TitleScene = Scene:extend()
local main_menu_screens = {
ModeSelectScene,
InputConfigScene,
}
function TitleScene:new()
self.main_menu_state = 1
end
function TitleScene:update()
end
function TitleScene:render()
love.graphics.setFont(font_3x5_2)
love.graphics.draw(
backgrounds["title"],
0, 0, 0,
0.5, 0.5
)
love.graphics.setColor(1, 1, 1, 0.5)
love.graphics.rectangle("fill", 20, 278 + 20 * self.main_menu_state, 160, 22)
love.graphics.setColor(1, 1, 1, 1)
for i, screen in pairs(main_menu_screens) do
love.graphics.printf(screen.title, 40, 280 + 20 * i, 120, "left")
end
end
function TitleScene:changeOption(rel)
local len = table.getn(main_menu_screens)
self.main_menu_state = (self.main_menu_state + len + rel - 1) % len + 1
end
function TitleScene:onKeyPress(e)
if e.scancode == "return" and e.isRepeat == false then
scene = main_menu_screens[self.main_menu_state]()
elseif (e.scancode == config.input["up"] or e.scancode == "up") and e.isRepeat == false then
self:changeOption(-1)
elseif (e.scancode == config.input["down"] or e.scancode == "down") and e.isRepeat == false then
self:changeOption(1)
end
end
return TitleScene

204
tetris/components/grid.lua Normal file
View File

@ -0,0 +1,204 @@
local Object = require 'libs.classic'
local Grid = Object:extend()
local empty = { skin = "", colour = "" }
function Grid:new()
self.grid = {}
self.grid_age = {}
for y = 1, 24 do
self.grid[y] = {}
self.grid_age[y] = {}
for x = 1, 10 do
self.grid[y][x] = empty
self.grid_age[y][x] = 0
end
end
end
function Grid:clear()
for y = 1, 24 do
for x = 1, 10 do
self.grid[y][x] = empty
self.grid_age[y][x] = 0
end
end
end
function Grid:isOccupied(x, y)
return self.grid[y+1][x+1] ~= empty
end
function Grid:isRowFull(row)
for index, square in pairs(self.grid[row]) do
if square == empty then return false end
end
return true
end
function Grid:canPlacePiece(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 >= 10 or x < 0 or y >= 24 or y < 0 or self.grid[y+1][x+1] ~= empty then
return false
end
end
return true
end
function Grid:canPlacePieceInVisibleGrid(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 >= 10 or x < 0 or y >= 24 or y < 4 or self.grid[y+1][x+1] ~= empty then
return false
end
end
return true
end
function Grid:getClearedRowCount()
local count = 0
for row = 1, 24 do
if self:isRowFull(row) then
count = count + 1
end
end
return count
end
function Grid:markClearedRows()
for row = 1, 24 do
if self:isRowFull(row) then
for x = 1, 10 do
self.grid[row][x] = {
skin = self.grid[row][x].skin,
colour = "X"
}
end
end
end
return true
end
function Grid:clearClearedRows()
for row = 1, 24 do
if self:isRowFull(row) then
for above_row = row, 2, -1 do
self.grid[above_row] = self.grid[above_row - 1]
self.grid_age[above_row] = self.grid_age[above_row - 1]
end
self.grid[1] = {empty, empty, empty, empty, empty, empty, empty, empty, empty, empty}
self.grid_age[1] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
end
end
return true
end
function Grid:copyBottomRow()
for row = 1, 23 do
self.grid[row] = self.grid[row+1]
self.grid_age[row] = self.grid_age[row+1]
end
self.grid[24] = {empty, empty, empty, empty, empty, empty, empty, empty, empty, empty}
self.grid_age[24] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
for col = 1, 10 do
self.grid[24][col] = (self.grid[23][col] == empty) and empty or {
skin = self.grid[23][col].skin,
colour = "G"
}
end
return true
end
function Grid:applyPiece(piece)
offsets = piece:getBlockOffsets()
for index, offset in pairs(offsets) do
x = piece.position.x + offset.x
y = piece.position.y + offset.y
self.grid[y+1][x+1] = {
skin = piece.skin,
colour = piece.shape
}
end
end
function Grid:update()
for y = 1, 24 do
for x = 1, 10 do
if self.grid[y][x] ~= empty then
self.grid_age[y][x] = self.grid_age[y][x] + 1
end
end
end
end
function Grid:draw()
for y = 1, 24 do
for x = 1, 10 do
if self.grid[y][x] ~= empty then
if self.grid_age[y][x] < 1 then
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(blocks[self.grid[y][x].skin]["F"], 48+x*16, y*16)
else
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)
end
love.graphics.setColor(0.8, 0.8, 0.8, 1)
love.graphics.setLineWidth(1)
if y > 1 and self.grid[y-1][x] == empty then
love.graphics.line(48.0+x*16, -0.5+y*16, 64.0+x*16, -0.5+y*16)
end
if y < 24 and self.grid[y+1][x] == empty then
love.graphics.line(48.0+x*16, 16.5+y*16, 64.0+x*16, 16.5+y*16)
end
if x > 1 and self.grid[y][x-1] == empty then
love.graphics.line(47.5+x*16, -0.0+y*16, 47.5+x*16, 16.0+y*16)
end
if x < 10 and self.grid[y][x+1] == empty then
love.graphics.line(64.5+x*16, -0.0+y*16, 64.5+x*16, 16.0+y*16)
end
end
end
end
end
function Grid:drawInvisible(opacity_function, garbage_opacity_function)
for y = 1, 24 do
for x = 1, 10 do
if self.grid[y][x] ~= empty then
if self.grid[y][x].colour == "X" then
opacity = 1
elseif garbage_opacity_function and self.grid[y][x].colour == "G" then
opacity = garbage_opacity_function(self.grid_age[y][x])
else
opacity = opacity_function(self.grid_age[y][x])
end
love.graphics.setColor(0.5, 0.5, 0.5, opacity)
love.graphics.draw(blocks[self.grid[y][x].skin][self.grid[y][x].colour], 48+x*16, y*16)
if opacity > 0 and self.grid[y][x].colour ~= "X" then
love.graphics.setColor(0.64, 0.64, 0.64)
love.graphics.setLineWidth(1)
if y > 1 and self.grid[y-1][x] == empty then
love.graphics.line(48.0+x*16, -0.5+y*16, 64.0+x*16, -0.5+y*16)
end
if y < 24 and self.grid[y+1][x] == empty then
love.graphics.line(48.0+x*16, 16.5+y*16, 64.0+x*16, 16.5+y*16)
end
if x > 1 and self.grid[y][x-1] == empty then
love.graphics.line(47.5+x*16, -0.0+y*16, 47.5+x*16, 16.0+y*16)
end
if x < 10 and self.grid[y][x+1] == empty then
love.graphics.line(64.5+x*16, -0.0+y*16, 64.5+x*16, 16.0+y*16)
end
end
end
end
end
end
return Grid

153
tetris/components/piece.lua Normal file
View File

@ -0,0 +1,153 @@
local Object = require 'libs.classic'
local Piece = Object:extend()
function Piece:new(shape, rotation, position, block_offsets, gravity, lock_delay, skin)
self.shape = shape
self.rotation = rotation
self.position = position
self.block_offsets = block_offsets
self.gravity = gravity
self.lock_delay = lock_delay
self.skin = skin
self.ghost = false
self.locked = false
end
-- Functions that return a new piece to test in rotation systems.
function Piece:withOffset(offset)
return Piece(
self.shape, self.rotation,
{x = self.position.x + offset.x, y = self.position.y + offset.y},
self.block_offsets, self.gravity, self.lock_delay, self.skin
)
end
function Piece:withRelativeRotation(rot)
local new_rot = self.rotation + rot
while new_rot < 0 do new_rot = new_rot + 4 end
while new_rot >= 4 do new_rot = new_rot - 4 end
return Piece(
self.shape, new_rot, self.position,
self.block_offsets, self.gravity, self.lock_delay, self.skin
)
end
-- Functions that return predicates relative to a grid.
function Piece:getBlockOffsets()
return self.block_offsets[self.shape][self.rotation + 1]
end
function Piece:occupiesSquare(x, y)
local offsets = self:getBlockOffsets()
for index, offset in pairs(offsets) do
local new_offset = {x = self.position.x + offset.x, y = self.position.y + offset.y}
if new_offset.x == x and new_offset.y == y then
return true
end
end
return false
end
function Piece:isMoveBlocked(grid, offset)
local moved_piece = self:withOffset(offset)
return not grid:canPlacePiece(moved_piece)
end
function Piece:isDropBlocked(grid)
return self:isMoveBlocked(grid, { x=0, y=1 })
end
-- Procedures to actually do stuff to pieces.
function Piece:setOffset(offset)
self.position.x = self.position.x + offset.x
self.position.y = self.position.y + offset.y
return self
end
function Piece:setRelativeRotation(rot)
new_rot = self.rotation + rot
while new_rot < 0 do new_rot = new_rot + 4 end
while new_rot >= 4 do new_rot = new_rot - 4 end
self.rotation = new_rot
return self
end
function Piece:moveInGrid(step, squares, grid)
local moved = false
for x = 1, squares do
if grid:canPlacePiece(self:withOffset(step)) then
moved = true
self:setOffset(step)
else
break
end
end
if moved and step.y == 0 then playSE("move") end
return self
end
function Piece:dropSquares(dropped_squares, grid)
self:moveInGrid({ x = 0, y = 1 }, dropped_squares, grid)
end
function Piece:dropToBottom(grid)
local piece_y = self.position.y
self:dropSquares(20, grid)
self.gravity = 0
if self.position.y > piece_y then
-- if it got dropped any, also reset lock delay
if self.ghost == false then playSE("bottom") end
self.lock_delay = 0
end
return self
end
function Piece:lockIfBottomed(grid)
if self:isDropBlocked(grid) then
self.locked = true
end
return self
end
function Piece:addGravity(gravity, grid)
local new_gravity = self.gravity + gravity
if self:isDropBlocked(grid) then
self.gravity = math.min(1, new_gravity)
self.lock_delay = self.lock_delay + 1
else
local dropped_squares = math.floor(new_gravity)
local new_frac_gravity = new_gravity - dropped_squares
self.gravity = new_frac_gravity
self:dropSquares(dropped_squares, grid)
if self:isDropBlocked(grid) then
playSE("bottom")
end
end
return self
end
-- Procedures for drawing.
function Piece:draw(opacity, brightness, grid, partial_das)
if opacity == nil then opacity = 1 end
if brightness == nil then brightness = 1 end
love.graphics.setColor(brightness, brightness, brightness, opacity)
local offsets = self:getBlockOffsets()
local gravity_offset = 0
if grid ~= nil and not self:isDropBlocked(grid) then
gravity_offset = self.gravity * 16
end
if partial_das == nil then partial_das = 0 end
for index, offset in pairs(offsets) do
local x = self.position.x + offset.x
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)
end
return false
end
return Piece

258
tetris/modes/demon_mode.lua Normal file
View File

@ -0,0 +1,258 @@
require 'funcs'
local GameMode = require 'tetris.modes.gamemode'
local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local DemonModeGame = GameMode:extend()
DemonModeGame.name = "Demon Mode"
DemonModeGame.hash = "DemonMode"
DemonModeGame.tagline = "Can you handle the ludicrous speed past level 20?"
DemonModeGame.arr = 1
DemonModeGame.drop_speed = 1
function DemonModeGame:new()
DemonModeGame.super:new()
self.roll_frames = 0
self.combo = 1
self.randomizer = History6RollsRandomizer()
self.grade = 0
self.section_start_time = 0
self.section_times = { [0] = 0 }
self.section_tetris_count = 0
self.section_tries = 0
self.enable_hold = true
self.lock_drop = true
self.next_queue_length = 3
end
function DemonModeGame:getARE()
if self.level < 500 then return 30
elseif self.level < 600 then return 25
elseif self.level < 700 then return 15
elseif self.level < 800 then return 14
elseif self.level < 900 then return 12
elseif self.level < 1000 then return 11
elseif self.level < 1100 then return 10
elseif self.level < 1300 then return 8
elseif self.level < 1400 then return 6
elseif self.level < 1700 then return 4
elseif self.level < 1800 then return 3
elseif self.level < 1900 then return 2
elseif self.level < 2000 then return 1
else return 0 end
end
function DemonModeGame:getLineARE()
return self:getARE()
end
function DemonModeGame:getDasLimit()
if self.level < 500 then return 15
elseif self.level < 1000 then return 10
elseif self.level < 1500 then return 5
elseif self.level < 1700 then return 4
elseif self.level < 1900 then return 3
elseif self.level < 2000 then return 2
else return 1 end
end
function DemonModeGame:getLineClearDelay()
if self.level < 600 then return 15
elseif self.level < 800 then return 10
elseif self.level < 1000 then return 8
elseif self.level < 1500 then return 5
elseif self.level < 1700 then return 3
elseif self.level < 1900 then return 2
elseif self.level < 2000 then return 1
else return 0 end
end
function DemonModeGame:getLockDelay()
if self.level < 100 then return 30
elseif self.level < 200 then return 25
elseif self.level < 300 then return 22
elseif self.level < 400 then return 20
elseif self.level < 1000 then return 15
elseif self.level < 1200 then return 10
elseif self.level < 1400 then return 9
elseif self.level < 1500 then return 8
elseif self.level < 1600 then return 7
elseif self.level < 1700 then return 6
elseif self.level < 1800 then return 5
elseif self.level < 1900 then return 4
elseif self.level < 2000 then return 3
else return 2 end
end
function DemonModeGame:getGravity()
return 20
end
local function getSectionForLevel(level)
return math.floor(level / 100) + 1
end
local cleared_row_levels = {1, 3, 6, 10}
function DemonModeGame:advanceOneFrame()
if self.clear then
self.roll_frames = self.roll_frames + 1
if self.roll_frames < 0 then
return
elseif self.roll_frames >= 1337 then
self.completed = true
end
elseif self.ready_frames == 0 then
self.frames = self.frames + 1
end
end
function DemonModeGame:onPieceEnter()
if (self.level % 100 ~= 99) and self.frames ~= 0 then
self.level = self.level + 1
end
end
function DemonModeGame:onLineClear(cleared_row_count)
if cleared_row_count == 4 then
self.section_tetris_count = self.section_tetris_count + 1
end
local advanced_levels = cleared_row_levels[cleared_row_count]
if not self.clear then
self:updateSectionTimes(self.level, self.level + advanced_levels)
end
end
function DemonModeGame:updateSectionTimes(old_level, new_level)
local section = math.floor(old_level / 100) + 1
if math.floor(old_level / 100) < math.floor(new_level / 100) then
-- If at least one Tetris in this section hasn't been made,
-- deny section passage.
if old_level > 500 then
if self.section_tetris_count == 0 then
self.level = 100 * math.floor(old_level / 100)
self.section_tries = self.section_tries + 1
else
self.level = math.min(new_level, 2500)
-- if this is first try (no denials, add a grade)
if self.section_tries == 0 then
self.grade = self.grade + 1
end
self.section_tries = 0
self.section_tetris_count = 0
-- record new section
section_time = self.frames - self.section_start_time
table.insert(self.section_times, section_time)
self.section_start_time = self.frames
-- maybe clear
if self.level == 2500 and not self.clear then
self.clear = true
self.grid:clear()
self.roll_frames = -150
end
end
elseif old_level < 100 then
-- If section time is under cutoff, skip to level 500.
if self.frames < sp(1,00) then
self.level = 500
self.grade = 5
self.section_tries = 0
self.section_tetris_count = 0
else
self.level = math.min(new_level, 2500)
end
-- record new section
section_time = self.frames - self.section_start_time
table.insert(self.section_times, section_time)
self.section_start_time = self.frames
end
else
self.level = math.min(new_level, 2500)
end
end
function DemonModeGame:updateScore(level, drop_bonus, cleared_lines)
if cleared_lines > 0 then
self.score = self.score + (
(math.ceil((level + cleared_lines) / 4) + drop_bonus) *
cleared_lines * (cleared_lines * 2 - 1) * (self.combo * 2 - 1)
)
self.lines = self.lines + cleared_lines
self.combo = self.combo + cleared_lines - 1
else
self.drop_bonus = 0
self.combo = 1
end
end
local letter_grades = {
[0] = "", "D", "C", "B", "A",
"S", "S-A", "S-B", "S-C", "S-D",
"X", "X-A", "X-B", "X-C", "X-D",
"W", "W-A", "W-B", "W-C", "W-D",
"Master", "MasterS", "MasterX", "MasterW", "Grand Master",
"Demon Master"
}
function DemonModeGame:getLetterGrade()
return letter_grades[self.grade]
end
function DemonModeGame:drawGrid()
if self.clear and not (self.completed or self.game_over) then
self.grid:drawInvisible(self.rollOpacityFunction)
else
self.grid:draw()
end
end
DemonModeGame.rollOpacityFunction = function(age)
if age > 4 then return 0
else return 1 - age / 4 end
end
function DemonModeGame:drawScoringInfo()
DemonModeGame.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("GRADE", 240, 120, 40, "left")
love.graphics.printf("SCORE", 240, 200, 40, "left")
love.graphics.printf("LEVEL", 240, 320, 40, "left")
-- draw section time data
local current_section = getSectionForLevel(self.level)
self:drawSectionTimesWithSecondary(current_section)
love.graphics.setFont(font_3x5_3)
love.graphics.printf(self.score, 240, 220, 90, "left")
love.graphics.printf(self:getLetterGrade(), 240, 140, 90, "left")
love.graphics.printf(string.format("%.2f", self.level / 100), 240, 340, 70, "right")
end
function DemonModeGame:getHighscoreData()
return {
grade = self.grade,
level = self.level,
frames = self.frames,
}
end
function DemonModeGame:getBackground()
return math.floor(self.level / 100)
end
return DemonModeGame

442
tetris/modes/gamemode.lua Normal file
View File

@ -0,0 +1,442 @@
local Object = require 'libs.classic'
require 'funcs'
local Grid = require 'tetris.components.grid'
local Randomizer = require 'tetris.randomizers.randomizer'
local GameMode = Object:extend()
GameMode.rollOpacityFunction = function(age) return 0 end
function GameMode:new()
self.grid = Grid()
self.randomizer = Randomizer()
self.piece = nil
self.ready_frames = 100
self.frames = 0
self.game_over_frames = 0
self.score = 0
self.level = 0
self.lines = 0
self.drop_bonus = 0
self.are = 0
self.lcd = 0
self.das = { direction = "none", frames = -1 }
self.move = "none"
self.prev_inputs = {}
self.next_queue = {}
self.game_over = false
self.clear = false
self.completed = false
-- configurable parameters
self.lock_drop = false
self.lock_hard_drop = false
self.instant_hard_drop = false
self.instant_soft_drop = true
self.enable_hold = false
self.enable_hard_drop = true
self.next_queue_length = 1
self.draw_section_times = false
self.draw_secondary_section_times = false
-- variables related to configurable parameters
self.drop_locked = false
self.hard_drop_locked = false
self.hold_queue = nil
self.held = false
self.section_start_time = 0
self.section_times = { [0] = 0 }
self.secondary_section_times = { [0] = 0 }
end
function GameMode:initialize()
-- after all the variables are initialized, run initialization procedures
for i = 1, 30 do
table.insert(self.next_queue, self:getNextPiece(ruleset))
end
end
function GameMode:getARR() return 1 end
function GameMode:getDropSpeed() return 1 end
function GameMode:getARE() return 25 end
function GameMode:getLineARE() return 25 end
function GameMode:getLockDelay() return 30 end
function GameMode:getLineClearDelay() return 40 end
function GameMode:getDasLimit() return 15 end
function GameMode:getNextPiece(ruleset)
return {
skin = "2tie",
shape = self.randomizer:nextPiece(),
orientation = ruleset:getDefaultOrientation(),
}
end
function GameMode:initialize(ruleset)
-- generate next queue
for i = 1, self.next_queue_length do
table.insert(self.next_queue, self:getNextPiece(ruleset))
end
end
function GameMode:update(inputs, ruleset)
if self.game_over then
self.game_over_frames = self.game_over_frames + 1
if self.game_over_frames >= 60 then
self.completed = true
end
return
end
if self.completed then return end
-- advance one frame
if self:advanceOneFrame(inputs) == false then return end
self:chargeDAS(inputs, self:getDasLimit(), self.getARR())
if self.piece == nil then
self:processDelays(inputs, ruleset)
else
-- perform active frame actions such as fading out the next queue
self:whilePieceActive()
local gravity = self:getGravity()
if self.enable_hold and inputs["hold"] == true and self.held == false then
self:hold(inputs, ruleset)
self.prev_inputs = inputs
return
end
if self.lock_drop and inputs["down"] ~= true then
self.drop_locked = false
end
if self.lock_hard_drop and inputs["up"] ~= true then
self.hard_drop_locked = false
end
ruleset:processPiece(
inputs, self.piece, self.grid, self:getGravity(), self.prev_inputs,
self.move, self:getLockDelay(), self:getDropSpeed(),
self.drop_locked, self.hard_drop_locked, self.enable_hard_drop
)
if inputs["up"] == true and
self.piece:isDropBlocked(self.grid) and
not self.hard_drop_locked and
self.instant_hard_drop
then
self.piece.locked = true
end
if inputs["down"] == true and
self.piece:isDropBlocked(self.grid) and
not self.drop_locked and
self.instant_soft_drop
then
self.piece.locked = true
end
if self.piece.locked == true then
self.grid:applyPiece(self.piece)
self:onPieceLock(self.piece)
self.piece = nil
if self.enable_hold then
self.held = false
end
self.grid:markClearedRows()
local cleared_row_count = self.grid:getClearedRowCount()
self:updateScore(self.level, self.drop_bonus, cleared_row_count)
if cleared_row_count > 0 then
self.lcd = self:getLineClearDelay()
self.are = self:getLineARE()
if self.lcd == 0 then
self.grid:clearClearedRows()
if self.are == 0 then
self:initializeOrHold(inputs, ruleset)
end
end
self:onLineClear(cleared_row_count)
else
if self:getARE() == 0 then
self:initializeOrHold(inputs, ruleset)
else
self.are = self:getARE()
end
end
end
end
self.prev_inputs = inputs
end
function GameMode:updateScore() end
function GameMode:advanceOneFrame()
if self.clear then
self.completed = true
elseif self.ready_frames == 0 then
self.frames = self.frames + 1
end
end
-- event functions
function GameMode:whilePieceActive() end
function GameMode:onPieceLock(piece) end
function GameMode:onLineClear(cleared_row_count) end
function GameMode:onPieceEnter() end
function GameMode:onHold() end
function GameMode:onGameOver()
switchBGM(nil)
end
function GameMode:chargeDAS(inputs)
if inputs[self.das.direction] == true 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.move = "right"
self.das = { direction = "right", frames = 0 }
elseif inputs["left"] == true then
self.move = "left"
self.das = { direction = "left", frames = 0 }
else
self.move = "none"
self.das = { direction = "none", frames = -1 }
end
end
function GameMode:processDelays(inputs, ruleset, drop_speed)
if self.ready_frames > 0 then
self.ready_frames = self.ready_frames - 1
if self.ready_frames == 0 then
self:initializeOrHold(inputs, ruleset)
end
elseif self.lcd > 0 then
self.lcd = self.lcd - 1
if self.lcd == 0 then
self.grid:clearClearedRows()
if self.are == 0 then
self:initializeOrHold(inputs, ruleset)
end
end
elseif self.are > 0 then
self.are = self.are - 1
if self.are == 0 then
self:initializeOrHold(inputs, ruleset)
end
end
end
function GameMode:initializeOrHold(inputs, ruleset)
if self.enable_hold and inputs["hold"] == true then
self:hold(inputs, ruleset)
else
self:initializeNextPiece(inputs, ruleset, self.next_queue[1])
end
self:onPieceEnter()
if not self.grid:canPlacePiece(self.piece) then
self:onGameOver()
self.game_over = true
end
end
function GameMode:hold(inputs, ruleset)
local data = copy(self.hold_queue)
if self.piece == nil then
self.hold_queue = self.next_queue[1]
table.remove(self.next_queue, 1)
table.insert(self.next_queue, self:getNextPiece(ruleset))
else
self.hold_queue = {
skin = self.piece.skin,
shape = self.piece.shape,
orientation = ruleset:getDefaultOrientation(),
}
end
if data == nil then
self:initializeNextPiece(inputs, ruleset, self.next_queue[1])
else
self:initializeNextPiece(inputs, ruleset, data, false)
end
self.held = true
self:onHold()
end
function GameMode:initializeNextPiece(inputs, ruleset, piece_data, generate_next_piece)
local gravity = self:getGravity()
self.piece = ruleset:initializePiece(
inputs, piece_data, self.grid, gravity,
self.prev_inputs, self.move,
self:getLockDelay(), self:getDropSpeed(),
self.lock_drop, self.lock_hard_drop
)
if self.lock_drop then
self.drop_locked = true
end
if self.lock_hard_drop then
self.hard_drop_locked = true
end
if generate_next_piece == nil then
table.remove(self.next_queue, 1)
table.insert(self.next_queue, self:getNextPiece(ruleset))
end
self:playNextSound()
end
function GameMode:playNextSound()
playSE("blocks", self.next_queue[1].shape)
end
function GameMode:getHighScoreData()
return {
score = self.score
}
end
function GameMode:drawPiece()
if self.piece ~= nil then
self.piece:draw(
1,
self:getLockDelay() == 0 and 1 or
(0.25 + 0.75 * math.max(1 - self.piece.gravity, 1 - (self.piece.lock_delay / self:getLockDelay()))),
self.grid
)
end
end
function GameMode:drawGhostPiece(ruleset)
if self.piece == nil then return end
local ghost_piece = self.piece:withOffset({x=0, y=0})
ghost_piece.ghost = true
ghost_piece:dropToBottom(self.grid)
ghost_piece:draw(0.5)
end
function GameMode:drawNextQueue(ruleset)
function drawPiece(piece, skin, offsets, pos_x, pos_y)
for index, offset in pairs(offsets) do
local x = ruleset.spawn_positions[piece].x + offset.x
local y = ruleset.spawn_positions[piece].y + offset.y
love.graphics.draw(blocks[skin][piece], pos_x+x*16, pos_y+y*16)
end
end
for i = 1, self.next_queue_length do
self:setNextOpacity(i)
local next_piece = self.next_queue[i].shape
local skin = self.next_queue[i].skin
local rotation = self.next_queue[i].orientation
if config.side_next then -- next at side
drawPiece(next_piece, skin, ruleset.block_offsets[next_piece][rotation], 192, -16+i*48)
else -- next at top
drawPiece(next_piece, skin, ruleset.block_offsets[next_piece][rotation], -16+i*80, -32)
end
end
if self.hold_queue ~= nil then
self:setHoldOpacity()
drawPiece(
self.hold_queue.shape,
self.hold_queue.skin,
ruleset.block_offsets[self.hold_queue.shape][self.hold_queue.orientation],
-16, -32
)
end
return false
end
function GameMode:setNextOpacity(i) love.graphics.setColor(1, 1, 1, 1) end
function GameMode:setHoldOpacity() love.graphics.setColor(1, 1, 1, 1) end
function GameMode:drawScoringInfo()
love.graphics.setColor(1, 1, 1, 1)
love.graphics.setFont(font_3x5_2)
if config["side_next"] then
love.graphics.printf("NEXT", 240, 72, 40, "left")
else
love.graphics.printf("NEXT", 64, 40, 40, "left")
end
love.graphics.print(
self.das.direction .. " " ..
self.das.frames .. " " ..
st(self.prev_inputs)
)
love.graphics.setFont(font_8x11)
love.graphics.printf(formatTime(self.frames), 64, 420, 160, "center")
end
function GameMode:drawSectionTimes(current_section)
local section_x = 530
for section, time in pairs(self.section_times) do
if section > 0 then
love.graphics.printf(formatTime(time), section_x, 40 + 20 * section, 90, "left")
end
end
love.graphics.printf(formatTime(self.frames - self.section_start_time), section_x, 40 + 20 * current_section, 90, "left")
end
function GameMode:drawSectionTimesWithSecondary(current_section)
local section_x = 530
local section_secondary_x = 440
for section, time in pairs(self.section_times) do
if section > 0 then
love.graphics.printf(formatTime(time), section_x, 40 + 20 * section, 90, "left")
end
end
for section, time in pairs(self.secondary_section_times) do
if section > 0 then
love.graphics.printf(formatTime(time), section_secondary_x, 40 + 20 * section, 90, "left")
end
end
local current_x
if table.getn(self.section_times) < table.getn(self.secondary_section_times) then
current_x = section_x
else
current_x = section_secondary_x
end
love.graphics.printf(formatTime(self.frames - self.section_start_time), current_x, 40 + 20 * current_section, 90, "left")
end
function GameMode:drawSectionTimesWithSplits(current_section)
local section_x = 440
local split_x = 530
local split_time = 0
for section, time in pairs(self.section_times) do
if section > 0 then
love.graphics.printf(formatTime(time), section_x, 40 + 20 * section, 90, "left")
split_time = split_time + time
love.graphics.printf(formatTime(split_time), split_x, 40 + 20 * section, 90, "left")
end
end
love.graphics.printf(formatTime(self.frames - self.section_start_time), section_x, 40 + 20 * current_section, 90, "left")
love.graphics.printf(formatTime(self.frames), split_x, 40 + 20 * current_section, 90, "left")
end
function GameMode:drawCustom() end
return GameMode

View File

@ -0,0 +1,155 @@
require 'funcs'
local GameMode = require 'tetris.modes.gamemode'
local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local IntervalTrainingGame = GameMode:extend()
IntervalTrainingGame.name = "Interval Training"
IntervalTrainingGame.hash = "IntervalTraining"
IntervalTrainingGame.tagline = "Can you clear the time hurdles when the game goes this fast?"
IntervalTrainingGame.arr = 1
IntervalTrainingGame.drop_speed = 1
function IntervalTrainingGame:new()
IntervalTrainingGame.super:new()
self.roll_frames = 0
self.combo = 1
self.randomizer = History6RollsRandomizer()
self.section_time_limit = 1800
self.section_start_time = 0
self.section_times = { [0] = 0 }
self.lock_drop = true
self.enable_hold = true
self.next_queue_length = 3
end
function IntervalTrainingGame:getARE()
return 4
end
function IntervalTrainingGame:getLineARE()
return 4
end
function IntervalTrainingGame:getDasLimit()
return 6
end
function IntervalTrainingGame:getLineClearDelay()
return 6
end
function IntervalTrainingGame:getLockDelay()
return 15
end
function IntervalTrainingGame:getGravity()
return 20
end
function IntervalTrainingGame:getSection()
return math.floor(level / 100) + 1
end
function IntervalTrainingGame:advanceOneFrame()
if self.clear then
self.roll_frames = self.roll_frames + 1
if self.roll_frames > 2968 then
self.completed = true
end
return false
elseif self.ready_frames == 0 then
self.frames = self.frames + 1
if self:getSectionTime() >= self.section_time_limit then
self.game_over = true
end
end
return true
end
function IntervalTrainingGame:onPieceEnter()
if (self.level % 100 ~= 99 or self.level == 998) and not self.clear and self.frames ~= 0 then
self.level = self.level + 1
end
end
function IntervalTrainingGame:onLineClear(cleared_row_count)
if not self.clear then
local new_level = self.level + cleared_row_count
self:updateSectionTimes(self.level, new_level)
self.level = math.min(new_level, 999)
if self.level == 999 then
self.clear = true
end
end
end
function IntervalTrainingGame:getSectionTime()
return self.frames - self.section_start_time
end
function IntervalTrainingGame:updateSectionTimes(old_level, new_level)
if math.floor(old_level / 100) < math.floor(new_level / 100) then
-- record new section
table.insert(self.section_times, self:getSectionTime())
self.section_start_time = self.frames
else
self.level = math.min(new_level, 999)
end
end
function IntervalTrainingGame:drawGrid(ruleset)
self.grid:draw()
end
function IntervalTrainingGame:getHighscoreData()
return {
level = self.level,
frames = self.frames,
}
end
function IntervalTrainingGame:drawScoringInfo()
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("TIME LEFT", 240, 250, 80, "left")
love.graphics.printf("LEVEL", 240, 320, 40, "left")
local current_section = math.floor(self.level / 100) + 1
self:drawSectionTimesWithSplits(current_section)
love.graphics.setFont(font_3x5_3)
love.graphics.printf(self.level, 240, 340, 40, "right")
-- draw time left, flash red if necessary
local time_left = self.section_time_limit - math.max(self:getSectionTime(), 0)
if not self.game_over and not self.clear and time_left < sp(0,10) and time_left % 4 < 2 then
love.graphics.setColor(1, 0.3, 0.3, 1)
end
love.graphics.printf(formatTime(time_left), 240, 270, 160, "left")
love.graphics.setColor(1, 1, 1, 1)
love.graphics.printf(self:getSectionEndLevel(), 240, 370, 40, "right")
end
function IntervalTrainingGame:getSectionEndLevel()
if self.level > 900 then return 999
else return math.floor(self.level / 100 + 1) * 100 end
end
function IntervalTrainingGame:getBackground()
return math.floor(self.level / 100)
end
return IntervalTrainingGame

131
tetris/modes/ligne.lua Normal file
View File

@ -0,0 +1,131 @@
require 'funcs'
local GameMode = require 'tetris.modes.gamemode'
local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local LigneGame = GameMode:extend()
LigneGame.name = "Ligne"
LigneGame.hash = "Ligne"
LigneGame.tagline = "How fast can you clear 40 lines?"
LigneGame.arr = 1
LigneGame.drop_speed = 1
function LigneGame:new()
LigneGame.super:new()
self.lines = 0
self.line_goal = 40
self.pieces = 0
self.randomizer = History6RollsRandomizer()
self.roll_frames = 0
self.lock_drop = true
self.lock_hard_drop = true
self.instant_hard_drop = true
self.instant_soft_drop = false
self.enable_hold = true
self.next_queue_length = 3
end
function LigneGame:getDropSpeed()
return 20
end
function LigneGame:getARR()
return 0
end
function LigneGame:getARE()
return 0
end
function LigneGame:getLineARE()
return self:getARE()
end
function LigneGame:getDasLimit()
return 6
end
function LigneGame:getLineClearDelay()
return 0
end
function LigneGame:getLockDelay()
return 15
end
function LigneGame:getGravity()
return 1/64
end
function LigneGame:advanceOneFrame()
if self.clear then
self.roll_frames = self.roll_frames + 1
if self.roll_frames > 150 then
self.completed = true
end
return false
elseif self.ready_frames == 0 then
self.frames = self.frames + 1
end
return true
end
function LigneGame:onPieceLock()
self.pieces = self.pieces + 1
end
function LigneGame:onLineClear(cleared_row_count)
if not self.clear then
self.lines = self.lines + cleared_row_count
if self.lines >= self.line_goal then
self.clear = true
end
end
end
function LigneGame:drawGrid(ruleset)
self.grid:draw()
if self.piece ~= nil then
self:drawGhostPiece(ruleset)
end
end
function LigneGame:getHighscoreData()
return {
level = self.level,
frames = self.frames,
}
end
function LigneGame:drawScoringInfo()
LigneGame.super.drawScoringInfo(self)
love.graphics.setColor(1, 1, 1, 1)
local text_x = config["side_next"] and 320 or 240
love.graphics.setFont(font_3x5_2)
love.graphics.printf("NEXT", 64, 40, 40, "left")
love.graphics.printf("LINES", text_x, 320, 40, "left")
love.graphics.printf("line/min", text_x, 160, 80, "left")
love.graphics.printf("piece/sec", text_x, 220, 80, "left")
love.graphics.setFont(font_3x5_3)
love.graphics.printf(string.format("%.02f", self.lines / math.max(1, self.frames) * 3600), text_x, 180, 80, "left")
love.graphics.printf(string.format("%.04f", self.pieces / math.max(1, self.frames) * 60), text_x, 240, 80, "left")
love.graphics.setFont(font_3x5_4)
love.graphics.printf(math.max(0, self.line_goal - self.lines), text_x, 340, 40, "left")
end
function LigneGame:getBackground()
return 2
end
return LigneGame

View File

@ -0,0 +1,446 @@
require 'funcs'
local GameMode = require 'tetris.modes.gamemode'
local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local Marathon2020Game = GameMode:extend()
Marathon2020Game.name = "Marathon 2020"
Marathon2020Game.hash = "Marathon2020"
Marathon2020Game.tagline = "2020 levels of pure pain! Can you achieve the World Master rank?"
function Marathon2020Game:new()
Marathon2020Game.super:new()
self.lock_drop = true
self.lock_hard_drop = true
self.enable_hold = true
self.next_queue_length = 3
self.delay_level = 0
self.roll_frames = 0
self.no_roll_frames = 0
self.combo = 1
self.randomizer = History6RollsRandomizer()
self.section_cool_count = 0
self.section_status = { [0] = "none" }
self.torikan_passed = {
[500] = false, [900] = false,
[1000] = false, [1500] = false, [1900] = false
}
self.torikan_hit = false
self.grade = 0
self.grade_points = 0
self.grade_point_decay_counter = 0
self.max_grade_points = 0
end
function Marathon2020Game:getARE()
if self.delay_level < 1 then return 27
elseif self.delay_level < 2 then return 24
elseif self.delay_level < 3 then return 21
elseif self.delay_level < 4 then return 18
elseif self.delay_level < 5 then return 16
elseif self.delay_level < 6 then return 14
elseif self.delay_level < 7 then return 12
elseif self.delay_level < 8 then return 10
elseif self.delay_level < 9 then return 8
elseif self.delay_level < 13 then return 6
elseif self.delay_level < 15 then return 5
else return 4 end
end
function Marathon2020Game:getLineARE()
return self:getARE()
end
function Marathon2020Game:getDasLimit()
if self.delay_level < 1 then return 15
elseif self.delay_level < 3 then return 12
elseif self.delay_level < 5 then return 9
elseif self.delay_level < 8 then return 8
elseif self.delay_level < 10 then return 7
elseif self.delay_level < 13 then return 6
elseif self.delay_level < 15 then return 5
elseif self.delay_level < 20 then return 4
else return 3 end
end
function Marathon2020Game:getLineClearDelay()
if self.delay_level < 1 then return 40
elseif self.delay_level < 3 then return 25
elseif self.delay_level < 4 then return 20
elseif self.delay_level < 5 then return 15
elseif self.delay_level < 7 then return 12
elseif self.delay_level < 9 then return 8
elseif self.delay_level < 11 then return 6
elseif self.delay_level < 14 then return 4
else return 2 end
end
function Marathon2020Game:getLockDelay()
if self.delay_level < 6 then return 30
elseif self.delay_level < 7 then return 26
elseif self.delay_level < 8 then return 22
elseif self.delay_level < 9 then return 19
elseif self.delay_level < 10 then return 17
elseif self.delay_level < 16 then return 15
elseif self.delay_level < 17 then return 13
elseif self.delay_level < 18 then return 11
elseif self.delay_level < 19 then return 10
elseif self.delay_level < 20 then return 9
else return 8 end
end
function Marathon2020Game:getGravity()
if self.level < 30 then return 4/256
elseif self.level < 35 then return 6/256
elseif self.level < 40 then return 8/256
elseif self.level < 50 then return 10/256
elseif self.level < 60 then return 12/256
elseif self.level < 70 then return 16/256
elseif self.level < 80 then return 32/256
elseif self.level < 90 then return 48/256
elseif self.level < 100 then return 64/256
elseif self.level < 120 then return 80/256
elseif self.level < 140 then return 96/256
elseif self.level < 160 then return 112/256
elseif self.level < 170 then return 128/256
elseif self.level < 200 then return 144/256
elseif self.level < 220 then return 4/256
elseif self.level < 230 then return 32/256
elseif self.level < 233 then return 64/256
elseif self.level < 236 then return 96/256
elseif self.level < 239 then return 128/256
elseif self.level < 243 then return 160/256
elseif self.level < 247 then return 192/256
elseif self.level < 251 then return 224/256
elseif self.level < 300 then return 1
elseif self.level < 330 then return 2
elseif self.level < 360 then return 3
elseif self.level < 400 then return 4
elseif self.level < 420 then return 5
elseif self.level < 450 then return 4
elseif self.level < 500 then return 3
else return 20 end
end
local cleared_row_levels = {1, 2, 4, 6}
function Marathon2020Game:advanceOneFrame()
if self.torikan_hit then
self.no_roll_frames = self.no_roll_frames + 1
if self.no_roll_frames > 120 then
self.completed = true
end
return false
elseif self.clear then
self.roll_frames = self.roll_frames + 1
if self.roll_frames < 0 then
return false
elseif self.roll_frames > 4000 then
self.completed = true
end
elseif self.ready_frames == 0 then
self.frames = self.frames + 1
end
return true
end
local cool_cutoffs = {
sp(0,45,00), sp(0,41,50), sp(0,38,50), sp(0,35,00), sp(0,32,50),
sp(0,29,20), sp(0,27,20), sp(0,24,80), sp(0,22,80), sp(0,20,60),
sp(0,19,60), sp(0,19,40), sp(0,19,40), sp(0,18,40), sp(0,18,20),
sp(0,16,20), sp(0,16,20), sp(0,16,20), sp(0,16,20), sp(0,16,20),
sp(0,15,20)
}
local levels_for_cleared_rows = { 1, 2, 4, 6 }
function Marathon2020Game:onPieceEnter()
self:updateLevel(1, false)
end
function Marathon2020Game:whilePieceActive()
self.grade_point_decay_counter = self.grade_point_decay_counter + self.grade + 2
if self.grade_point_decay_counter > 240 then
self.grade_point_decay_counter = 0
self.grade_points = math.max(0, self.grade_points - 1)
end
end
function Marathon2020Game:onLineClear(cleared_row_count)
self:updateLevel(levels_for_cleared_rows[cleared_row_count], true)
self:updateGrade(cleared_row_count)
end
function Marathon2020Game:updateLevel(increment, line_clear)
local new_level
if self.torikan_passed[900] == false then
if line_clear == false and (
math.floor((self.level + increment) / 100) > math.floor(self.level / 100) or
self.level == 998
) then
new_level = math.min(998, self.level + (99 - self.level % 100))
else
new_level = math.min(999, self.level + increment)
end
elseif self.torikan_passed[1900] == false then
if line_clear == false and (
math.floor((self.level + increment) / 100) > math.floor(self.level / 100) or
self.level == 1999
) then
new_level = math.min(1999, self.level + (99 - self.level % 100))
else
new_level = math.min(2000, self.level + increment)
end
else
if line_clear == false and (
self.level < 1900 and
math.floor((self.level + increment) / 100) > math.floor(self.level / 100)
) then
new_level = self.level + (99 - self.level % 100)
elseif line_clear == false and self.level + increment > 2019 then
new_level = 2019
else
new_level = math.min(2020, self.level + increment)
end
end
if not self.clear then
self:updateSectionTimes(self.level, new_level)
if not self.clear then
self.level = new_level
end
end
end
local low_cleared_line_points = {10, 20, 30, 40}
local mid_cleared_line_points = {2, 6, 12, 24}
local high_cleared_line_points = {1, 4, 9, 20}
local function getGradeForGradePoints(points)
return math.floor(math.sqrt((points / 50) * 8 + 1) / 2 - 0.5)
-- Don't be afraid of the above function. All it does is make it so that
-- you need 50 points to get to grade 1, 100 points to grade 2, etc.
end
function Marathon2020Game:updateGrade(cleared_lines)
-- update grade points and max grade points
local point_level = math.floor(self.level / 100) + self.delay_level
local plus_points = math.max(
low_cleared_line_points[cleared_lines],
mid_cleared_line_points[cleared_lines] * (1 + point_level / 2),
high_cleared_line_points[cleared_lines] * (point_level - 2),
(self.level >= 1000 and cleared_lines == 4) and self.grade * 30 or 0
)
self.grade_points = self.grade_points + plus_points
if self.grade_points > self.max_grade_points then
self.max_grade_points = self.grade_points
end
self.grade = getGradeForGradePoints(self.max_grade_points)
end
function Marathon2020Game:getTotalGrade()
return self.grade + self.section_cool_count
end
local function getSectionForLevel(level)
if level < 2001 then
return math.floor(level / 100) + 1
else
return 20
end
end
function Marathon2020Game:getEndOfSectionForSection(section)
if self.torikan_passed[900] == false and section == 10 then return 999
elseif self.torikan_passed[1900] == false and section == 20 then return 2000
elseif section == 20 then return 2020
else return section * 100 end
end
function Marathon2020Game:sectionPassed(old_level, new_level)
if self.torikan_passed[900] == false then
return (
(math.floor(old_level / 100) < math.floor(new_level / 100)) or
(new_level >= 999)
)
elseif self.torikan_passed[1900] == false then
return (
(math.floor(old_level / 100) < math.floor(new_level / 100)) or
(new_level >= 2000)
)
else
return (
(new_level < 2001 and math.floor(old_level / 100) < math.floor(new_level / 100)) or
(new_level >= 2020)
)
end
end
function Marathon2020Game:checkTorikan(section)
if section == 5 and self.frames < sp(6,00,00) then self.torikan_passed[500] = true end
if section == 9 and self.frames < sp(8,30,00) then self.torikan_passed[900] = true end
if section == 10 and self.frames < sp(8,45,00) then self.torikan_passed[1000] = true end
if section == 15 and self.frames < sp(11,30,00) then self.torikan_passed[1500] = true end
if section == 19 and self.frames < sp(13,15,00) then self.torikan_passed[1900] = true end
end
function Marathon2020Game:checkClear(level)
if (
self.torikan_passed[500] == false and level >= 500 or
self.torikan_passed[900] == false and level >= 999 or
self.torikan_passed[1000] == false and level >= 1000 or
self.torikan_passed[1500] == false and level >= 1500 or
self.torikan_passed[1900] == false and level >= 2000 or
level >= 2020
) then
if self.torikan_passed[500] == false then self.level = 500
elseif self.torikan_passed[900] == false then self.level = 999
elseif self.torikan_passed[1000] == false then self.level = 1000
elseif self.torikan_passed[1500] == false then self.level = 1500
elseif self.torikan_passed[1900] == false then self.level = 2000
else self.level = 2020 end
self.clear = true
self.grid:clear()
if (
self.torikan_passed[900] == false and level >= 999 or
level >= 2020
) then
self.roll_frames = -150
else
self.torikan_hit = true
self.no_roll_frames = -150
end
end
end
function Marathon2020Game:updateSectionTimes(old_level, new_level)
function sectionCool()
self.section_cool_count = self.section_cool_count + 1
self.delay_level = math.min(20, self.delay_level + 1)
table.insert(self.section_status, "cool")
end
local section = getSectionForLevel(old_level)
if section <= 19 and old_level % 100 < 70 and new_level >= math.floor(old_level / 100) * 100 + 70 then
-- record section 70 time
section_70_time = self.frames - self.section_start_time
table.insert(self.secondary_section_times, section_70_time)
end
if self:sectionPassed(old_level, new_level) then
-- record new section
section_time = self.frames - self.section_start_time
table.insert(self.section_times, section_time)
self.section_start_time = self.frames
if section > 4 then self.delay_level = math.min(20, self.delay_level + 1) end
self:checkTorikan(section)
self:checkClear(new_level)
if (
section <= 19 and self.section_status[section - 1] == "cool" and
self.secondary_section_times[section] < self.secondary_section_times[section - 1] + 120 and
self.secondary_section_times[section] < cool_cutoffs[section]
) then
sectionCool()
elseif self.section_status[section - 1] == "cool" then
table.insert(self.section_status, "none")
elseif section <= 19 and self.secondary_section_times[section] < cool_cutoffs[section] then
sectionCool()
else
table.insert(self.section_status, "none")
end
end
end
function Marathon2020Game:updateScore(level, drop_bonus, cleared_lines)
if cleared_lines > 0 then
self.score = self.score + (
(math.ceil((level + cleared_lines) / 4) + drop_bonus) *
cleared_lines * (cleared_lines * 2 - 1) * (self.combo * 2 - 1)
)
self.lines = self.lines + cleared_lines
self.combo = self.combo + cleared_lines - 1
else
self.drop_bonus = 0
self.combo = 1
end
end
Marathon2020Game.mRollOpacityFunction = function(age)
if age > 300 then return 0
elseif age < 240 then return 1
else return (300 - age) / 60 end
end
Marathon2020Game.rollOpacityFunction = function(age)
if age > 4 then return 0
else return 1 - age / 4 end
end
function Marathon2020Game:qualifiesForMRoll()
return false -- until I actually have grading working
end
function Marathon2020Game:drawGrid()
if self.clear and not (self.completed or self.game_over) then
if self:qualifiesForMRoll() then
self.grid:drawInvisible(self.mRollOpacityFunction)
else
self.grid:drawInvisible(self.rollOpacityFunction)
end
else
self.grid:draw()
if self.piece ~= nil and self.level < 100 then
self:drawGhostPiece(ruleset)
end
end
end
function Marathon2020Game:drawScoringInfo()
Marathon2020Game.super.drawScoringInfo(self)
local current_section = getSectionForLevel(self.level)
local text_x = config["side_next"] and 320 or 240
love.graphics.setFont(font_3x5_2)
love.graphics.printf("GRADE", text_x, 100, 40, "left")
love.graphics.printf("GRADE PTS.", text_x, 200, 90, "left")
love.graphics.printf("LEVEL", text_x, 320, 40, "left")
self:drawSectionTimesWithSecondary(current_section)
love.graphics.setFont(font_3x5_3)
love.graphics.printf(self:getTotalGrade(), text_x, 120, 90, "left")
love.graphics.printf(self.grade_points, text_x, 220, 90, "left")
love.graphics.printf(self.level, text_x, 340, 50, "right")
if self.clear then
love.graphics.printf(self.level, text_x, 370, 50, "right")
else
love.graphics.printf(self:getEndOfSectionForSection(current_section), text_x, 370, 50, "right")
end
end
function Marathon2020Game:getHighscoreData()
return {
grade = self.grade,
level = self.level,
frames = self.frames,
}
end
function Marathon2020Game:getBackground()
return math.min(19, math.floor(self.level / 100))
end
return Marathon2020Game

View File

@ -0,0 +1,226 @@
require 'funcs'
local GameMode = require 'tetris.modes.gamemode'
local Piece = require 'tetris.components.piece'
local History4RollsRandomizer = require 'tetris.randomizers.history_4rolls'
local MarathonA1Game = GameMode:extend()
MarathonA1Game.name = "Marathon A1"
MarathonA1Game.hash = "MarathonA1"
MarathonA1Game.tagline = "Can you score enough points to reach the title of Grand Master?"
MarathonA1Game.arr = 1
MarathonA1Game.drop_speed = 1
function MarathonA1Game:new()
MarathonA1Game.super:new()
self.roll_frames = 0
self.combo = 1
self.gm_conditions = {
level300 = false,
level500 = false,
level999 = false
}
self.randomizer = History4RollsRandomizer()
self.lock_drop = false
self.enable_hard_drop = false
self.enable_hold = false
self.next_queue_length = 1
end
function MarathonA1Game:getARE()
return 25
end
function MarathonA1Game:getLineARE()
return 25
end
function MarathonA1Game:getDasLimit()
return 15
end
function MarathonA1Game:getLineClearDelay()
return 40
end
function MarathonA1Game:getLockDelay()
return 30
end
local function getRankForScore(score)
if score < 400 then return {rank = "9", next = 400}
elseif score < 800 then return {rank = "8", next = 800}
elseif score < 1400 then return {rank = "7", next = 1400}
elseif score < 2000 then return {rank = "6", next = 2000}
elseif score < 3500 then return {rank = "5", next = 3500}
elseif score < 5500 then return {rank = "4", next = 5500}
elseif score < 8000 then return {rank = "3", next = 8000}
elseif score < 12000 then return {rank = "2", next = 12000}
elseif score < 16000 then return {rank = "1", next = 16000}
elseif score < 22000 then return {rank = "S1", next = 22000}
elseif score < 30000 then return {rank = "S2", next = 30000}
elseif score < 40000 then return {rank = "S3", next = 40000}
elseif score < 52000 then return {rank = "S4", next = 52000}
elseif score < 66000 then return {rank = "S5", next = 66000}
elseif score < 82000 then return {rank = "S6", next = 82000}
elseif score < 100000 then return {rank = "S7", next = 100000}
elseif score < 120000 then return {rank = "S8", next = 120000}
else return {rank = "S9", next = "???"}
end
end
function MarathonA1Game:getGravity()
local level = self.level
if (level < 30) then return 4/256
elseif (level < 35) then return 6/256
elseif (level < 40) then return 8/256
elseif (level < 50) then return 10/256
elseif (level < 60) then return 12/256
elseif (level < 70) then return 16/256
elseif (level < 80) then return 32/256
elseif (level < 90) then return 48/256
elseif (level < 100) then return 64/256
elseif (level < 120) then return 80/256
elseif (level < 140) then return 96/256
elseif (level < 160) then return 112/256
elseif (level < 170) then return 128/256
elseif (level < 200) then return 144/256
elseif (level < 220) then return 4/256
elseif (level < 230) then return 32/256
elseif (level < 233) then return 64/256
elseif (level < 236) then return 96/256
elseif (level < 239) then return 128/256
elseif (level < 243) then return 160/256
elseif (level < 247) then return 192/256
elseif (level < 251) then return 224/256
elseif (level < 300) then return 1
elseif (level < 330) then return 2
elseif (level < 360) then return 3
elseif (level < 400) then return 4
elseif (level < 420) then return 5
elseif (level < 450) then return 4
elseif (level < 500) then return 3
else return 20
end
end
function MarathonA1Game:advanceOneFrame()
if self.clear then
self.roll_frames = self.roll_frames + 1
if self.roll_frames > 2968 then
self.completed = true
end
elseif self.ready_frames == 0 then
self.frames = self.frames + 1
end
return true
end
function MarathonA1Game:onPieceEnter()
if (self.level % 100 ~= 99 and self.level ~= 998) and not self.clear and self.frames ~= 0 then
self.level = self.level + 1
end
end
function MarathonA1Game:onLineClear(cleared_row_count)
self:checkGMRequirements(self.level, self.level + cleared_row_count)
if not self.clear then
local new_level = math.min(self.level + cleared_row_count, 999)
if self.level == 999 then
self.clear = true
else
self.level = new_level
end
end
end
function MarathonA1Game:updateScore(level, drop_bonus, cleared_lines)
if cleared_lines > 0 then
self.score = self.score + (
(math.ceil((level + cleared_lines) / 4) + drop_bonus) *
cleared_lines * (cleared_lines * 2 - 1) * self.combo
)
self.lines = self.lines + cleared_lines
self.combo = self.combo + (cleared_lines - 1) * 2
else
self.drop_bonus = 0
self.combo = 1
end
end
function MarathonA1Game:checkGMRequirements(old_level, new_level)
if old_level < 300 and new_level >= 300 then
if self.score > 12000 and self.frames <= sp(4,15) then
self.gm_conditions["level300"] = true
end
elseif old_level < 500 and new_level >= 500 then
if self.score > 40000 and self.frames <= sp(7,30) then
self.gm_conditions["level500"] = true
end
elseif old_level < 999 and new_level >= 999 then
if self.score > 126000 and self.frames <= sp(13,30) then
self.gm_conditions["level900"] = true
end
end
end
function MarathonA1Game:drawGrid()
self.grid:draw()
end
function MarathonA1Game:drawScoringInfo()
MarathonA1Game.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("GRADE", 240, 120, 40, "left")
love.graphics.printf("SCORE", 240, 200, 40, "left")
love.graphics.printf("NEXT RANK", 240, 260, 90, "left")
love.graphics.printf("LEVEL", 240, 320, 40, "left")
love.graphics.setFont(font_3x5_3)
love.graphics.printf(self.score, 240, 220, 90, "left")
if self.gm_conditions["level300"] and self.gm_conditions["level500"] and self.gm_conditions["level900"] then
love.graphics.printf("GM", 240, 140, 90, "left")
else
love.graphics.printf(getRankForScore(self.score).rank, 240, 140, 90, "left")
end
love.graphics.printf(getRankForScore(self.score).next, 240, 280, 90, "left")
love.graphics.printf(self.level, 240, 340, 40, "right")
love.graphics.printf(self:getSectionEndLevel(), 240, 370, 40, "right")
love.graphics.setFont(font_8x11)
love.graphics.printf(formatTime(self.frames), 64, 420, 160, "center")
end
function MarathonA1Game:getSectionEndLevel()
if self.level > 900 then return 999
else return math.floor(self.level / 100 + 1) * 100 end
end
function MarathonA1Game:getBackground()
return math.floor(self.level / 100)
end
function MarathonA1Game:getHighscoreData()
return {
grade = self.grade,
score = self.score,
level = self.level,
frames = self.frames,
}
end
return MarathonA1Game

View File

@ -0,0 +1,363 @@
require 'funcs'
local GameMode = require 'tetris.modes.gamemode'
local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local MarathonA2Game = GameMode:extend()
MarathonA2Game.name = "Marathon A2"
MarathonA2Game.hash = "MarathonA2"
MarathonA2Game.tagline = "The points don't matter! Can you reach the invisible roll?"
MarathonA2Game.arr = 1
MarathonA2Game.drop_speed = 1
function MarathonA2Game:new()
MarathonA2Game.super:new()
self.roll_frames = 0
self.combo = 1
self.randomizer = History6RollsRandomizer()
self.grade = 0
self.grade_points = 0
self.grade_point_decay_counter = 0
self.section_start_time = 0
self.section_times = { [0] = 0 }
self.section_tetrises = { [0] = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
self.randomizer = History6RollsRandomizer()
self.lock_drop = false
self.enable_hold = false
self.next_queue_length = 1
end
function MarathonA2Game:getARE()
if self.level < 700 then return 25
elseif self.level < 800 then return 16
else return 12 end
end
function MarathonA2Game:getLineARE()
if self.level < 600 then return 25
elseif self.level < 700 then return 16
elseif self.level < 800 then return 12
else return 6 end
end
function MarathonA2Game:getDasLimit()
if self.level < 500 then return 15
elseif self.level < 900 then return 9
else return 7 end
end
function MarathonA2Game:getLineClearDelay()
if self.level < 500 then return 40
elseif self.level < 600 then return 25
elseif self.level < 700 then return 16
elseif self.level < 800 then return 12
else return 6 end
end
function MarathonA2Game:getLockDelay()
if self.level < 900 then return 30
else return 17 end
end
function MarathonA2Game:getGravity()
if (self.level < 30) then return 4/256
elseif (self.level < 35) then return 6/256
elseif (self.level < 40) then return 8/256
elseif (self.level < 50) then return 10/256
elseif (self.level < 60) then return 12/256
elseif (self.level < 70) then return 16/256
elseif (self.level < 80) then return 32/256
elseif (self.level < 90) then return 48/256
elseif (self.level < 100) then return 64/256
elseif (self.level < 120) then return 80/256
elseif (self.level < 140) then return 96/256
elseif (self.level < 160) then return 112/256
elseif (self.level < 170) then return 128/256
elseif (self.level < 200) then return 144/256
elseif (self.level < 220) then return 4/256
elseif (self.level < 230) then return 32/256
elseif (self.level < 233) then return 64/256
elseif (self.level < 236) then return 96/256
elseif (self.level < 239) then return 128/256
elseif (self.level < 243) then return 160/256
elseif (self.level < 247) then return 192/256
elseif (self.level < 251) then return 224/256
elseif (self.level < 300) then return 1
elseif (self.level < 330) then return 2
elseif (self.level < 360) then return 3
elseif (self.level < 400) then return 4
elseif (self.level < 420) then return 5
elseif (self.level < 450) then return 4
elseif (self.level < 500) then return 3
else return 20
end
end
function MarathonA2Game:advanceOneFrame()
if self.clear then
self.roll_frames = self.roll_frames + 1
if self.roll_frames > 3694 then
self.completed = true
if self.grade == 32 then
self.grade = 33
end
end
elseif self.ready_frames == 0 then
self.frames = self.frames + 1
end
return true
end
function MarathonA2Game:onPieceEnter()
if (self.level % 100 ~= 99 and self.level ~= 998) and not self.clear and self.frames ~= 0 then
self.level = self.level + 1
end
end
function MarathonA2Game:onLineClear(cleared_row_count)
self:updateSectionTimes(self.level, self.level + cleared_row_count)
self.level = math.min(self.level + cleared_row_count, 999)
if self.level == 999 and not self.clear then
self.clear = true
if self:qualifiesForMRoll() then
self.grade = 32
end
self.grid:clear()
self.roll_frames = -150
end
end
function MarathonA2Game:updateScore(level, drop_bonus, cleared_lines)
self:updateGrade(cleared_lines)
if cleared_lines > 0 then
self.score = self.score + (
(math.ceil((level + cleared_lines) / 4) + drop_bonus) *
cleared_lines * (cleared_lines * 2 - 1) * (self.combo * 2 - 1)
)
self.lines = self.lines + cleared_lines
self.combo = self.combo + cleared_lines - 1
else
self.drop_bonus = 0
self.combo = 1
end
end
function MarathonA2Game:updateSectionTimes(old_level, new_level)
if self.clear then return end
if math.floor(old_level / 100) < math.floor(new_level / 100) or
new_level >= 999 then
-- record new section
section_time = self.frames - self.section_start_time
self.section_times[math.floor(old_level / 100)] = section_time
self.section_start_time = self.frames
end
end
local grade_point_bonuses = {
{10, 20, 40, 50},
{10, 20, 30, 40},
{10, 20, 30, 40},
{10, 15, 30, 40},
{10, 15, 20, 40},
{5, 15, 20, 30},
{5, 10, 20, 30},
{5, 10, 15, 30},
{5, 10, 15, 30},
{5, 10, 15, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
}
local grade_point_decays = {
125, 80, 80, 50, 45, 45, 45,
40, 40, 40, 40, 40, 30, 30, 30,
20, 20, 20, 20, 20,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
10, 10
}
local combo_multipliers = {
{1.0, 1.0, 1.0, 1.0},
{1.2, 1.4, 1.5, 1.0},
{1.2, 1.5, 1.8, 1.0},
{1.4, 1.6, 2.0, 1.0},
{1.4, 1.7, 2.2, 1.0},
{1.4, 1.8, 2.3, 1.0},
{1.4, 1.9, 2.4, 1.0},
{1.5, 2.0, 2.5, 1.0},
{1.5, 2.1, 2.6, 1.0},
{2.0, 2.5, 3.0, 1.0},
}
local grade_conversion = {
[0] = 0,
1, 2, 3, 4, 5, 5, 6, 6, 7, 7,
7, 8, 8, 8, 9, 9, 9, 10, 11, 12,
12, 12, 13, 13, 14, 14, 15, 15, 16, 16,
17, 18, 19
}
function MarathonA2Game:updateGrade(cleared_lines)
if self.clear then return end
if cleared_lines == 0 then
self.grade_point_decay_counter = self.grade_point_decay_counter + 1
if self.grade_point_decay_counter >= grade_point_decays[self.grade + 1] then
self.grade_point_decay_counter = 0
self.grade_points = math.max(0, self.grade_points - 1)
end
else
self.grade_points = self.grade_points + (
math.ceil(
grade_point_bonuses[self.grade + 1][cleared_lines] *
combo_multipliers[math.min(self.combo, 10)][cleared_lines]
) * (1 + math.floor(self.level / 250))
)
if self.grade_points >= 100 and self.grade < 31 then
self.grade_points = 0
self.grade = self.grade + 1
end
end
end
local tetris_requirements = { [0] = 2, 2, 2, 2, 2, 1, 1, 1, 1, 1 }
function MarathonA2Game:qualifiesForMRoll()
if not self.clear then return false end
-- tetris requirements
for section = 0, 9 do
if self.section_tetrises[section] < tetris_requirements[section] then
return false
end
end
-- section time requirements
local section_average = 0
for section = 0, 4 do
section_average = section_average + self.section_times[section]
if self.section_times[section] > sp(1,05) then
return false
end
end
-- section time average requirements
if self.section_times[5] > section_average / 5 then
return false
end
for section = 6, 9 do
if self.section_times[section] > self.section_times[section - 1] + 120 then
return false
end
end
if self.grade < 17 or self.frames > sp(8,45) then
return false
end
return true
end
function MarathonA2Game:getLetterGrade()
local grade = grade_conversion[self.grade]
if grade < 9 then
return tostring(9 - grade)
elseif grade < 18 then
return "S" .. tostring(grade - 8)
elseif grade == 18 then
return "M"
else
return "GM"
end
end
MarathonA2Game.rollOpacityFunction = function(age)
if age < 240 then return 1
elseif age > 300 then return 0
else return 1 - (age - 240) / 60 end
end
MarathonA2Game.mRollOpacityFunction = function(age)
if age > 4 then return 0
else return 1 - age / 4 end
end
function MarathonA2Game:drawGrid(ruleset)
if self.clear and not (self.completed or self.game_over) then
if self:qualifiesForMRoll() then
self.grid:drawInvisible(self.mRollOpacityFunction)
else
self.grid:drawInvisible(self.rollOpacityFunction)
end
else
self.grid:draw()
if self.piece ~= nil and self.level < 100 then
self:drawGhostPiece(ruleset)
end
end
end
function MarathonA2Game:drawScoringInfo()
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("GRADE", 240, 120, 40, "left")
love.graphics.printf("SCORE", 240, 200, 40, "left")
love.graphics.printf("LEVEL", 240, 320, 40, "left")
love.graphics.setFont(font_3x5_3)
love.graphics.printf(self:getLetterGrade(), 240, 140, 90, "left")
love.graphics.printf(self.score, 240, 220, 90, "left")
love.graphics.printf(self.level, 240, 340, 40, "right")
love.graphics.printf(self:getSectionEndLevel(), 240, 370, 40, "right")
love.graphics.setFont(font_8x11)
love.graphics.printf(formatTime(self.frames), 64, 420, 160, "center")
end
function MarathonA2Game:getHighscoreData()
return {
grade = grade_conversion[self.grade],
score = self.score,
level = self.level,
frames = self.frames,
}
end
function MarathonA2Game:getSectionEndLevel()
if self.level > 900 then return 999
else return math.floor(self.level / 100 + 1) * 100 end
end
function MarathonA2Game:getBackground()
return math.floor(self.level / 100)
end
return MarathonA2Game

View File

@ -0,0 +1,431 @@
require 'funcs'
local GameMode = require 'tetris.modes.gamemode'
local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls_35bag'
local MarathonA3Game = GameMode:extend()
MarathonA3Game.name = "Marathon A3"
MarathonA3Game.hash = "MarathonA3"
MarathonA3Game.tagline = "The game gets faster way more quickly! Can you get all the Section COOLs?"
MarathonA3Game.arr = 1
MarathonA3Game.drop_speed = 1
function MarathonA3Game:new()
MarathonA3Game.super:new()
self.speed_level = 0
self.roll_frames = 0
self.combo = 1
self.grade = 0
self.grade_points = 0
self.roll_points = 0
self.grade_point_decay_counter = 0
self.section_cool_grade = 0
self.section_status = { [0] = "none" }
self.section_start_time = 0
self.section_70_times = { [0] = 0 }
self.section_times = { [0] = 0 }
self.randomizer = History6RollsRandomizer()
self.lock_drop = true
self.lock_hard_drop = true
self.enable_hold = true
self.next_queue_length = 3
end
function MarathonA3Game:getARE()
if self.speed_level < 700 then return 27
elseif self.speed_level < 800 then return 18
elseif self.speed_level < 1000 then return 14
elseif self.speed_level < 1100 then return 8
elseif self.speed_level < 1200 then return 7
else return 6 end
end
function MarathonA3Game:getLineARE()
if self.speed_level < 600 then return 27
elseif self.speed_level < 700 then return 18
elseif self.speed_level < 800 then return 14
elseif self.speed_level < 1100 then return 8
elseif self.speed_level < 1200 then return 7
else return 6 end
end
function MarathonA3Game:getDasLimit()
if self.speed_level < 500 then return 15
elseif self.speed_level < 900 then return 9
else return 7 end
end
function MarathonA3Game:getLineClearDelay()
if self.speed_level < 500 then return 40
elseif self.speed_level < 600 then return 25
elseif self.speed_level < 700 then return 16
elseif self.speed_level < 800 then return 12
else return 6 end
end
function MarathonA3Game:getLockDelay()
if self.speed_level < 900 then return 30
elseif self.speed_level < 1100 then return 17
else return 15 end
end
function MarathonA3Game:getGravity()
if (self.speed_level < 30) then return 4/256
elseif (self.speed_level < 35) then return 6/256
elseif (self.speed_level < 40) then return 8/256
elseif (self.speed_level < 50) then return 10/256
elseif (self.speed_level < 60) then return 12/256
elseif (self.speed_level < 70) then return 16/256
elseif (self.speed_level < 80) then return 32/256
elseif (self.speed_level < 90) then return 48/256
elseif (self.speed_level < 100) then return 64/256
elseif (self.speed_level < 120) then return 80/256
elseif (self.speed_level < 140) then return 96/256
elseif (self.speed_level < 160) then return 112/256
elseif (self.speed_level < 170) then return 128/256
elseif (self.speed_level < 200) then return 144/256
elseif (self.speed_level < 220) then return 4/256
elseif (self.speed_level < 230) then return 32/256
elseif (self.speed_level < 233) then return 64/256
elseif (self.speed_level < 236) then return 96/256
elseif (self.speed_level < 239) then return 128/256
elseif (self.speed_level < 243) then return 160/256
elseif (self.speed_level < 247) then return 192/256
elseif (self.speed_level < 251) then return 224/256
elseif (self.speed_level < 300) then return 1
elseif (self.speed_level < 330) then return 2
elseif (self.speed_level < 360) then return 3
elseif (self.speed_level < 400) then return 4
elseif (self.speed_level < 420) then return 5
elseif (self.speed_level < 450) then return 4
elseif (self.speed_level < 500) then return 3
else return 20
end
end
function MarathonA3Game:advanceOneFrame()
if self.clear then
self.roll_frames = self.roll_frames + 1
if self.roll_frames < 0 then
if self.roll_frames + 1 == 0 then
switchBGM("credit_roll", "gm3")
end
return
elseif self.roll_frames > 3238 then
if self:qualifiesForMRoll() then
self.roll_points = self.roll_points + 160
else
self.roll_points = self.roll_points + 50
end
switchBGM(nil)
self.completed = true
end
elseif self.ready_frames == 0 then
self.frames = self.frames + 1
end
return true
end
function MarathonA3Game:onPieceEnter()
if (self.level % 100 ~= 99) and self.level ~= 998 and self.frames ~= 0 then
self:updateSectionTimes(self.level, self.level + 1)
self.level = self.level + 1
self.speed_level = self.speed_level + 1
end
end
local cleared_row_levels = {1, 2, 4, 6}
function MarathonA3Game:onLineClear(cleared_row_count)
local advanced_levels = cleared_row_levels[cleared_row_count]
self:updateSectionTimes(self.level, self.level + advanced_levels)
if not self.clear then
self.level = math.min(self.level + advanced_levels, 999)
end
self.speed_level = self.speed_level + advanced_levels
if self.level == 999 and not self.clear then
self.clear = true
self.grid:clear()
self.roll_frames = -150
end
end
local cool_cutoffs = {
sp(0,52), sp(0,52), sp(0,49), sp(0,45), sp(0,45),
sp(0,42), sp(0,42), sp(0,38), sp(0,38),
}
local regret_cutoffs = {
sp(0,90), sp(0,75), sp(0,75), sp(0,68), sp(0,60),
sp(0,60), sp(0,50), sp(0,50), sp(0,50), sp(0,50),
}
function MarathonA3Game:updateSectionTimes(old_level, new_level)
if self.clear then return end
local section = math.floor(old_level / 100) + 1
if math.floor(old_level / 100) < math.floor(new_level / 100) or
new_level >= 999 then
-- record new section
section_time = self.frames - self.section_start_time
table.insert(self.section_times, section_time)
self.section_start_time = self.frames
if section_time > regret_cutoffs[section] then
self.section_cool_grade = self.section_cool_grade - 1
table.insert(self.section_status, "regret")
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_cool_grade = self.section_cool_grade + 1
self.speed_level = self.speed_level + 100
table.insert(self.section_status, "cool")
elseif self.section_status[section - 1] == "cool" then
table.insert(self.section_status, "none")
elseif section <= 9 and self.section_70_times[section] < cool_cutoffs[section] then
self.section_cool_grade = self.section_cool_grade + 1
self.speed_level = self.speed_level + 100
table.insert(self.section_status, "cool")
else
table.insert(self.section_status, "none")
end
elseif section <= 9 and old_level % 100 < 70 and new_level % 100 >= 70 then
-- record section 70 time
section_70_time = self.frames - self.section_start_time
table.insert(self.section_70_times, section_70_time)
end
end
function MarathonA3Game:updateScore(level, drop_bonus, cleared_lines)
self:updateGrade(cleared_lines)
if cleared_lines > 0 then
self.score = self.score + (
(math.ceil((level + cleared_lines) / 4) + drop_bonus) *
cleared_lines * (cleared_lines * 2 - 1) * (self.combo * 2 - 1)
)
self.lines = self.lines + cleared_lines
self.combo = self.combo + cleared_lines - 1
else
self.drop_bonus = 0
self.combo = 1
end
end
local grade_point_bonuses = {
{10, 20, 40, 50},
{10, 20, 30, 40},
{10, 20, 30, 40},
{10, 15, 30, 40},
{10, 15, 20, 40},
{5, 15, 20, 30},
{5, 10, 20, 30},
{5, 10, 15, 30},
{5, 10, 15, 30},
{5, 10, 15, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
{2, 12, 13, 30},
}
local grade_point_decays = {
125, 80, 80, 50, 45, 45, 45,
40, 40, 40, 40, 40, 30, 30, 30,
20, 20, 20, 20, 20,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
10, 10
}
local combo_multipliers = {
{1.0, 1.0, 1.0, 1.0},
{1.0, 1.2, 1.4, 1.5},
{1.0, 1.2, 1.5, 1.8},
{1.0, 1.4, 1.6, 2.0},
{1.0, 1.4, 1.7, 2.2},
{1.0, 1.4, 1.8, 2.3},
{1.0, 1.4, 1.9, 2.4},
{1.0, 1.5, 2.0, 2.5},
{1.0, 1.5, 2.1, 2.6},
{1.0, 2.0, 2.5, 3.0},
}
local roll_points = {4, 8, 12, 26}
local mroll_points = {10, 20, 30, 100}
local grade_conversion = {
[0] = 0,
1, 2, 3, 4, 5, 5, 6, 6, 7, 7,
7, 8, 8, 8, 9, 9, 9, 10, 11, 12, 12,
12, 12, 13, 13, 14, 14, 15, 15, 16, 16,
17
}
function MarathonA3Game:updateGrade(cleared_lines)
if cleared_lines == 0 then
self.grade_point_decay_counter = self.grade_point_decay_counter + 1
if self.grade_point_decay_counter >= grade_point_decays[self.grade + 1] then
self.grade_point_decay_counter = 0
self.grade_points = math.max(0, self.grade_points - 1)
end
else
if self.clear then
if self:qualifiesForMRoll() then
self.roll_points = self.roll_points + mroll_points[cleared_lines]
else
self.roll_points = self.roll_points + roll_points[cleared_lines]
end
else
self.grade_points = self.grade_points + (
math.ceil(
grade_point_bonuses[self.grade + 1][cleared_lines] *
combo_multipliers[math.min(self.combo, 10)][cleared_lines]
) * (1 + math.floor(self.level / 250))
)
if self.grade_points >= 100 and self.grade < 31 then
self.grade_points = 0
self.grade = self.grade + 1
end
end
end
end
function MarathonA3Game:qualifiesForMRoll()
return self.grade >= 27 and self.section_cool_grade >= 9
end
function MarathonA3Game:getAggregateGrade()
return self.section_cool_grade + math.floor(self.roll_points / 100) + grade_conversion[self.grade]
end
local master_grades = { "M", "MK", "MV", "MO", "MM" }
function MarathonA3Game:getLetterGrade()
local grade = self:getAggregateGrade()
if grade < 9 then
return tostring(9 - grade)
elseif grade < 18 then
return "S" .. tostring(grade - 8)
elseif grade < 27 then
return "M" .. tostring(grade - 17)
elseif grade < 32 then
return master_grades[grade - 26]
else
return "GM"
end
end
function MarathonA3Game:drawGrid()
if self.clear and not (self.completed or self.game_over) then
if self:qualifiesForMRoll() then
self.grid:drawInvisible(self.mRollOpacityFunction)
else
self.grid:drawInvisible(self.rollOpacityFunction)
end
else
self.grid:draw()
end
end
MarathonA3Game.rollOpacityFunction = function(age)
if age < 240 then return 1
elseif age > 300 then return 0
else return 1 - (age - 240) / 60 end
end
MarathonA3Game.mRollOpacityFunction = function(age)
if age > 4 then return 0
else return 1 - age / 4 end
end
function MarathonA3Game:drawScoringInfo()
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("GRADE", 240, 120, 40, "left")
love.graphics.printf("SCORE", 240, 200, 40, "left")
love.graphics.printf("LEVEL", 240, 320, 40, "left")
-- draw section time data
current_section = math.floor(self.level / 100) + 1
section_x = 530
section_70_x = 440
for section, time in pairs(self.section_times) do
if section > 0 then
love.graphics.printf(formatTime(time), section_x, 40 + 20 * section, 90, "left")
end
end
for section, time in pairs(self.section_70_times) do
if section > 0 then
love.graphics.printf(formatTime(time), section_70_x, 40 + 20 * section, 90, "left")
end
end
local current_x
if table.getn(self.section_times) < table.getn(self.section_70_times) then
current_x = section_x
else
current_x = section_70_x
end
love.graphics.printf(formatTime(self.frames - self.section_start_time), current_x, 40 + 20 * current_section, 90, "left")
love.graphics.setFont(font_3x5_3)
love.graphics.printf(self.score, 240, 220, 90, "left")
love.graphics.printf(self:getLetterGrade(), 240, 140, 90, "left")
love.graphics.printf(self.level, 240, 340, 40, "right")
love.graphics.printf(self:getSectionEndLevel(), 240, 370, 40, "right")
love.graphics.setFont(font_8x11)
love.graphics.printf(formatTime(self.frames), 64, 420, 160, "center")
end
function MarathonA3Game:getHighscoreData()
return {
grade = self:getAggregateGrade(),
level = self.level,
frames = self.frames,
}
end
function MarathonA3Game:getSectionEndLevel()
if self.level > 900 then return 999
else return math.floor(self.level / 100 + 1) * 100 end
end
function MarathonA3Game:getBackground()
return math.floor(self.level / 100)
end
return MarathonA3Game

View File

@ -0,0 +1,176 @@
require 'funcs'
local GameMode = require 'tetris.modes.gamemode'
local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local MarathonL1Game = GameMode:extend()
MarathonL1Game.name = "Line Attack"
MarathonL1Game.hash = "MarathonL1"
MarathonL1Game.tagline = "Can you clear the time hurdles when the game goes this fast?"
MarathonL1Game.arr = 1
MarathonL1Game.drop_speed = 1
function MarathonL1Game:new()
MarathonL1Game.super:new()
self.roll_frames = 0
self.randomizer = History6RollsRandomizer()
self.section_time_limit = 3600
self.section_start_time = 0
self.section_times = { [0] = 0 }
self.section_clear = false
self.lock_drop = true
self.enable_hold = true
self.next_queue_length = 3
end
function MarathonL1Game:getARE()
if self.lines < 10 then return 18
elseif self.lines < 40 then return 14
elseif self.lines < 60 then return 12
elseif self.lines < 70 then return 10
elseif self.lines < 80 then return 8
elseif self.lines < 90 then return 7
else return 6 end
end
function MarathonL1Game:getLineARE()
return self:getARE()
end
function MarathonL1Game:getDasLimit()
if self.lines < 20 then return 10
elseif self.lines < 50 then return 9
elseif self.lines < 70 then return 8
else return 7 end
end
function MarathonL1Game:getLineClearDelay()
if self.lines < 10 then return 14
elseif self.lines < 30 then return 9
else return 5 end
end
function MarathonL1Game:getLockDelay()
if self.lines < 10 then return 28
elseif self.lines < 20 then return 24
elseif self.lines < 30 then return 22
elseif self.lines < 40 then return 20
elseif self.lines < 50 then return 18
elseif self.lines < 70 then return 14
else return 13 end
end
function MarathonL1Game:getGravity()
return 20
end
function MarathonL1Game:getSection()
return math.floor(level / 100) + 1
end
function MarathonL1Game:advanceOneFrame()
if self.clear then
self.roll_frames = self.roll_frames + 1
if self.roll_frames < 0 then
return false
elseif self.roll_frames > 2968 then
self.completed = true
end
elseif self.ready_frames == 0 then
if not self.section_clear then
self.frames = self.frames + 1
end
if self:getSectionTime() >= self.section_time_limit then
self.game_over = true
end
end
return true
end
function MarathonL1Game:onLineClear(cleared_row_count)
if not self.clear then
local new_lines = self.lines + cleared_row_count
self:updateSectionTimes(self.lines, new_lines)
self.lines = math.min(new_lines, 150)
if self.lines == 150 then
self.clear = true
self.roll_frames = -150
end
end
end
function MarathonL1Game:getSectionTime()
return self.frames - self.section_start_time
end
function MarathonL1Game:updateSectionTimes(old_lines, new_lines)
if math.floor(old_lines / 10) < math.floor(new_lines / 10) then
-- record new section
table.insert(self.section_times, self:getSectionTime())
self.section_start_time = self.frames
self.section_clear = true
end
end
function MarathonL1Game:onPieceEnter()
self.section_clear = false
end
function MarathonL1Game:drawGrid(ruleset)
self.grid:draw()
end
function MarathonL1Game:getHighscoreData()
return {
lines = self.lines,
frames = self.frames,
}
end
function MarathonL1Game:drawScoringInfo()
MarathonL1Game.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("TIME LEFT", 240, 250, 80, "left")
love.graphics.printf("LINES", 240, 320, 40, "left")
local current_section = math.floor(self.lines / 10) + 1
self:drawSectionTimesWithSplits(current_section)
love.graphics.setFont(font_3x5_3)
love.graphics.printf(self.lines, 240, 340, 40, "right")
love.graphics.printf(self.clear and self.lines or self:getSectionEndLines(), 240, 370, 40, "right")
-- draw time left, flash red if necessary
local time_left = self.section_time_limit - math.max(self:getSectionTime(), 0)
if not self.game_over and not self.clear and time_left < sp(0,10) and time_left % 4 < 2 then
love.graphics.setColor(1, 0.3, 0.3, 1)
end
love.graphics.printf(formatTime(time_left), 240, 270, 160, "left")
love.graphics.setColor(1, 1, 1, 1)
end
function MarathonL1Game:getSectionEndLines()
return math.floor(self.lines / 10 + 1) * 10
end
function MarathonL1Game:getBackground()
return math.floor(self.lines / 10)
end
return MarathonL1Game

170
tetris/modes/pacer_test.lua Normal file
View File

@ -0,0 +1,170 @@
require 'funcs'
local GameMode = require 'tetris.modes.gamemode'
local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local PacerTest = GameMode:extend()
PacerTest.name = "TetrisGram™ Pacer Test"
PacerTest.hash = "PacerTest"
PacerTest.tagline = ""
PacerTest.arr = 1
PacerTest.drop_speed = 1
local function getLevelFrames(level)
if level == 1 then return 72 * 60 / 8.0
else return 72 * 60 / (8 + level * 0.5)
end
end
local level_end_sections = {
7, 15, 23, 32, 41, 51, 61, 72, 83, 94,
106, 118, 131, 144, 157, 171, 185, 200,
215, 231, 247
}
function PacerTest:new()
PacerTest.super:new()
self.ready_frames = 2430
self.clear_frames = 0
self.randomizer = History6RollsRandomizer()
self.level = 1
self.section = 0
self.level_frames = 0
self.section_lines = 0
self.section_clear = false
self.strikes = 0
self.lock_drop = true
self.lock_hard_drop = true
self.enable_hold = true
self.instant_hard_drop = true
self.instant_soft_drop = false
self.next_queue_length = 3
end
function PacerTest:initialize(ruleset)
for i = 1, 30 do
table.insert(self.next_queue, self:getNextPiece(ruleset))
end
self.level_frames = getLevelFrames(1)
switchBGM("pacer_test")
end
function PacerTest:getARE()
return 0
end
function PacerTest:getLineARE()
return 0
end
function PacerTest:getDasLimit()
return 8
end
function PacerTest:getLineClearDelay()
return 6
end
function PacerTest:getLockDelay()
return 30
end
function PacerTest:getGravity()
return 1/64
end
function PacerTest:getSection()
return math.floor(level / 100) + 1
end
function PacerTest:advanceOneFrame()
if self.clear then
self.clear_frames = self.clear_frames + 1
if self.clear_frames > 600 then
self.completed = true
end
return false
elseif self.ready_frames == 0 then
self.frames = self.frames + 1
self.level_frames = self.level_frames - 1
if self.level_frames <= 0 then
self:checkSectionStatus()
self.section = self.section + 1
if self.section >= level_end_sections[self.level] then
self.level = self.level + 1
end
self.level_frames = self.level_frames + getLevelFrames(self.level)
end
end
return true
end
function PacerTest:checkSectionStatus()
if self.section_clear then
self.strikes = 0
self.section_clear = false
else
self.strikes = self.strikes + 1
if self.strikes >= 200 then
self.game_over = true
fadeoutBGM(2.5)
end
end
self.section_lines = 0
end
function PacerTest:onLineClear(cleared_row_count)
self.section_lines = self.section_lines + cleared_row_count
if self.section_lines >= 3 then
self.section_clear = true
end
end
function PacerTest:drawGrid(ruleset)
self.grid:draw()
if self.piece ~= nil then
self:drawGhostPiece(ruleset)
end
end
function PacerTest:getHighscoreData()
return {
level = self.level,
frames = self.frames,
}
end
function PacerTest:drawScoringInfo()
PacerTest.super.drawScoringInfo(self)
love.graphics.setColor(1, 1, 1, 1)
local text_x = config["side_next"] and 320 or 240
love.graphics.setFont(font_3x5_2)
love.graphics.printf("NEXT", 64, 40, 40, "left")
love.graphics.printf("LINES", text_x, 224, 70, "left")
love.graphics.printf("LEVEL", text_x, 320, 40, "left")
for i = 1, math.min(self.strikes, 3) do
love.graphics.draw(misc_graphics["strike"], text_x + (i - 1) * 30, 280)
end
love.graphics.setFont(font_3x5_3)
love.graphics.printf(self.section_lines .. "/3", text_x, 244, 40, "left")
love.graphics.printf(self.level, text_x, 340, 40, "right")
love.graphics.printf(self.section, text_x, 370, 40, "right")
end
function PacerTest:getBackground()
return self.level - 1
end
return PacerTest

View File

@ -0,0 +1,199 @@
require 'funcs'
local GameMode = require 'tetris.modes.gamemode'
local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local PhantomManiaGame = GameMode:extend()
PhantomManiaGame.name = "Phantom Mania"
PhantomManiaGame.hash = "PhantomMania"
PhantomManiaGame.tagline = "The blocks disappear as soon as they're locked! Can you remember where everything is?"
PhantomManiaGame.arr = 1
PhantomManiaGame.drop_speed = 1
function PhantomManiaGame:new()
PhantomManiaGame.super:new()
self.lock_drop = true
self.next_queue_length = 1
self.roll_frames = 0
self.combo = 1
self.randomizer = History6RollsRandomizer()
end
function PhantomManiaGame:getARE()
if self.level < 100 then return 18
elseif self.level < 200 then return 14
elseif self.level < 400 then return 8
elseif self.level < 500 then return 7
else return 6 end
end
function PhantomManiaGame:getLineARE()
if self.level < 100 then return 18
elseif self.level < 400 then return 8
elseif self.level < 500 then return 7
else return 6 end
end
function PhantomManiaGame:getDasLimit()
if self.level < 200 then return 11
elseif self.level < 300 then return 10
elseif self.level < 400 then return 9
else return 7 end
end
function PhantomManiaGame:getLineClearDelay()
return self:getLineARE()
end
function PhantomManiaGame:getLockDelay()
if self.level < 100 then return 30
elseif self.level < 200 then return 26
elseif self.level < 300 then return 22
elseif self.level < 400 then return 18
else return 15 end
end
function PhantomManiaGame:getGravity()
return 20
end
function PhantomManiaGame:hitTorikan(old_level, new_level)
if old_level < 300 and new_level >= 300 and self.frames > sp(2,28) then
self.level = 300
return true
end
if old_level < 500 and new_level >= 500 and self.frames > sp(3,38) then
self.level = 500
return true
end
if old_level < 800 and new_level >= 800 and self.frames > sp(5,23) then
self.level = 800
return true
end
return false
end
function PhantomManiaGame:advanceOneFrame()
if self.clear then
self.roll_frames = self.roll_frames + 1
if self.roll_frames < 0 then
return false
elseif self.roll_frames > 1982 then
self.completed = true
end
elseif self.ready_frames == 0 then
self.frames = self.frames + 1
end
return true
end
function PhantomManiaGame:onPieceEnter()
if (self.level % 100 ~= 99 and self.level ~= 998) and not self.clear and self.frames ~= 0 then
self.level = self.level + 1
end
end
function PhantomManiaGame:onLineClear(cleared_row_count)
if not self.clear then
local new_level = self.level + cleared_row_count
if self:hitTorikan(self.level, new_level) then
if new_level >= 999 then
self.level = 999
end
self.clear = true
else
self.level = new_level
end
end
end
function PhantomManiaGame:updateScore(level, drop_bonus, cleared_lines)
if cleared_lines > 0 then
self.score = self.score + (
(math.ceil((level + cleared_lines) / 4) + drop_bonus) *
cleared_lines * (cleared_lines * 2 - 1) * (self.combo * 2 - 1)
)
self.lines = self.lines + cleared_lines
self.combo = self.combo + cleared_lines - 1
else
self.drop_bonus = 0
self.combo = 1
end
end
PhantomManiaGame.rollOpacityFunction = function(age)
if age > 4 then return 0
else return 1 - age / 4 end
end
function PhantomManiaGame:drawGrid()
if not (self.game_over or self.clear) then
self.grid:drawInvisible(self.rollOpacityFunction)
else
self.grid:draw()
end
end
local function getLetterGrade(level, clear)
if level < 300 or level == 300 and clear then
return ""
elseif level < 500 or level == 500 and clear then
return "M"
elseif level < 700 then
return "MK"
elseif level < 800 or level == 800 and clear then
return "MV"
elseif level < 900 then
return "MO"
elseif level < 999 then
return "MM"
elseif level == 999 then
return "GM"
end
end
function PhantomManiaGame:drawScoringInfo()
PhantomManiaGame.super.drawScoringInfo(self)
local text_x = config["side_next"] and 320 or 240
love.graphics.setFont(font_3x5_2)
love.graphics.printf("GRADE", text_x, 120, 40, "left")
love.graphics.printf("SCORE", text_x, 200, 40, "left")
love.graphics.printf("LEVEL", text_x, 320, 40, "left")
love.graphics.setFont(font_3x5_3)
love.graphics.printf(getLetterGrade(self.level, self.clear), text_x, 140, 90, "left")
love.graphics.printf(self.score, text_x, 220, 90, "left")
love.graphics.printf(self.level, text_x, 340, 40, "right")
if self.clear then
love.graphics.printf(self.level, text_x, 370, 40, "right")
else
love.graphics.printf(self:getSectionEndLevel(), text_x, 370, 40, "right")
end
end
function PhantomManiaGame:getSectionEndLevel()
if self.level > 900 then return 999
else return math.floor(self.level / 100 + 1) * 100 end
end
function PhantomManiaGame:getBackground()
return math.floor(self.level / 100)
end
function PhantomManiaGame:getHighscoreData()
return {
level = self.level,
frames = self.frames,
}
end
return PhantomManiaGame

View File

@ -0,0 +1,303 @@
require 'funcs'
local GameMode = require 'tetris.modes.gamemode'
local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local PhantomMania2Game = GameMode:extend()
PhantomMania2Game.name = "Phantom Mania 2"
PhantomMania2Game.hash = "PhantomMania2"
PhantomMania2Game.tagline = "The blocks disappear even faster now! Can you make it to level 1300?"
PhantomMania2Game.arr = 1
PhantomMania2Game.drop_speed = 1
function PhantomMania2Game:new()
PhantomMania2Game.super:new()
self.level = 0
self.grade = 0
self.garbage = 0
self.clear = false
self.completed = false
self.roll_frames = 0
self.combo = 1
self.hold_age = 0
self.queue_age = 0
self.randomizer = History6RollsRandomizer()
self.lock_drop = true
self.enable_hold = true
self.next_queue_length = 3
end
function PhantomMania2Game:getARE()
if self.level < 300 then return 12
else return 6 end
end
function PhantomMania2Game:getLineARE()
if self.level < 100 then return 8
elseif self.level < 200 then return 7
elseif self.level < 500 then return 6
elseif self.level < 1300 then return 5
else return 6 end
end
function PhantomMania2Game:getDasLimit()
if self.level < 200 then return 9
elseif self.level < 500 then return 7
else return 5 end
end
function PhantomMania2Game:getLineClearDelay()
return self:getLineARE() - 2
end
function PhantomMania2Game:getLockDelay()
if self.level < 200 then return 18
elseif self.level < 300 then return 17
elseif self.level < 500 then return 15
elseif self.level < 600 then return 13
else return 12 end
end
function PhantomMania2Game:getGravity()
return 20
end
function PhantomMania2Game:getGarbageLimit()
if self.level < 600 then return 20
elseif self.level < 700 then return 18
elseif self.level < 800 then return 10
elseif self.level < 900 then return 9
else return 8 end
end
function PhantomMania2Game:getNextPiece(ruleset)
return {
skin = self.level >= 1000 and "bone" or "2tie",
shape = self.randomizer:nextPiece(),
orientation = ruleset:getDefaultOrientation(),
}
end
function PhantomMania2Game:hitTorikan(old_level, new_level)
if old_level < 300 and new_level >= 300 and self.frames > sp(2,02) then
self.level = 300
return true
end
if old_level < 500 and new_level >= 500 and self.frames > sp(3,03) then
self.level = 500
return true
end
if old_level < 800 and new_level >= 800 and self.frames > sp(4,40) then
self.level = 800
return true
end
if old_level < 1000 and new_level >= 1000 and self.frames > sp(5,38) then
self.level = 1000
return true
end
return false
end
function PhantomMania2Game:advanceOneFrame()
if self.clear then
self.roll_frames = self.roll_frames + 1
if self.roll_frames < 0 then
if self.roll_frames + 1 == 0 then
switchBGM("credit_roll", "gm3")
return true
end
return false
elseif self.roll_frames > 3238 then
switchBGM(nil)
self.completed = true
end
elseif self.ready_frames == 0 then
self.frames = self.frames + 1
self.hold_age = self.hold_age + 1
end
return true
end
function PhantomMania2Game:whilePieceActive()
self.queue_age = self.queue_age + 1
end
function PhantomMania2Game:onPieceEnter()
self.queue_age = 0
if (self.level % 100 ~= 99) and not self.clear and self.frames ~= 0 then
self.level = self.level + 1
end
end
local cleared_row_levels = {1, 2, 4, 6}
local cleared_row_points = {0.02, 0.05, 0.15, 0.6}
function PhantomMania2Game:onLineClear(cleared_row_count)
if not self.clear then
local new_level = self.level + cleared_row_levels[cleared_row_count]
self:updateSectionTimes(self.level, new_level)
if new_level >= 1300 or self:hitTorikan(self.level, new_level) then
if new_level >= 1300 then
self.level = 1300
end
self.clear = true
self.grid:clear()
self.roll_frames = -150
else
self.level = math.min(new_level, 1300)
end
self:advanceBottomRow(-cleared_row_count)
end
end
function PhantomMania2Game:onPieceLock()
self:advanceBottomRow(1)
end
function PhantomMania2Game:onHold()
self.hold_age = 0
end
function PhantomMania2Game:updateScore(level, drop_bonus, cleared_lines)
if cleared_lines > 0 then
self.score = self.score + (
(math.ceil((level + cleared_lines) / 4) + drop_bonus) *
cleared_lines * (cleared_lines * 2 - 1) * (self.combo * 2 - 1)
)
self.lines = self.lines + cleared_lines
self.combo = self.combo + cleared_lines - 1
else
self.drop_bonus = 0
self.combo = 1
end
end
local cool_cutoffs = {
sp(0,36), sp(0,36), sp(0,36), sp(0,36), sp(0,36),
sp(0,30), sp(0,30), sp(0,30), sp(0,30), sp(0,30),
sp(0,27), sp(0,27), sp(0,27),
}
local regret_cutoffs = {
sp(0,50), sp(0,50), sp(0,50), sp(0,50), sp(0,50),
sp(0,40), sp(0,40), sp(0,40), sp(0,40), sp(0,40),
sp(0,35), sp(0,35), sp(0,35),
}
function PhantomMania2Game:updateSectionTimes(old_level, new_level)
if math.floor(old_level / 100) < math.floor(new_level / 100) then
local section = math.floor(old_level / 100) + 1
section_time = self.frames - self.section_start_time
table.insert(self.section_times, section_time)
self.section_start_time = self.frames
if section_time <= cool_cutoffs[section] then
self.grade = self.grade + 2
elseif section_time <= regret_cutoffs[section] then
self.grade = self.grade + 1
end
end
end
function PhantomMania2Game:advanceBottomRow(dx)
if self.level >= 500 and self.level < 1000 then
self.garbage = math.max(self.garbage + dx, 0)
if self.garbage >= self:getGarbageLimit() then
self.grid:copyBottomRow()
self.garbage = 0
end
end
end
PhantomMania2Game.rollOpacityFunction = function(age)
if age > 4 then return 0
else return 1 - age / 4 end
end
PhantomMania2Game.garbageOpacityFunction = function(age)
if age > 30 then return 0
else return 1 - age / 30 end
end
function PhantomMania2Game:drawGrid()
if not (self.game_over or self.clear) then
self.grid:drawInvisible(self.rollOpacityFunction, self.garbageOpacityFunction)
else
self.grid:draw()
end
end
local function getLetterGrade(grade)
if grade == 0 then
return "1"
elseif grade <= 9 then
return "S" .. tostring(grade)
else
return "M" .. tostring(grade - 9)
end
end
function PhantomMania2Game:setNextOpacity(i)
if self.level > 1000 then
local hidden_next_pieces = math.floor(self.level / 100) - 10
if i < hidden_next_pieces then
love.graphics.setColor(1, 1, 1, 0)
elseif i == hidden_next_pieces then
love.graphics.setColor(1, 1, 1, 1 - math.min(1, self.queue_age / 4))
else
love.graphics.setColor(1, 1, 1, 1)
end
else
love.graphics.setColor(1, 1, 1, 1)
end
end
function PhantomMania2Game:setHoldOpacity()
if self.level > 1000 then
love.graphics.setColor(1, 1, 1, 1 - math.min(1, self.hold_age / 15))
else
love.graphics.setColor(1, 1, 1, 1)
end
end
function PhantomMania2Game:drawScoringInfo()
PhantomMania2Game.super.drawScoringInfo(self)
love.graphics.setColor(1, 1, 1, 1)
local text_x = config["side_next"] and 320 or 240
love.graphics.setFont(font_3x5_2)
love.graphics.printf("GRADE", text_x, 120, 40, "left")
love.graphics.printf("SCORE", text_x, 200, 40, "left")
love.graphics.printf("LEVEL", text_x, 320, 40, "left")
love.graphics.setFont(font_3x5_3)
love.graphics.printf(getLetterGrade(math.floor(self.grade)), text_x, 140, 90, "left")
love.graphics.printf(self.score, text_x, 220, 90, "left")
love.graphics.printf(self.level, text_x, 340, 50, "right")
if self.clear then
love.graphics.printf(self.level, text_x, 370, 50, "right")
else
love.graphics.printf(math.floor(self.level / 100 + 1) * 100, text_x, 370, 50, "right")
end
end
function PhantomMania2Game:getBackground()
return math.floor(self.level / 100)
end
function PhantomMania2Game:getHighscoreData()
return {
level = self.level,
frames = self.frames,
grade = self.grade,
}
end
return PhantomMania2Game

View File

@ -0,0 +1,200 @@
require 'funcs'
local GameMode = require 'tetris.modes.gamemode'
local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local PhantomManiaGame = GameMode:extend()
PhantomManiaGame.name = "Phantom Mania N"
PhantomManiaGame.hash = "PhantomManiaN"
PhantomManiaGame.tagline = "The old mode from Nullpomino."
PhantomManiaGame.arr = 1
PhantomManiaGame.drop_speed = 1
function PhantomManiaGame:new()
PhantomManiaGame.super:new()
self.lock_drop = true
self.next_queue_length = 3
self.enable_hold = true
self.roll_frames = 0
self.combo = 1
self.randomizer = History6RollsRandomizer()
end
function PhantomManiaGame:getARE()
if self.level < 100 then return 18
elseif self.level < 200 then return 14
elseif self.level < 400 then return 8
elseif self.level < 500 then return 7
else return 6 end
end
function PhantomManiaGame:getLineARE()
if self.level < 100 then return 18
elseif self.level < 400 then return 8
elseif self.level < 500 then return 7
else return 6 end
end
function PhantomManiaGame:getDasLimit()
if self.level < 200 then return 11
elseif self.level < 300 then return 10
elseif self.level < 400 then return 9
else return 7 end
end
function PhantomManiaGame:getLineClearDelay()
return self:getLineARE()
end
function PhantomManiaGame:getLockDelay()
if self.level < 100 then return 30
elseif self.level < 200 then return 26
elseif self.level < 300 then return 22
elseif self.level < 400 then return 18
else return 15 end
end
function PhantomManiaGame:getGravity()
return 20
end
function PhantomManiaGame:hitTorikan(old_level, new_level)
if old_level < 300 and new_level >= 300 and self.frames > sp(2,28) then
self.level = 300
return true
end
if old_level < 500 and new_level >= 500 and self.frames > sp(3,38) then
self.level = 500
return true
end
if old_level < 800 and new_level >= 800 and self.frames > sp(5,23) then
self.level = 800
return true
end
return false
end
function PhantomManiaGame:advanceOneFrame()
if self.clear then
self.roll_frames = self.roll_frames + 1
if self.roll_frames < 0 then
return false
elseif self.roll_frames > 1982 then
self.completed = true
end
elseif self.ready_frames == 0 then
self.frames = self.frames + 1
end
return true
end
function PhantomManiaGame:onPieceEnter()
if (self.level % 100 ~= 99 and self.level ~= 998) and not self.clear and self.frames ~= 0 then
self.level = self.level + 1
end
end
function PhantomManiaGame:onLineClear(cleared_row_count)
if not self.clear then
local new_level = self.level + cleared_row_count
if self:hitTorikan(self.level, new_level) then
if new_level >= 999 then
self.level = 999
end
self.clear = true
else
self.level = new_level
end
end
end
function PhantomManiaGame:updateScore(level, drop_bonus, cleared_lines)
if cleared_lines > 0 then
self.score = self.score + (
(math.ceil((level + cleared_lines) / 4) + drop_bonus) *
cleared_lines * (cleared_lines * 2 - 1) * (self.combo * 2 - 1)
)
self.lines = self.lines + cleared_lines
self.combo = self.combo + cleared_lines - 1
else
self.drop_bonus = 0
self.combo = 1
end
end
PhantomManiaGame.rollOpacityFunction = function(age)
if age > 4 then return 0
else return 1 - age / 4 end
end
function PhantomManiaGame:drawGrid()
if not (self.game_over or self.clear) then
self.grid:drawInvisible(self.rollOpacityFunction)
else
self.grid:draw()
end
end
local function getLetterGrade(level, clear)
if level < 300 or level == 300 and clear then
return ""
elseif level < 500 or level == 500 and clear then
return "M"
elseif level < 700 then
return "MK"
elseif level < 800 or level == 800 and clear then
return "MV"
elseif level < 900 then
return "MO"
elseif level < 999 then
return "MM"
elseif level == 999 then
return "GM"
end
end
function PhantomManiaGame:drawScoringInfo()
PhantomManiaGame.super.drawScoringInfo(self)
local text_x = config["side_next"] and 320 or 240
love.graphics.setFont(font_3x5_2)
love.graphics.printf("GRADE", text_x, 120, 40, "left")
love.graphics.printf("SCORE", text_x, 200, 40, "left")
love.graphics.printf("LEVEL", text_x, 320, 40, "left")
love.graphics.setFont(font_3x5_3)
love.graphics.printf(getLetterGrade(self.level, self.clear), text_x, 140, 90, "left")
love.graphics.printf(self.score, text_x, 220, 90, "left")
love.graphics.printf(self.level, text_x, 340, 40, "right")
if self.clear then
love.graphics.printf(self.level, text_x, 370, 40, "right")
else
love.graphics.printf(self:getSectionEndLevel(), text_x, 370, 40, "right")
end
end
function PhantomManiaGame:getSectionEndLevel()
if self.level > 900 then return 999
else return math.floor(self.level / 100 + 1) * 100 end
end
function PhantomManiaGame:getBackground()
return math.floor(self.level / 100)
end
function PhantomManiaGame:getHighscoreData()
return {
level = self.level,
frames = self.frames,
}
end
return PhantomManiaGame

Some files were not shown because too many files have changed in this diff Show More