Compare commits

..

22 Commits

Author SHA1 Message Date
Joe Z
2b9740768b How did that get away? 2019-07-07 23:14:17 -04:00
Joe Z
0e65b39395 BGM kept looping after exiting credits_a3, which is not what we want. 2019-07-07 19:38:46 -04:00
Joe Z
ca04333bb7 Lowered the volume of Credits A3 by a factor of 10. 2019-07-07 19:34:07 -04:00
Joe Zeng
80c7c99bd7 Added Credits A3 mode. (#26)
Also updated the documentation.
2019-07-07 18:55:03 -04:00
Joe Z
717afbebf6 Added Marathon WCB. 2019-07-07 18:06:13 -04:00
Joe Z
7227085f84 Made instant DAS respect instant gravity. 2019-07-07 17:25:35 -04:00
Joe Zeng
c40392f00f DAS priority reversal (#25)
* Reversed the priority of key presses when charging DAS.
* Made it an actual config option.
* Config should be false by default.
2019-07-07 17:23:17 -04:00
Joe Z
001c8f0ea8 Fixed some issues with Marathon A1. 2019-07-07 12:11:28 -04:00
Joe Z
7fa0e60145 Made the menu scroll up and down when it overflows. 2019-06-29 15:09:48 -04:00
Joe Z
901f7f2d12 Fixed a bug in the "actual cleared row count" for Big Mode. 2019-06-22 00:07:10 -04:00
Joe Z
35f4aea67d Added Ti-SRS and modified delay curve behaviour on Marathon 2020.
I realized that playing at 4/8 for 800 levels straight is probably too much,
so I made it that only the first 10 sections count for advancing the delay
curve faster than it would normally go. Now only the last 500 levels can be
at delay level 20.
2019-06-21 23:44:58 -04:00
Joe Z
7deaa5ab92 Added all the Marathon AX modes and Ace-ARS. 2019-06-19 22:56:33 -04:00
Joe Zeng
1254de15d5 Refactored the "Ligne" modes. (#21)
* Added Ligne C89, now known as Marathon C89.
* Refactored all the Ligne modes to no longer use the "Ligne" name.

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

Implements #13.
2019-06-16 22:16:09 -04:00
Joe Zeng
5131061e42 Added conventions for code submission / review.
Also, coding conventions didn't deserve to go first, so I reordered the sections a little bit.
2019-06-15 23:28:53 -04:00
Joe Z
209e60e82e Fixed a roll and section COOL bug in Marathon A3. 2019-06-09 18:37:23 -04:00
Joe Zeng
c4ba80b60d Fixed a roll level bug in Phantom Mania. (#18)
Also refactored Phantom Mania N so that any changes made to regular Phantom Mania get applied automatically.
2019-06-09 09:14:43 -04:00
Joe Zeng
2ba957f65a Updated the README with instructions for running the trunk release. 2019-06-04 11:44:39 -04:00
Joe Zeng
321de8564c Background 20 doesn't exist!
Should _actually_ fix #1 this time.
2019-06-03 23:16:24 -04:00
Joe Zeng
96ac054cf6 Stopped bottom-row garbage from clearing 5 lines. (#16)
Resolves #15.

1) Cleared row count is marked before the onPieceLock method is called, letting the piece lock procedure react to the count of rows the piece is about to clear. (In practice, only 0 and non-0 will be different.)

2) The modes with bottom-row garbage will not advance the garbage counter when the piece is about to clear lines, as should be the case.

Also included:

3) Changed the Always O Randomizer to the Always Randomizer that takes which piece it should "always" produce as an argument in the constructor.

4) Fixed the torikan for level 800 in Phantom Mania 2. It should have been 4:45, not 4:40.
2019-06-03 23:12:48 -04:00
Joe Zeng
b7fc51f4bd Made the coding convention examples a little more expressive. 2019-06-01 23:41:36 -04:00
Joe Z
04ccd628be Fixed up the docs. 2019-05-31 23:59:04 -04:00
46 changed files with 1410 additions and 829 deletions

View File

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

View File

@@ -1,5 +1,3 @@
The code in Cambridge is licensed under the MIT license.
Copyright (c) 2018-2019 Joe Zeng
Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -18,12 +16,4 @@ 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.
SOFTWARE.

56
README.md Normal file
View File

@@ -0,0 +1,56 @@
Cambridge
=========
Welcome to Cambridge, the next open-source falling-block game engine!
Installation instructions
-------------------------
Pre-built releases are available on the releases page.
### Windows
Unzip the exe file and run it directly. All assets are currently bundled inside the executable.
### macOS
For the time being, the file `cambridge.love` only works on the command line. Install `love` with [https://brew.sh/](Homebrew), and run:
$ love cambridge.love
### Linux
Same as macOS, except install `love` with your favourite package manager.
Running from source
-------------------
If you want the bleeding-edge release, you can also clone the code straight from this repository.
### macOS, Linux
If you haven't already, install `love` with your favourite package manager (Homebrew on macOS, your system's default on Linux). **Make sure you're using LÖVE 11, because it won't work with earlier versions!**
Clone the repository in git:
git clone https://github.com/joezeng/cambridge
Then, navigate to the root directory that you just cloned, and type:
love .
It should run automatically!
License
-------
The Cambridge project is licensed under the MIT license (included in LICENSE.md).
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.

View File

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

View File

@@ -1,20 +1,40 @@
Game modes
==========
There are several classes of game modes.
There are several classes of game modes. The modes that originate from other games are organized by suffix:
* The "C" series stand for "Classic" games, games that were produced before around 1992-1993 and generally have no wallkicks or lock delay.
* C84 - The original version from the Electronika 60.
* C88 - Sega Tetris.
* C89 - Nintendo / NES Tetris.
* The "A" series stand for "Arika" games, or games in the Tetris the Grand Master series.
* A1 - Tetris The Grand Master (the original from 1998).
* A2 - Tetris The Absolute The Grand Master 2 PLUS.
* A3 - Tetris The Grand Master 3 Terror-Instinct.
* AX - Tetris The Grand Master ACE (X for Xbox).
* The "G" series stand for "Guideline" games, or games that follow the Tetris Guideline.
* GF - Tetris Friends (2007-2019)
* GJ - Tetris Online Japan (2005-2011)
* The "N" series stands for Nullpomino, only used for Phantom Mania N.
* The "W" series stands for "Web" games, which are fanmade games released on the web.
* WCB - RainComplex.net's Cat Boi Quatro.
MARATHON
--------
Modes in which the goal is to play as well as possible over a limited game interval, to ultimately achieve the title of Grand Master.
Modes in which the goal is to play as well as possible over a limited game interval.
* **MARATHON 2020**: 2020 levels of pure pain. Can you make it all the way?
* **MARATHON WCB**: CatBoiQuatro! Can you control the pieces?
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 AX**: Normal mode from TGM Ace.
* **MARATHON AX2**: Hi-Speed1 mode from TGM Ace.
* **MARATHON AX3**: Hi-Speed2 mode from TGM Ace.
* **MARATHON C89**: Nintendo NES Tetris. Can you transition and make it to the killscreen?
SURVIVAL
@@ -22,9 +42,49 @@ SURVIVAL
Modes that concentrate on how long you can survive an increasingly fast and difficult game.
* **SURVIVAL 2020**: It only gets worse. Beware of bone blocks!
* **SURVIVAL 2020**: It only gets worse from Marathon 2020. Beware of the total delay!
From other games:
* **SURVIVAL A1**: 20G mode from Tetris the Grand Master.
* **SURVIVAL A2**: T.A. Death.
* **SURVIVAL A3**: Ti Shirase.
* **SURVIVAL AX**: Another mode from TGM Ace.
* **SURVIVAL AX2**: Another2 mode from TGM Ace.
RACE
----
Modes with no levels, just a single timed goal.
* **Race 40**: How fast can you clear 40 lines? No limits, no holds barred.
CREDITS
-------
Modes that are just the credit rolls of specific games.
* **CREDITS A3**: Tetris the Grand Master 3's famous M-roll.
PHANTOM MANIA
-------------
Modes where pieces turn invisible as soon as you lock them. One of Cambridge's signature features.
* **Phantom Mania**: The classic invisible mode from Nullpomino. Can you handle 999 levels of "the M roll"?
* **Phantom Mania 2**: Phantom Mania but way faster! Can you face a mode where even the garbage and the next preview turn invisible?
OTHER MODES
-----------
* **Strategy**: How well can you plan ahead your movements? Can you handle only having a short time to place each piece?
* **TetrisGram™ Pacer Test**: is a multi-stage piece-placing ability test that progressively gets more difficult as it continues.
* **Interval Training**: 30 seconds per section. 20G. 15 frames of lock delay. How long can you last?
* **Demon Mode**: An original mode from Oshisaure! Can you push through the ever faster levels and not get denied?

View File

@@ -1,207 +0,0 @@
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

@@ -1,104 +0,0 @@
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

@@ -1,32 +0,0 @@
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 |

View File

@@ -10,16 +10,13 @@ A ruleset consists of the following things:
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.
There are six rulesets currently supported:
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.
* Cambridge - a ruleset original to Cambridge, used for all custom modes. Supports 180-degree rotations!
There are four rotation systems currently supported:
* SRS - the rotation system used in the Tetris Guideline games. Supports 180-degree rotations!
* Ti-SRS - SRS but with no 180-degree rotations.
* Cambridge
* Classic ARS
* Ti-ARS
* SRS
* ARS - the rotation system from the original Tetris the Grand Master.
* Ti-ARS - ARS with floorkicks! From TGM3: Terror Instinct.
* Ace-ARS - ARS with floorkicks and move reset! From TGM ACE.

View File

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

View File

@@ -11,6 +11,7 @@ function love.load()
config["side_next"] = false
config["reverse_rotate"] = true
config["fullscreen"] = false
config["das_last_key"] = false
love.window.setMode(love.graphics.getWidth(), love.graphics.getHeight(), {resizable = true});

Binary file not shown.

View File

@@ -5,31 +5,42 @@ ModeSelectScene.title = "Game Start"
current_mode = 1
current_ruleset = 1
MAX_MODES = 19
game_modes = {
require 'tetris.modes.marathon_2020',
require 'tetris.modes.survival_2020',
require 'tetris.modes.strategy',
require 'tetris.modes.interval_training',
require 'tetris.modes.pacer_test',
require 'tetris.modes.marathon_wcb',
require 'tetris.modes.demon_mode',
require 'tetris.modes.phantom_mania',
require 'tetris.modes.phantom_mania2',
require 'tetris.modes.phantom_mania_n',
require 'tetris.modes.ligne',
require 'tetris.modes.race_40',
require 'tetris.modes.marathon_a1',
require 'tetris.modes.marathon_a2',
require 'tetris.modes.marathon_a3',
require 'tetris.modes.marathon_ax',
require 'tetris.modes.marathon_ax2',
require 'tetris.modes.marathon_ax3',
require 'tetris.modes.marathon_c89',
require 'tetris.modes.survival_a1',
require 'tetris.modes.survival_a2',
require 'tetris.modes.survival_a3',
require 'tetris.modes.marathon_l1',
require 'tetris.modes.survival_ax',
require 'tetris.modes.survival_ax2',
require 'tetris.modes.credits_a3',
}
rulesets = {
require 'tetris.rulesets.cambridge',
require 'tetris.rulesets.standard',
require 'tetris.rulesets.standard_ti',
require 'tetris.rulesets.arika',
require 'tetris.rulesets.arika_ti',
require 'tetris.rulesets.standard_exp',
require 'tetris.rulesets.arika_ace',
--require 'tetris.rulesets.bonkers',
--require 'tetris.rulesets.shirase',
--require 'tetris.rulesets.super302',
@@ -46,38 +57,58 @@ end
function ModeSelectScene:update()
end
function getCursorHeight(x)
return 78 + 20 * x
end
function ModeSelectScene:drawList(name, items, cursor, x)
local start, finish
if table.getn(items) < MAX_MODES then
start = 1
finish = table.getn(items)
else
if cursor < 10 then
start = 1
finish = 19
elseif cursor > table.getn(items) - 9 then
start = table.getn(items) - 18
finish = table.getn(items)
else
start = cursor - 9
finish = cursor + 9
end
end
if self.menu_state.select == name then
love.graphics.setColor(1, 1, 1, 0.5)
else
love.graphics.setColor(1, 1, 1, 0.25)
end
love.graphics.rectangle("fill", x, getCursorHeight(cursor - start), 240, 22)
love.graphics.setColor(1, 1, 1, 1)
for idx = start, finish do
local item = items[idx]
love.graphics.printf(item.name, x + 20, 80 + 20 * (idx - start), 200, "left")
end
end
function ModeSelectScene:render()
love.graphics.setFont(font_3x5_2)
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, 30)
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
self:drawList("mode", game_modes, self.menu_state.mode, 20)
self:drawList("ruleset", rulesets, self.menu_state.ruleset, 320)
end
function ModeSelectScene:onKeyPress(e)

View File

@@ -38,6 +38,10 @@ function Grid:isRowFull(row)
end
function Grid:canPlacePiece(piece)
if piece.big then
return self:canPlaceBigPiece(piece)
end
local offsets = piece:getBlockOffsets()
for index, offset in pairs(offsets) do
local x = piece.position.x + offset.x
@@ -49,7 +53,29 @@ function Grid:canPlacePiece(piece)
return true
end
function Grid:canPlaceBigPiece(piece)
local offsets = piece:getBlockOffsets()
for index, offset in pairs(offsets) do
local x = piece.position.x + offset.x
local y = piece.position.y + offset.y
if x >= 5 or x < 0 or y >= 12 or y < 0 or
self.grid[y * 2 + 1][x * 2 + 1] ~= empty or
self.grid[y * 2 + 1][x * 2 + 2] ~= empty or
self.grid[y * 2 + 2][x * 2 + 1] ~= empty or
self.grid[y * 2 + 2][x * 2 + 2] ~= empty
then
return false
end
end
return true
end
function Grid:canPlacePieceInVisibleGrid(piece)
if piece.big then
return self:canPlaceBigPiece(piece)
-- forget canPlaceBigPieceInVisibleGrid for now
end
local offsets = piece:getBlockOffsets()
for index, offset in pairs(offsets) do
local x = piece.position.x + offset.x
@@ -116,6 +142,10 @@ function Grid:copyBottomRow()
end
function Grid:applyPiece(piece)
if piece.big then
self:applyBigPiece(piece)
return
end
offsets = piece:getBlockOffsets()
for index, offset in pairs(offsets) do
x = piece.position.x + offset.x
@@ -127,6 +157,22 @@ function Grid:applyPiece(piece)
end
end
function Grid:applyBigPiece(piece)
offsets = piece:getBlockOffsets()
for index, offset in pairs(offsets) do
x = piece.position.x + offset.x
y = piece.position.y + offset.y
for a = 1, 2 do
for b = 1, 2 do
self.grid[y*2+a][x*2+b] = {
skin = piece.skin,
colour = piece.shape
}
end
end
end
end
function Grid:update()
for y = 1, 24 do
for x = 1, 10 do
@@ -148,19 +194,21 @@ function Grid:draw()
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)
if self.grid[y][x].skin ~= "bone" then
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

View File

@@ -2,7 +2,7 @@ local Object = require 'libs.classic'
local Piece = Object:extend()
function Piece:new(shape, rotation, position, block_offsets, gravity, lock_delay, skin)
function Piece:new(shape, rotation, position, block_offsets, gravity, lock_delay, skin, big)
self.shape = shape
self.rotation = rotation
self.position = position
@@ -12,6 +12,7 @@ function Piece:new(shape, rotation, position, block_offsets, gravity, lock_delay
self.skin = skin
self.ghost = false
self.locked = false
self.big = big
end
-- Functions that return a new piece to test in rotation systems.
@@ -20,7 +21,7 @@ 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
self.block_offsets, self.gravity, self.lock_delay, self.skin, self.big
)
end
@@ -30,7 +31,7 @@ function Piece:withRelativeRotation(rot)
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
self.block_offsets, self.gravity, self.lock_delay, self.skin, self.big
)
end
@@ -76,12 +77,15 @@ function Piece:setRelativeRotation(rot)
return self
end
function Piece:moveInGrid(step, squares, grid)
function Piece:moveInGrid(step, squares, grid, instant)
local moved = false
for x = 1, squares do
if grid:canPlacePiece(self:withOffset(step)) then
moved = true
self:setOffset(step)
if instant then
self:dropToBottom(grid)
end
else
break
end
@@ -138,14 +142,25 @@ function Piece:draw(opacity, brightness, grid, partial_das)
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 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)
if self.big then
love.graphics.draw(
blocks[self.skin][self.shape],
64+x*32+partial_das*2, 16+y*32+gravity_offset*2,
0, 2, 2
)
else
love.graphics.draw(
blocks[self.skin][self.shape],
64+x*16+partial_das, 16+y*16+gravity_offset
)
end
end
return false
end

119
tetris/modes/credits_a3.lua Normal file
View File

@@ -0,0 +1,119 @@
require 'funcs'
local IntervalTrainingMode = require 'tetris.modes.interval_training'
local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local CreditsA3Game = IntervalTrainingMode:extend()
CreditsA3Game.name = "Credits A3"
CreditsA3Game.hash = "CreditsA3"
CreditsA3Game.tagline = "How consistently can you clear the Ti M-roll?"
function CreditsA3Game:new()
CreditsA3Game.super:new()
self.section_time_limit = 3238
self.norm = 0
self.section = 0
end
function CreditsA3Game:advanceOneFrame(inputs, ruleset)
if self.frames == 0 then
switchBGM("credit_roll", "gm3")
end
if self.roll_frames > 0 then
self.roll_frames = self.roll_frames - 1
if self.roll_frames == 0 then
-- reset
self.norm = 0
self.frames = 0
self.grid:clear()
switchBGM("credit_roll", "gm3")
self:initializeOrHold(inputs, ruleset)
else
return false
end
elseif self.ready_frames == 0 then
self.frames = self.frames + 1
if self:getSectionTime() >= self.section_time_limit then
self.norm = self.norm + 16
self.piece = nil
if self.norm >= 60 then
self.section = self.section + 1
self.roll_frames = 150
else
self.game_over = true
switchBGM(nil)
end
end
end
return true
end
function CreditsA3Game:onPieceEnter()
-- do nothing
end
function CreditsA3Game:onLineClear(cleared_row_count)
if not self.clear then
self.norm = self.norm + (cleared_row_count == 4 and 10 or cleared_row_count)
end
end
CreditsA3Game.rollOpacityFunction = function(age)
if age > 4 then return 0
else return 1 - age / 4 end
end
function CreditsA3Game:drawGrid(ruleset)
if not self.game_over and self.roll_frames < 30 then
self.grid:drawInvisible(self.rollOpacityFunction)
else
self.grid:draw()
end
end
function CreditsA3Game: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("NORM", 240, 320, 40, "left")
self:drawSectionTimesWithSplits(self.section)
love.graphics.setFont(font_3x5_3)
-- 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
if self.norm >= 44 then
love.graphics.setColor(0.3, 1, 0.3, 1) -- flash green if goal has been cleared
else
love.graphics.setColor(1, 0.3, 0.3, 1) -- otherwise flash red
end
end
love.graphics.printf(formatTime(time_left), 240, 270, 160, "left")
love.graphics.setColor(1, 1, 1, 1)
love.graphics.printf(self.norm, 240, 340, 40, "right")
if self.game_over or self.roll_frames > 0 then
love.graphics.printf("60", 240, 370, 40, "right")
else
love.graphics.printf("44", 240, 370, 40, "right")
end
end
function CreditsA3Game:getBackground()
return self.section
end
return CreditsA3Game

View File

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

View File

@@ -36,8 +36,10 @@ function GameMode:new()
self.enable_hold = false
self.enable_hard_drop = true
self.next_queue_length = 1
self.additive_gravity = true
self.draw_section_times = false
self.draw_secondary_section_times = false
self.big_mode = false
-- variables related to configurable parameters
self.drop_locked = false
self.hard_drop_locked = false
@@ -89,10 +91,20 @@ function GameMode:update(inputs, ruleset)
if self.completed then return end
-- advance one frame
if self:advanceOneFrame(inputs) == false then return end
if self:advanceOneFrame(inputs, ruleset) == false then return end
self:chargeDAS(inputs, self:getDasLimit(), self.getARR())
-- set attempt flags
if inputs["left"] or inputs["right"] then self:onAttemptPieceMove(self.piece) end
if
inputs["rotate_left"] or inputs["rotate_right"] or
inputs["rotate_left2"] or inputs["rotate_right2"] or
inputs["rotate_180"]
then
self:onAttemptPieceRotate(self.piece)
end
if self.piece == nil then
self:processDelays(inputs, ruleset)
else
@@ -119,7 +131,8 @@ function GameMode:update(inputs, ruleset)
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
self.drop_locked, self.hard_drop_locked,
self.enable_hard_drop, self.additive_gravity
)
local piece_dy = self.piece.position.y - piece_y
@@ -145,18 +158,22 @@ function GameMode:update(inputs, ruleset)
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()
if self.big_mode then
cleared_row_count = cleared_row_count / 2
end
self:onPieceLock(self.piece, cleared_row_count)
self:updateScore(self.level, self.drop_bonus, cleared_row_count)
self.piece = nil
if self.enable_hold then
self.held = false
end
if cleared_row_count > 0 then
self.lcd = self:getLineClearDelay()
self.are = self:getLineARE()
@@ -191,7 +208,9 @@ end
-- event functions
function GameMode:whilePieceActive() end
function GameMode:onPieceLock(piece) end
function GameMode:onAttemptPieceMove(piece) end
function GameMode:onAttemptPieceRotate(piece) end
function GameMode:onPieceLock(piece, cleared_row_count) end
function GameMode:onLineClear(cleared_row_count) end
function GameMode:onPieceEnter() end
function GameMode:onHold() end
@@ -208,30 +227,66 @@ 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
-- DAS functions
function GameMode:startRightDAS()
self.move = "right"
self.das = { direction = "right", frames = 0 }
if self:getDasLimit() == 0 then
self:continueDAS()
end
end
function GameMode:startLeftDAS()
self.move = "left"
self.das = { direction = "left", frames = 0 }
if self:getDasLimit() == 0 then
self:continueDAS()
end
end
function GameMode:continueDAS()
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
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 }
self.das.frames = das_frames
end
end
function GameMode:stopDAS()
self.move = "none"
self.das = { direction = "none", frames = -1 }
end
function GameMode:chargeDAS(inputs)
if config["das_last_key"] then
if inputs["right"] == true and self.das.direction ~= "right" and not self.prev_inputs["right"] then
self:startRightDAS()
elseif inputs["left"] == true and self.das.direction ~= "left" and not self.prev_inputs["left"] then
self:startLeftDAS()
elseif inputs[self.das.direction] == true then
self:continueDAS()
else
self:stopDAS()
end
else -- default behaviour, das first key pressed
if inputs[self.das.direction] == true then
self:continueDAS()
elseif inputs["right"] == true then
self:startRightDAS()
elseif inputs["left"] == true then
self:startLeftDAS()
else
self:stopDAS()
end
end
end
@@ -298,7 +353,7 @@ function GameMode:initializeNextPiece(inputs, ruleset, piece_data, generate_next
inputs, piece_data, self.grid, gravity,
self.prev_inputs, self.move,
self:getLockDelay(), self:getDropSpeed(),
self.lock_drop, self.lock_hard_drop
self.lock_drop, self.lock_hard_drop, self.big_mode
)
if self.lock_drop then
self.drop_locked = true
@@ -389,8 +444,7 @@ function GameMode:drawScoringInfo()
love.graphics.print(
self.das.direction .. " " ..
self.das.frames .. " " ..
st(self.prev_inputs) ..
self.drop_bonus
st(self.prev_inputs)
)
love.graphics.setFont(font_8x11)
@@ -400,7 +454,7 @@ end
function GameMode:drawSectionTimes(current_section)
local section_x = 530
for section, time in pairs(self.section_times) do
for section, time in ipairs(self.section_times) do
if section > 0 then
love.graphics.printf(formatTime(time), section_x, 40 + 20 * section, 90, "left")
end
@@ -409,7 +463,7 @@ function GameMode:drawSectionTimes(current_section)
love.graphics.printf(formatTime(self.frames - self.section_start_time), section_x, 40 + 20 * current_section, 90, "left")
end
function GameMode:drawSectionTimesWithSecondary(current_section)
function GameMode:drawSectionTimesWithSecondary(current_section, section_colour_function)
local section_x = 530
local section_secondary_x = 440
@@ -420,6 +474,9 @@ function GameMode:drawSectionTimesWithSecondary(current_section)
end
for section, time in pairs(self.secondary_section_times) do
if self.section_colour_function then
love.graphics.setColor(self:section_colour_function(section))
end
if section > 0 then
love.graphics.printf(formatTime(time), section_secondary_x, 40 + 20 * section, 90, "left")
end

View File

@@ -28,19 +28,19 @@ function IntervalTrainingGame:new()
end
function IntervalTrainingGame:getARE()
return 4
return 6
end
function IntervalTrainingGame:getLineARE()
return 4
return 6
end
function IntervalTrainingGame:getDasLimit()
return 6
return 7
end
function IntervalTrainingGame:getLineClearDelay()
return 6
return 4
end
function IntervalTrainingGame:getLockDelay()
@@ -130,8 +130,6 @@ function IntervalTrainingGame:drawScoringInfo()
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
@@ -140,6 +138,7 @@ function IntervalTrainingGame:drawScoringInfo()
love.graphics.printf(formatTime(time_left), 240, 270, 160, "left")
love.graphics.setColor(1, 1, 1, 1)
love.graphics.printf(self.level, 240, 340, 40, "right")
love.graphics.printf(self:getSectionEndLevel(), 240, 370, 40, "right")
end

View File

@@ -323,9 +323,9 @@ function Marathon2020Game:checkClear(level)
end
function Marathon2020Game:updateSectionTimes(old_level, new_level)
function sectionCool()
function sectionCool(section)
self.section_cool_count = self.section_cool_count + 1
self.delay_level = math.min(20, self.delay_level + 1)
if section < 10 then self.delay_level = math.min(20, self.delay_level + 1) end
table.insert(self.section_status, "cool")
end
@@ -343,7 +343,7 @@ function Marathon2020Game:updateSectionTimes(old_level, new_level)
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
if section > 5 then self.delay_level = math.min(20, self.delay_level + 1) end
self:checkTorikan(section)
self:checkClear(new_level)
@@ -352,11 +352,11 @@ function Marathon2020Game:updateSectionTimes(old_level, new_level)
self.secondary_section_times[section] < self.secondary_section_times[section - 1] + 120 and
self.secondary_section_times[section] < cool_cutoffs[section]
) then
sectionCool()
sectionCool(section)
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()
sectionCool(section)
else
table.insert(self.section_status, "none")
end
@@ -417,6 +417,14 @@ function Marathon2020Game:drawGrid()
end
end
function Marathon2020Game:sectionColourFunction(section)
if self.section_status[section] == "cool" then
return { 0, 1, 0, 1 }
else
return { 1, 1, 1, 1 }
end
end
function Marathon2020Game:drawScoringInfo()
Marathon2020Game.super.drawScoringInfo(self)
@@ -428,7 +436,7 @@ function Marathon2020Game:drawScoringInfo()
love.graphics.printf("GRADE PTS.", text_x, 200, 90, "left")
love.graphics.printf("LEVEL", text_x, 320, 40, "left")
self:drawSectionTimesWithSecondary(current_section)
self:drawSectionTimesWithSecondary(current_section, self.sectionColourFunction)
love.graphics.setFont(font_3x5_3)
love.graphics.printf(self:getTotalGrade(), text_x, 120, 90, "left")

View File

@@ -12,7 +12,6 @@ MarathonA1Game.hash = "MarathonA1"
MarathonA1Game.tagline = "Can you score enough points to reach the title of Grand Master?"
function MarathonA1Game:new()
MarathonA1Game.super:new()
@@ -131,7 +130,8 @@ 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
if new_level == 999 then
self.level = 999
self.clear = true
else
self.level = new_level
@@ -140,6 +140,7 @@ function MarathonA1Game:onLineClear(cleared_row_count)
end
function MarathonA1Game:updateScore(level, drop_bonus, cleared_lines)
if self.clear then return end
if cleared_lines > 0 then
self.combo = self.combo + (cleared_lines - 1) * 2
self.score = self.score + (

View File

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

View File

@@ -5,22 +5,19 @@ local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local MarathonL1Game = GameMode:extend()
local MarathonAXGame = GameMode:extend()
MarathonL1Game.name = "Line Attack"
MarathonL1Game.hash = "MarathonL1"
MarathonL1Game.tagline = "Can you clear the time hurdles when the game goes this fast?"
MarathonAXGame.name = "Marathon AX"
MarathonAXGame.hash = "MarathonAX"
MarathonAXGame.tagline = "Can you clear the time hurdles when the game goes this fast?"
function MarathonL1Game:new()
MarathonL1Game.super:new()
function MarathonAXGame:new()
MarathonAXGame.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
@@ -30,52 +27,48 @@ function MarathonL1Game:new()
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
function MarathonAXGame:getSectionTimeLimit()
if self.lines < 20 then return 7200
else return 5400 end
end
function MarathonL1Game:getLineARE()
function MarathonAXGame:getARE()
return 27
end
function MarathonAXGame: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
function MarathonAXGame:getDasLimit()
return 15
end
function MarathonL1Game:getLineClearDelay()
if self.lines < 10 then return 14
elseif self.lines < 30 then return 9
else return 5 end
function MarathonAXGame:getLineClearDelay()
return 40
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
function MarathonAXGame:getLockDelay()
return 30
end
function MarathonL1Game:getGravity()
return 20
function MarathonAXGame:getGravity()
if self.lines < 10 then return 4/256
elseif self.lines < 20 then return 12/256
elseif self.lines < 30 then return 48/256
elseif self.lines < 40 then return 72/256
elseif self.lines < 50 then return 96/256
elseif self.lines < 60 then return 1/2
elseif self.lines < 70 then return 1
elseif self.lines < 80 then return 3/2
elseif self.lines < 90 then return 2
elseif self.lines < 100 then return 3
elseif self.lines < 110 then return 4
elseif self.lines < 120 then return 5
else return 20 end
end
function MarathonL1Game:getSection()
return math.floor(level / 100) + 1
end
function MarathonL1Game:advanceOneFrame()
function MarathonAXGame:advanceOneFrame()
if self.clear then
self.roll_frames = self.roll_frames + 1
if self.roll_frames < 0 then
@@ -87,14 +80,14 @@ function MarathonL1Game:advanceOneFrame()
if not self.section_clear then
self.frames = self.frames + 1
end
if self:getSectionTime() >= self.section_time_limit then
if self:getSectionTime() >= self:getSectionTimeLimit() then
self.game_over = true
end
end
return true
end
function MarathonL1Game:onLineClear(cleared_row_count)
function MarathonAXGame:onLineClear(cleared_row_count)
if not self.clear then
local new_lines = self.lines + cleared_row_count
self:updateSectionTimes(self.lines, new_lines)
@@ -106,11 +99,11 @@ function MarathonL1Game:onLineClear(cleared_row_count)
end
end
function MarathonL1Game:getSectionTime()
function MarathonAXGame:getSectionTime()
return self.frames - self.section_start_time
end
function MarathonL1Game:updateSectionTimes(old_lines, new_lines)
function MarathonAXGame: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())
@@ -119,23 +112,23 @@ function MarathonL1Game:updateSectionTimes(old_lines, new_lines)
end
end
function MarathonL1Game:onPieceEnter()
function MarathonAXGame:onPieceEnter()
self.section_clear = false
end
function MarathonL1Game:drawGrid(ruleset)
function MarathonAXGame:drawGrid(ruleset)
self.grid:draw()
end
function MarathonL1Game:getHighscoreData()
function MarathonAXGame:getHighscoreData()
return {
lines = self.lines,
frames = self.frames,
}
end
function MarathonL1Game:drawScoringInfo()
MarathonL1Game.super.drawScoringInfo(self)
function MarathonAXGame:drawScoringInfo()
MarathonAXGame.super.drawScoringInfo(self)
love.graphics.setColor(1, 1, 1, 1)
@@ -157,7 +150,7 @@ function MarathonL1Game:drawScoringInfo()
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)
local time_left = self:getSectionTimeLimit() - 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
@@ -165,12 +158,12 @@ function MarathonL1Game:drawScoringInfo()
love.graphics.setColor(1, 1, 1, 1)
end
function MarathonL1Game:getSectionEndLines()
function MarathonAXGame:getSectionEndLines()
return math.floor(self.lines / 10 + 1) * 10
end
function MarathonL1Game:getBackground()
function MarathonAXGame:getBackground()
return math.floor(self.lines / 10)
end
return MarathonL1Game
return MarathonAXGame

View File

@@ -0,0 +1,42 @@
require 'funcs'
local MarathonAX = require 'tetris.modes.marathon_ax'
local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local MarathonAX2Game = MarathonAX:extend()
MarathonAX2Game.name = "Marathon AX2"
MarathonAX2Game.hash = "MarathonAX2"
MarathonAX2Game.tagline = "Can you clear the time hurdles when the game goes this fast?"
function MarathonAX2Game:new()
MarathonAX2Game.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 MarathonAX2Game:getGravity()
if self.lines < 10 then return 84/256
elseif self.lines < 20 then return 1/2
elseif self.lines < 30 then return 1
elseif self.lines < 40 then return 2
elseif self.lines < 50 then return 3
elseif self.lines < 60 then return 4
elseif self.lines < 70 then return 5
else return 20 end
end
return MarathonAX2Game

View File

@@ -0,0 +1,35 @@
require 'funcs'
local MarathonAX = require 'tetris.modes.marathon_ax'
local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local MarathonAX3Game = MarathonAX:extend()
MarathonAX3Game.name = "Marathon AX3"
MarathonAX3Game.hash = "MarathonAX3"
MarathonAX3Game.tagline = "Can you clear the time hurdles when the game goes this fast?"
function MarathonAX3Game:new()
MarathonAX3Game.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 MarathonAX3Game:getGravity()
return 20
end
return MarathonAX3Game

View File

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

View File

@@ -0,0 +1,133 @@
require 'funcs'
local GameMode = require 'tetris.modes.gamemode'
local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local MarathonWCBGame = GameMode:extend()
MarathonWCBGame.name = "Marathon WCB"
MarathonWCBGame.hash = "MarathonWCB"
MarathonWCBGame.tagline = "When all the pieces slip right to their destinations... can you keep up?"
function MarathonWCBGame:new()
MarathonWCBGame.super:new()
self.pieces = 0
self.randomizer = History6RollsRandomizer()
self.lock_drop = true
self.lock_hard_drop = true
self.instant_hard_drop = true
self.instant_soft_drop = true
self.enable_hold = false
self.next_queue_length = 3
self.piece_is_active = false
end
function MarathonWCBGame:getDropSpeed()
return 20
end
function MarathonWCBGame:getARR()
return 0
end
function MarathonWCBGame:getARE()
return 0
end
function MarathonWCBGame:getLineARE()
return 0
end
function MarathonWCBGame:getDasLimit()
return 0
end
function MarathonWCBGame:getLineClearDelay()
return 0
end
function MarathonWCBGame:getLockDelay()
return math.huge
end
function MarathonWCBGame:getGravity()
return self.piece_is_active and 20 or 0
end
function MarathonWCBGame: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 MarathonWCBGame:onAttemptPieceMove()
if self.piece ~= nil then
-- don't let the piece move before it's finished dropping
self.piece:dropToBottom(self.grid)
end
self.piece_is_active = true
end
function MarathonWCBGame:onAttemptPieceRotate()
self.piece_is_active = true
end
function MarathonWCBGame:onPieceLock()
self.piece_is_active = false
self.pieces = self.pieces + 1
end
function MarathonWCBGame:onLineClear(cleared_row_count)
if not self.clear then
self.lines = self.lines + cleared_row_count
end
end
function MarathonWCBGame:drawGrid(ruleset)
self.grid:draw()
if self.piece ~= nil then
self:drawGhostPiece(ruleset)
end
end
function MarathonWCBGame:getHighscoreData()
return {
pieces = self.pieces,
frames = self.frames,
}
end
function MarathonWCBGame:drawScoringInfo()
MarathonWCBGame.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, 160, 80, "left")
love.graphics.printf("pieces", text_x, 220, 80, "left")
love.graphics.setFont(font_3x5_3)
love.graphics.printf(self.lines, text_x, 180, 80, "left")
love.graphics.printf(self.pieces, text_x, 240, 80, "left")
end
function MarathonWCBGame:getBackground()
return (math.floor(self.pieces / 50) % 20)
end
return MarathonWCBGame

View File

@@ -164,7 +164,7 @@ function PacerTest:drawScoringInfo()
end
function PacerTest:getBackground()
return math.min(self.level - 1, 20)
return math.min(self.level - 1, 19)
end
return PacerTest

View File

@@ -11,9 +11,6 @@ PhantomManiaGame.name = "Phantom Mania"
PhantomManiaGame.hash = "PhantomMania"
PhantomManiaGame.tagline = "The blocks disappear as soon as they're locked! Can you remember where everything is?"
function PhantomManiaGame:new()
PhantomManiaGame.super:new()
@@ -102,7 +99,7 @@ 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 or self:hitTorikan(self.level, new_level) then
if new_level >= 999 then
self.level = 999
end

View File

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

View File

@@ -1,200 +1,16 @@
require 'funcs'
local PhantomManiaGame = require 'tetris.modes.phantom_mania'
local GameMode = require 'tetris.modes.gamemode'
local Piece = require 'tetris.components.piece'
local PhantomManiaNGame = PhantomManiaGame:extend()
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
PhantomManiaNGame.name = "Phantom Mania N"
PhantomManiaNGame.hash = "PhantomManiaN"
PhantomManiaNGame.tagline = "The old mode from Nullpomino, for Ti-ARS and SRS support."
local PhantomManiaGame = GameMode:extend()
function PhantomManiaNGame:new()
PhantomManiaNGame.super:new()
PhantomManiaGame.name = "Phantom Mania N"
PhantomManiaGame.hash = "PhantomManiaN"
PhantomManiaGame.tagline = "The old mode from Nullpomino."
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
return PhantomManiaNGame

View File

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

View File

@@ -44,13 +44,14 @@ function SurvivalA3Game:getLineARE()
end
function SurvivalA3Game:getDasLimit()
if self.level < 200 then return 9
if self.level < 100 then return 9
elseif self.level < 500 then return 7
else return 5 end
end
function SurvivalA3Game:getLineClearDelay()
return self:getLineARE() - 2
if self.level < 1300 then return self:getLineARE() - 2
else return 6 end
end
function SurvivalA3Game:getLockDelay()
@@ -60,7 +61,8 @@ function SurvivalA3Game:getLockDelay()
elseif self.level < 600 then return 13
elseif self.level < 1100 then return 12
elseif self.level < 1200 then return 10
else return 8 end
elseif self.level < 1300 then return 8
else return 15 end
end
function SurvivalA3Game:getGravity()
@@ -130,6 +132,7 @@ function SurvivalA3Game:onLineClear(cleared_row_count)
if new_level >= 1300 or self:hitTorikan(self.level, new_level) then
if new_level >= 1300 then
self.level = 1300
self.big_mode = true
end
self.clear = true
self.grid:clear()
@@ -141,8 +144,8 @@ function SurvivalA3Game:onLineClear(cleared_row_count)
end
end
function SurvivalA3Game:onPieceLock()
self:advanceBottomRow(1)
function SurvivalA3Game:onPieceLock(piece, cleared_row_count)
if cleared_row_count == 0 then self:advanceBottomRow(1) end
end
function SurvivalA3Game:updateScore(level, drop_bonus, cleared_lines)
@@ -188,10 +191,8 @@ 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)
return "S" .. tostring(grade)
end
end

View File

@@ -0,0 +1,76 @@
require 'funcs'
local MarathonAX = require 'tetris.modes.marathon_ax'
local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local SurvivalAXGame = MarathonAX:extend()
SurvivalAXGame.name = "Survival AX"
SurvivalAXGame.hash = "SurvivalAX"
SurvivalAXGame.tagline = "Can you clear the time hurdles when the game goes this fast?"
function SurvivalAXGame:new()
SurvivalAXGame.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 SurvivalAXGame:getSectionTimeLimit()
return 3600
end
function SurvivalAXGame: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 SurvivalAXGame:getLineARE()
return self:getARE()
end
function SurvivalAXGame: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 SurvivalAXGame:getLineClearDelay()
if self.lines < 10 then return 14
elseif self.lines < 30 then return 8
else return 5 end
end
function SurvivalAXGame:getLockDelay()
if self.lines < 10 then return 30
elseif self.lines < 20 then return 26
elseif self.lines < 30 then return 24
elseif self.lines < 40 then return 22
elseif self.lines < 50 then return 20
elseif self.lines < 70 then return 16
else return 15 end
end
function SurvivalAXGame:getGravity()
return 20
end
return SurvivalAXGame

View File

@@ -0,0 +1,59 @@
require 'funcs'
local MarathonAX2 = require 'tetris.modes.marathon_ax2'
local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local SurvivalAX2Game = MarathonAX2:extend()
SurvivalAX2Game.name = "Survival AX2"
SurvivalAX2Game.hash = "SurvivalAX2"
SurvivalAX2Game.tagline = "Can you clear the time hurdles when the game goes this fast?"
function SurvivalAX2Game:new()
SurvivalAX2Game.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 SurvivalAX2Game:getSectionTimeLimit()
return 3600
end
function SurvivalAX2Game:getARE()
return 6
end
function SurvivalAX2Game:getLineARE()
return self:getARE()
end
function SurvivalAX2Game:getDasLimit()
return 7
end
function SurvivalAX2Game:getLineClearDelay()
return 5
end
function SurvivalAX2Game:getLockDelay()
return 15
end
function SurvivalAX2Game:getGravity()
return 20
end
return SurvivalAX2Game

View File

@@ -0,0 +1,18 @@
local Randomizer = require 'tetris.randomizers.randomizer'
local AlwaysRandomizer = Randomizer:extend()
function AlwaysRandomizer:new(piece)
self.piece = piece
self:initialize()
self.next_queue = {}
for i = 1, 30 do
table.insert(self.next_queue, self:generatePiece())
end
end
function AlwaysRandomizer:generatePiece()
return self.piece
end
return AlwaysRandomizer

View File

@@ -1,9 +0,0 @@
local Randomizer = require 'tetris.randomizers.randomizer'
local AlwaysORandomizer = Randomizer:extend()
function AlwaysORandomizer:generatePiece()
return "O"
end
return AlwaysORandomizer

View File

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

View File

@@ -0,0 +1,32 @@
local ArikaTI = require 'tetris.rulesets.arika_ti'
local ARS = ArikaTI:extend()
ARS.name = "Ace-ARS"
ARS.hash = "ArikaAce"
function ARS:onPieceCreate(piece, grid)
piece.floorkick = 0
piece.rotate_counter = 0
piece.move_counter = 0
end
function ARS:onPieceDrop(piece, grid)
piece.lock_delay = 0 -- step reset
end
function ARS:onPieceMove(piece, grid)
piece.lock_delay = 0 -- move reset
if piece:isDropBlocked(grid) then
piece.move_counter = piece.move_counter + 1
if piece.move_counter >= 128 then
piece.locked = true
end
end
end
function ARS:onPieceRotate(piece, grid)
self:onPieceMove(piece, grid)
end
return ARS

View File

@@ -16,6 +16,16 @@ ARS.spawn_positions = {
Z = { x=4, y=5 },
}
ARS.big_spawn_positions = {
I = { x=2, y=2 },
J = { x=2, y=3 },
L = { x=2, y=3 },
O = { x=2, y=3 },
S = { x=2, y=3 },
T = { x=2, y=3 },
Z = { x=2, y=3 },
}
ARS.block_offsets = {
I={
{ {x=0, y=0}, {x=-1, y=0}, {x=-2, y=0}, {x=1, y=0} },
@@ -107,13 +117,12 @@ function ARS:attemptWallkicks(piece, new_piece, rot_dir, grid)
end
end
elseif piece.shape ~= "I" then
-- kick right, kick left
if (grid:canPlacePiece(new_piece:withOffset({x=1, y=0}))) then
piece:setRelativeRotation(rot_dir):setOffset({x=1, y=0})
elseif (grid:canPlacePiece(new_piece:withOffset({x=-1, y=0}))) then
piece:setRelativeRotation(rot_dir):setOffset({x=-1, y=0})
end
else
-- kick right, kick left
if (grid:canPlacePiece(new_piece:withOffset({x=1, y=0}))) then
piece:setRelativeRotation(rot_dir):setOffset({x=1, y=0})
elseif (grid:canPlacePiece(new_piece:withOffset({x=-1, y=0}))) then
piece:setRelativeRotation(rot_dir):setOffset({x=-1, y=0})
end
end
end

View File

@@ -16,6 +16,16 @@ CRS.spawn_positions = {
Z = { x=4, y=4 },
}
CRS.big_spawn_positions = {
I = { x=2, y=2 },
J = { x=2, y=3 },
L = { x=2, y=3 },
O = { x=2, y=3 },
S = { x=2, y=2 },
T = { x=2, y=3 },
Z = { x=2, y=2 },
}
CRS.block_offsets = {
I={
{ {x=0, y=0}, {x=-1, y=0}, {x=-2, y=0}, {x=1, y=0} },
@@ -388,6 +398,7 @@ function CRS:onPieceDrop(piece, grid)
end
function CRS:onPieceMove(piece, grid)
piece.lock_delay = 0 -- move reset
if piece:isDropBlocked(grid) then
piece.move_counter = piece.move_counter + 1
if piece.move_counter >= 24 then
@@ -397,6 +408,7 @@ function CRS:onPieceMove(piece, grid)
end
function CRS:onPieceRotate(piece, grid)
piece.lock_delay = 0 -- move reset
if piece:isDropBlocked(grid) then
piece.rotate_counter = piece.rotate_counter + 1
if piece.rotate_counter >= 12 then
@@ -405,6 +417,4 @@ function CRS:onPieceRotate(piece, grid)
end
end
function CRS:getDefaultOrientation() return 1 end -- downward facing pieces by default
return CRS

View File

@@ -56,26 +56,33 @@ function Ruleset:attemptWallkicks(piece, new_piece, rot_dir, grid)
-- do nothing in default ruleset
end
function Ruleset:movePiece(piece, grid, move)
function Ruleset:movePiece(piece, grid, move, instant)
local x = piece.position.x
if move == "left" then
piece:moveInGrid({x=-1, y=0}, 1, grid)
elseif move == "speedleft" then
piece:moveInGrid({x=-1, y=0}, 10, grid)
piece:moveInGrid({x=-1, y=0}, 1, grid, false)
elseif move == "right" then
piece:moveInGrid({x=1, y=0}, 1, grid)
piece:moveInGrid({x=1, y=0}, 1, grid, false)
elseif move == "speedleft" then
piece:moveInGrid({x=-1, y=0}, 10, grid, instant)
elseif move == "speedright" then
piece:moveInGrid({x=1, y=0}, 10, grid)
piece:moveInGrid({x=1, y=0}, 10, grid, instant)
end
if piece.position.x ~= x then
self:onPieceMove(piece, grid)
end
end
function Ruleset:dropPiece(inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked, hard_drop_enabled)
function Ruleset:dropPiece(
inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked,
hard_drop_enabled, additive_gravity
)
local y = piece.position.y
if inputs["down"] == true and drop_locked == false then
piece:addGravity(gravity + drop_speed, grid)
if additive_gravity then
piece:addGravity(gravity + drop_speed, grid)
else
piece:addGravity(math.max(gravity, drop_speed), grid)
end
elseif inputs["up"] == true and hard_drop_enabled == true then
if hard_drop_locked == true or piece:isDropBlocked(grid) then
piece:addGravity(gravity, grid)
@@ -102,12 +109,19 @@ function Ruleset:getDefaultOrientation() return 1 end
function Ruleset:initializePiece(
inputs, data, grid, gravity, prev_inputs,
move, lock_delay, drop_speed,
drop_locked, hard_drop_locked
drop_locked, hard_drop_locked, big
)
local spawn_positions
if big then
spawn_positions = self.big_spawn_positions
else
spawn_positions = self.spawn_positions
end
local piece = Piece(data.shape, data.orientation - 1, {
x = self.spawn_positions[data.shape].x,
y = self.spawn_positions[data.shape].y
}, self.block_offsets, 0, 0, data.skin)
x = spawn_positions[data.shape].x,
y = spawn_positions[data.shape].y
}, self.block_offsets, 0, 0, data.skin, big)
self:onPieceCreate(piece)
self:rotatePiece(inputs, piece, grid, {}, true)
self:dropPiece(inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked)
@@ -120,11 +134,15 @@ function Ruleset:onPieceCreate(piece) end
function Ruleset:processPiece(
inputs, piece, grid, gravity, prev_inputs,
move, lock_delay, drop_speed,
drop_locked, hard_drop_locked, hard_drop_enabled
drop_locked, hard_drop_locked,
hard_drop_enabled, additive_gravity
)
self:rotatePiece(inputs, piece, grid, prev_inputs, false)
self:movePiece(piece, grid, move)
self:dropPiece(inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked, hard_drop_enabled)
self:movePiece(piece, grid, move, gravity >= 20)
self:dropPiece(
inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked,
hard_drop_enabled, additive_gravity
)
self:lockPiece(piece, grid, lock_delay)
end

View File

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

View File

@@ -0,0 +1,30 @@
local Standard = require 'tetris.rulesets.standard'
local SRS = Standard:extend()
SRS.name = "Ti-SRS"
SRS.hash = "StandardTI"
function SRS:onPieceMove(piece, grid)
piece.lock_delay = 0 -- move reset
if piece:isDropBlocked(grid) then
piece.move_counter = piece.move_counter + 1
if piece.move_counter >= 10 then
piece.locked = true
end
end
end
function SRS:onPieceRotate(piece, grid)
piece.lock_delay = 0 -- rotate reset
if piece:isDropBlocked(grid) then
piece.rotate_counter = piece.rotate_counter + 1
if piece.rotate_counter >= 8 then
piece.locked = true
end
end
end
function SRS:get180RotationValue() return config["reverse_rotate"] and 1 or 3 end
return SRS