Compare commits

..

99 Commits

Author SHA1 Message Date
Oshisaure
e6a60b0021 Merge branch 'master' of https://github.com/SashLilac/cambridge into master 2020-10-19 16:54:27 +01:00
Oshisaure
5f29c987f2 Tweaked Cambridge RS to fix how S and Z show in the next queue 2020-10-19 16:50:49 +01:00
Ishaan Bhardwaj
608d75b1ac Update README.md 2020-10-19 08:45:22 -04:00
Oshisaure
1427c0d19e Updated Ti-World and ACE-SRS to use the Arika kick table
This possibly needs further testing, although the example
given [on the wiki](tetris.wiki/SRS#Arika_SRS) works
2020-10-19 05:07:31 +01:00
Ishaan Bhardwaj
e221a91d73 Swapped some settings around 2020-10-18 23:32:57 -04:00
Oshisaure
bdcd25b82c Fixed T floorkick in ARS Ti/ACE/ACE2 2020-10-19 04:19:36 +01:00
Ishaan Bhardwaj
a5158e0994 Fixed Demon Mode torikan madness 2020-10-18 21:54:24 -04:00
Oshisaure
d946b17e13 Minor change on PR #3 to use the error handling that was already implemented 2020-10-18 00:40:14 +01:00
Oshisaure
69a5c0a21a Merge pull request #3 from MyPasswordIsWeak/master
Fixed discord rpc not working on linux
2020-10-18 00:12:50 +01:00
MyPasswordIsWeak
b6423c3335 Change tabs to spaces for consistency 2020-10-17 21:17:49 +02:00
MyPasswordIsWeak
5b960d7291 Change single quotes to double quotes 2020-10-17 21:15:06 +02:00
MyPasswordIsWeak
54f4b0b890 Fixed rpc not working on linux 2020-10-17 21:11:38 +02:00
Oshisaure
8c62f321a0 Added secret grade for the Phantom Mania/-2/-N modes 2020-10-17 04:22:41 +01:00
Oshisaure
fdffd2cd9a Actually made ACE-ARS2 selectable 2020-10-17 04:17:25 +01:00
Oshisaure
8ddf468121 Removed print statements leftover from when i was making sure the torikans were working as intended 2020-10-17 03:42:31 +01:00
Oshisaure
8e77407ff2 Merge branch 'master' of https://github.com/SashLilac/cambridge 2020-10-17 03:38:07 +01:00
Oshisaure
92c852d178 Renamed ACE-ARS to ACE-ARS2 and added ACE-ARS that guideline piece lock and colours 2020-10-17 03:37:44 +01:00
Ishaan Bhardwaj
f658ed63f2 apparently I made ti-srs too strict 2020-10-16 22:20:18 -04:00
Oshisaure
c2d1c1183c Survival A3 torikan now switches to 03:03:00 when playing with World-type rulesets 2020-10-17 02:51:29 +01:00
Ishaan Bhardwaj
36c568feaf demon mode grade display fixed 2020-10-16 21:35:30 -04:00
Ishaan Bhardwaj
bf30fcefbd Add Sega randomizer 2020-10-14 15:00:17 -04:00
Oshisaure
d9f5bd16d7 Disabled BGM for now since the only music is the pacer test (not selectable in game) and the credit roll 2020-10-14 19:30:37 +01:00
Ishaan Bhardwaj
d978ff8d87 fixed a thing 2020-10-14 13:45:56 -04:00
Ishaan Bhardwaj
b47d0f36b9 oshi forced me to add bags 2020-10-14 13:43:28 -04:00
Ishaan Bhardwaj
abc210c69c made our debug randomizer not bad 2020-10-13 22:10:27 -04:00
Ishaan Bhardwaj
436e4ac861 Update README.md 2020-10-13 15:56:11 -04:00
Oshisaure
a48d7c67b5 Merge branch 'HEAD' of https://github.com/SashLilac/cambridge.git 2020-10-13 20:26:33 +01:00
Oshisaure
f6ca79ff91 Fixed big mode spawn positions 2020-10-13 20:26:07 +01:00
Ishaan Bhardwaj
4eb3901610 Some big mode fixes 2020-10-13 14:16:23 -04:00
Ishaan Bhardwaj
3f7fc4b622 Rename Mac RPC lib 2020-10-13 11:20:46 -04:00
Ishaan Bhardwaj
ac7ae91c39 Small RPC change 2020-10-12 22:44:47 -04:00
Oshisaure
0c8e910245 Linux RPC maybe? 2020-10-13 01:23:24 +01:00
Oshisaure
6233ffb12d Fixed RPC icon and included RPC lib for mac 2020-10-13 00:39:13 +01:00
Oshisaure
1f78bb9e99 Merge branch 'HEAD' of https://github.com/SashLilac/cambridge.git 2020-10-12 21:22:15 +01:00
Oshisaure
a125c09106 Fixed crash on loading the game with no save 2020-10-12 21:21:10 +01:00
Ishaan Bhardwaj
090ffa5126 Update README.md 2020-10-12 14:48:00 -04:00
Ishaan Bhardwaj
12a6f42198 Revert "Fixing step reset" - didn't realize infinite floorkicks
This reverts commit 0c317d9ce1.
2020-10-11 15:46:34 -04:00
Ishaan Bhardwaj
0c317d9ce1 Fixing step reset 2020-10-11 15:41:56 -04:00
Ishaan Bhardwaj
eddfee566d Grade display changed for TA Death modes 2020-10-11 15:34:10 -04:00
Ishaan Bhardwaj
7fe366a8de Bravo score update 2020-10-11 14:17:18 -04:00
Ishaan Bhardwaj
55be30c99f experimental bravo formula for tgm1 2020-10-11 13:21:03 -04:00
Ishaan Bhardwaj
36ceef8488 experimental bravo formula for tgm1 2020-10-11 12:57:57 -04:00
Oshisaure
b59edb5e8e Accidentally swapped blue and orange in the colour scheme update, changing it back with this commit 2020-10-11 03:12:22 +01:00
Oshisaure
5d32b6a3e7 Discord RPC cleanup
- Loading Discord RPC is now handled by `load/rpc.lua`
- Removed `presence` global, call `DiscordRPC:update()` directly with what needs updating
- Game doesn't crash anymore if the Discord RPC fails to load
- Added RPC variables in the gamemode superclass to let each gamemode handle its special case
2020-10-11 02:17:48 +01:00
Oshisaure
05230ac046 Game settings screen, and minor fix on discordRPC
- Uses BG previously from the input config screen, which has gotten a new BG
- Minor tweak on the input config screen to display all inputs names regardless of if they are bound or not
- Added Mod1 function to `funcs.lua`, may be useful again sometime
- Added game settings
  * Manual locking (per gamemode, per ruleset, on harddrop or on softdrop)
  * Piece colours (per ruleset, TTC or Arika)
  * World Reverse toggle
- Moved the discordRPC `libs/` directory, as it's a third party library
- Edited the `discordRPC.lua` file to look for the dll at the right place regardless of how you run the game (until we fuse it that is)

This should have probably been done in several commits, sorry about that
2020-10-11 00:42:56 +01:00
Oshisaure
f28dc08ae2 Updated Race 40 randomiser to use 7-bag no SZO start 2020-10-10 23:07:12 +01:00
Ishaan Bhardwaj
ecd958bdc5 Update README.md 2020-10-10 10:30:31 -04:00
Ishaan Bhardwaj
43f59cfde8 Merge pull request #1 from haileylgbt/master
Added fitting menu sfx + RPC
2020-10-09 21:37:45 -04:00
Ishaan Bhardwaj
00c46961f9 Bug. 2020-10-09 21:31:49 -04:00
Ishaan Bhardwaj
0a0053276b SG!!!! 2020-10-09 21:14:20 -04:00
Hailey
d0f1d869a8 RP Icon 2020-10-10 09:47:33 +10:00
Hailey
29ee000998 Minor Rich Presence adjustments 2020-10-10 09:05:59 +10:00
Hailey
995fd7fee9 rich presence!! 2020-10-10 08:43:22 +10:00
Hailey
67abf35a28 Merge pull request #1 from SashLilac/master
Removed the BG limit, because someone is a madman
2020-10-10 08:02:00 +10:00
Ishaan Bhardwaj
9982613e26 Removed the BG limit, because someone is a madman 2020-10-09 17:55:22 -04:00
Hailey
629beb7240 Added fitting menu sfx 2020-10-10 07:50:05 +10:00
Oshisaure
4cb20101b0 Added background for options screen 2020-10-09 18:44:48 +01:00
Ishaan Bhardwaj
63c0721978 Slight cosmetic change concerning the bonus timer 2020-10-09 13:37:39 -04:00
Oshisaure
1366451a3d Fixed randomisers in A1-A4 modes:
- History 4-rolls, history 6-rolls and history 6-rolls 35-bag no longer deal S, Z or O as their first piece
- Added a 7-bag randomiser with the same behaviour (bag7noSZOstart)
- Tweaked the "35-bag" part of the history 6-rolls 35-bag randomiser to work closer to what it's supposed to be
- Switched Marathon AX4's randomiser from history 6-rolls to 7-bag no SZO start
2020-10-09 04:34:11 +01:00
Ishaan Bhardwaj
6b624b9853 Ti-World placeholder 2020-10-08 22:20:37 -04:00
Ishaan Bhardwaj
b0bda25466 Cool Regret System TM 2020-10-08 22:13:35 -04:00
Oshisaure
2bde9d1378 Added secret grade detection for Marathon A1-A3 and Survival A1-A3 2020-10-09 02:00:42 +01:00
Ishaan Bhardwaj
6178b2cee9 Proper bravo detection! 2020-10-08 20:44:06 -04:00
Ishaan Bhardwaj
4e8a237de3 TA GM requirements updated 2020-10-08 20:35:40 -04:00
Oshisaure
5606251ea7 Added extra buttons:
- Implemented retry button*
- Escape on mode select sends you to title screen
- Escape on title screen closes the game
- Added "Exit Game" entry on title screen, closes the game when selected

*Pardon my angry comment in `scene/game.lua`.
2020-10-08 04:56:46 +01:00
Oshisaure
a4984fd687 Fixed jank regarding the area above the field
- The game now discards blocks locked over y = 1 instead of panicking and crashing
2020-10-07 20:40:43 +01:00
Ishaan Bhardwaj
b7ef7d1976 Fix I's ghost piece in ACE modes 2020-10-07 15:17:56 -04:00
Oshisaure
293b7398a2 Typo lol 2020-10-07 20:11:15 +01:00
Oshisaure
8a2237a77c Fixed Arika rulesets
- Fixed centre column rule on Classic ARS, Ti-ARS and ACE-ARS
- Added T floorkick for Ti-ARS and ACE-ARS
2020-10-07 19:54:18 +01:00
Ishaan Bhardwaj
bdad32ac79 For Oshisaure :) 2020-10-07 13:24:28 -04:00
Oshisaure
3cc918841f Added display for time limit extensions 2020-10-07 01:57:07 +01:00
Oshisaure
5f7ea0648e Fixed Konoha randomiser and added ghost before lv100 2020-10-06 23:53:02 +01:00
Oshisaure
5d34218b97 Merge branch 'master' of https://github.com/SashLilac/cambridge 2020-10-06 21:50:36 +01:00
Oshisaure
fcd8b0f360 Set a fixed height for piece previews 2020-10-06 21:45:57 +01:00
Ishaan Bhardwaj
b983e1c108 Merge branch 'master' of https://github.com/SashLilac/cambridge 2020-10-06 16:29:24 -04:00
Ishaan Bhardwaj
36ab451b70 Fix big mode spawns 2020-10-06 16:27:28 -04:00
Oshisaure
8fef7faa6a Fixed randomiser and next queue whhhackiness 2020-10-06 21:10:15 +01:00
Ishaan Bhardwaj
f13e2096b2 More randomizer fixes? 2020-10-06 15:49:06 -04:00
Oshisaure
6b7f18d58a Refactored funcs.lua
- Renamed st and sp to strTrueValues and frameTime respectively
- Modified files calling these to use the new names
- Tidying like formatTime now using a single string.format
2020-10-06 18:14:00 +01:00
Ishaan Bhardwaj
d5ce2ee9ba Update README.md 2020-10-06 10:22:23 -04:00
Ishaan Bhardwaj
f04b57e7eb Update README.md 2020-10-06 10:20:01 -04:00
Ishaan Bhardwaj
be644bf57b Made level counter not look awkward 2020-10-05 23:24:59 -04:00
Ishaan Bhardwaj
9d15feef33 Another fix? 2020-10-05 22:47:34 -04:00
Ishaan Bhardwaj
634a5bc03b Potential fix for bag not working 2020-10-05 22:33:57 -04:00
Ishaan Bhardwaj
f1ad1f0ea4 Added the lost Konoha mode from TGM4 2020-10-05 22:17:15 -04:00
Ishaan Bhardwaj
a534331b11 Ace-ARS! 2020-10-05 11:52:37 -04:00
Ishaan Bhardwaj
d602fdfc7e TAP big mode added 2020-10-05 11:11:46 -04:00
Ishaan Bhardwaj
971151e210 Added 5 bag randomizer 2020-10-04 22:49:17 -04:00
Ishaan Bhardwaj
593cad0e71 Update README to fork 2020-09-24 22:23:05 -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
77 changed files with 3235 additions and 921 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.

74
README.md Normal file
View File

@@ -0,0 +1,74 @@
![Cambridge Banner](https://cdn.discordapp.com/attachments/764432435802013709/767724895076614154/cambridge_logo_lt.png)
Cambridge
=========
Welcome to Cambridge, the next open-source falling-block game engine!
This fork is written and maintained exclusively by [SashLilac](https://github.com/SashLilac) and [Oshisaure](https://github.com/oshisaure)!
Join our Discord server for help and a welcoming community! https://discord.gg/mteMJw4
Credits
-------
- [Lilla Oshisaure](https://www.youtube.com/user/LeSpyroshisaure) for their amazing contributions to my life in general!
- [The Tetra Legends Discord](http://discord.com/invite/7hMx5r2) for supporting me and playtesting!
- [joezeng](https://github.com/joezeng) for the original project.
- [Hailey](https://github.com/haileylgbt) for some miscellaneous assets.
- MarkGamed7794 for some miscellaneous contributions.
- Mizu for the Cambridge logo and the [Cambridge launcher](https://github.com/rexxt/cambridge-launcher).
- MattMayuga for the Cambridge banner.
![Cambridge Logo](https://cdn.discordapp.com/attachments/625496179433668635/763363717730664458/Icon_2.png)
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 [Homebrew](https://brew.sh), 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/SashLilac/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,13 +1,26 @@
Game modes
==========
There are several classes of game modes.
There are several classes of game modes. The modes that originate from other games are organized by suffix:
* The "C" series stand for "Classic" games, games that were produced before around 1992-1993 and generally have no wallkicks or lock delay.
* C84 - The original version from the Electronika 60.
* C88 - Sega Tetris.
* C89 - Nintendo / NES Tetris.
* The "A" series stand for "Arika" games, or games in the Tetris the Grand Master series.
* A1 - Tetris The Grand Master (the original from 1998).
* A2 - Tetris The Absolute The Grand Master 2 PLUS.
* A3 - Tetris The Grand Master 3 Terror-Instinct.
* AX - Tetris The Grand Master ACE (X for Xbox).
* The "G" series stand for "Guideline" games, or games that follow the Tetris Guideline.
* GF - Tetris Friends (2007-2019)
* GJ - Tetris Online Japan (2005-2011)
* N stands for Nullpomino, only used for Phantom Mania N.
MARATHON
--------
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?
@@ -15,6 +28,8 @@ 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 AX4**: Another mode from TGM Ace.
* **MARATHON C89**: Nintendo NES Tetris. Can you transition and make it to the killscreen?
SURVIVAL
@@ -22,9 +37,40 @@ 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.
RACE
----
Modes with no levels, just a single timed goal.
* **Race 40**: How fast can you clear 40 lines? No limits, no holds barred.
PHANTOM MANIA
-------------
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

@@ -1,4 +1,5 @@
function copy(t)
-- returns deep copy of t (as opposed to the shallow copy you get from var = t)
if type(t) ~= "table" then return t end
local meta = getmetatable(t)
local target = {}
@@ -7,7 +8,8 @@ function copy(t)
return target
end
function st(tbl)
function strTrueValues(tbl)
-- returns a concatenation of all the keys in tbl with value true, separated with spaces
str = ""
for k, v in pairs(tbl) do
if v == true then
@@ -17,14 +19,16 @@ function st(tbl)
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)
function frameTime(min, sec, hth)
-- returns a time in frames from a time in minutes-seconds-hundredths format
if min == nil then min = 0 end
if sec == nil then sec = 0 end
if hth == nil then hth = 0 end
return min*3600 + sec*60 + math.ceil(hth * 0.6)
end
function vAdd(v1, v2)
-- returns the sum of vectors v1 and v2
return {
x = v1.x + v2.x,
y = v1.y + v2.y
@@ -32,6 +36,7 @@ function vAdd(v1, v2)
end
function vNeg(v)
-- returns the opposite of vector v
return {
x = -v.x,
y = -v.y
@@ -39,17 +44,26 @@ function vNeg(v)
end
function formatTime(frames)
-- returns a mm:ss:hh (h=hundredths) representation of the time in frames given
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)
local min, sec, hund
min = math.floor(frames/3600)
sec = math.floor(frames/60) % 60
hund = math.floor(frames/.6) % 100
str = string.format("%02d:%02d.%02d", min, sec, hund)
return str
end
function formatBigNum(number)
-- returns a string representing a number with commas as thousands separator (e.g. 12,345,678)
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
function Mod1(n, m)
-- returns a number congruent to n modulo m in the range [1;m] (as opposed to [0;m-1])
return ((n-1) % m) + 1
end

BIN
libs/discord-rpc.dll Normal file

Binary file not shown.

BIN
libs/discord-rpc.dylib Normal file

Binary file not shown.

BIN
libs/discord-rpc.so Normal file

Binary file not shown.

268
libs/discordRPC.lua Normal file
View File

@@ -0,0 +1,268 @@
local ffi = require "ffi"
-- Get the host os to load correct lib
local osname = love.system.getOS()
local discordRPClib = nil
if osname == "Linux" then
discordRPClib = ffi.load(love.filesystem.getSource().."/libs/discord-rpc.so")
elseif osname == "OS X" then
discordRPClib = ffi.load(love.filesystem.getSource().."/libs/discord-rpc.dylib")
elseif osname == "Windows" then
discordRPClib = ffi.load(love.filesystem.getSource().."/libs/discord-rpc.dll")
else
-- Else it crashes later on
error(string.format("Discord rpc not supported on platform (%s)", osname))
end
ffi.cdef[[
typedef struct DiscordRichPresence {
const char* state; /* max 128 bytes */
const char* details; /* max 128 bytes */
int64_t startTimestamp;
int64_t endTimestamp;
const char* largeImageKey; /* max 32 bytes */
const char* largeImageText; /* max 128 bytes */
const char* smallImageKey; /* max 32 bytes */
const char* smallImageText; /* max 128 bytes */
const char* partyId; /* max 128 bytes */
int partySize;
int partyMax;
const char* matchSecret; /* max 128 bytes */
const char* joinSecret; /* max 128 bytes */
const char* spectateSecret; /* max 128 bytes */
int8_t instance;
} DiscordRichPresence;
typedef struct DiscordUser {
const char* userId;
const char* username;
const char* discriminator;
const char* avatar;
} DiscordUser;
typedef void (*readyPtr)(const DiscordUser* request);
typedef void (*disconnectedPtr)(int errorCode, const char* message);
typedef void (*erroredPtr)(int errorCode, const char* message);
typedef void (*joinGamePtr)(const char* joinSecret);
typedef void (*spectateGamePtr)(const char* spectateSecret);
typedef void (*joinRequestPtr)(const DiscordUser* request);
typedef struct DiscordEventHandlers {
readyPtr ready;
disconnectedPtr disconnected;
erroredPtr errored;
joinGamePtr joinGame;
spectateGamePtr spectateGame;
joinRequestPtr joinRequest;
} DiscordEventHandlers;
void Discord_Initialize(const char* applicationId,
DiscordEventHandlers* handlers,
int autoRegister,
const char* optionalSteamId);
void Discord_Shutdown(void);
void Discord_RunCallbacks(void);
void Discord_UpdatePresence(const DiscordRichPresence* presence);
void Discord_ClearPresence(void);
void Discord_Respond(const char* userid, int reply);
void Discord_UpdateHandlers(DiscordEventHandlers* handlers);
]]
local discordRPC = {} -- module table
-- proxy to detect garbage collection of the module
discordRPC.gcDummy = newproxy(true)
local function unpackDiscordUser(request)
return ffi.string(request.userId), ffi.string(request.username),
ffi.string(request.discriminator), ffi.string(request.avatar)
end
-- callback proxies
-- note: callbacks are not JIT compiled (= SLOW), try to avoid doing performance critical tasks in them
-- luajit.org/ext_ffi_semantics.html
local ready_proxy = ffi.cast("readyPtr", function(request)
if discordRPC.ready then
discordRPC.ready(unpackDiscordUser(request))
end
end)
local disconnected_proxy = ffi.cast("disconnectedPtr", function(errorCode, message)
if discordRPC.disconnected then
discordRPC.disconnected(errorCode, ffi.string(message))
end
end)
local errored_proxy = ffi.cast("erroredPtr", function(errorCode, message)
if discordRPC.errored then
discordRPC.errored(errorCode, ffi.string(message))
end
end)
local joinGame_proxy = ffi.cast("joinGamePtr", function(joinSecret)
if discordRPC.joinGame then
discordRPC.joinGame(ffi.string(joinSecret))
end
end)
local spectateGame_proxy = ffi.cast("spectateGamePtr", function(spectateSecret)
if discordRPC.spectateGame then
discordRPC.spectateGame(ffi.string(spectateSecret))
end
end)
local joinRequest_proxy = ffi.cast("joinRequestPtr", function(request)
if discordRPC.joinRequest then
discordRPC.joinRequest(unpackDiscordUser(request))
end
end)
-- helpers
local function checkArg(arg, argType, argName, func, maybeNil)
assert(type(arg) == argType or (maybeNil and arg == nil),
string.format("Argument \"%s\" to function \"%s\" has to be of type \"%s\"",
argName, func, argType))
end
local function checkStrArg(arg, maxLen, argName, func, maybeNil)
if maxLen then
assert(type(arg) == "string" and arg:len() <= maxLen or (maybeNil and arg == nil),
string.format("Argument \"%s\" of function \"%s\" has to be of type string with maximum length %d",
argName, func, maxLen))
else
checkArg(arg, "string", argName, func, true)
end
end
local function checkIntArg(arg, maxBits, argName, func, maybeNil)
maxBits = math.min(maxBits or 32, 52) -- lua number (double) can only store integers < 2^53
local maxVal = 2^(maxBits-1) -- assuming signed integers, which, for now, are the only ones in use
assert(type(arg) == "number" and math.floor(arg) == arg
and arg < maxVal and arg >= -maxVal
or (maybeNil and arg == nil),
string.format("Argument \"%s\" of function \"%s\" has to be a whole number <= %d",
argName, func, maxVal))
end
-- function wrappers
function discordRPC.initialize(applicationId, autoRegister, optionalSteamId)
local func = "discordRPC.Initialize"
checkStrArg(applicationId, nil, "applicationId", func)
checkArg(autoRegister, "boolean", "autoRegister", func)
if optionalSteamId ~= nil then
checkStrArg(optionalSteamId, nil, "optionalSteamId", func)
end
local eventHandlers = ffi.new("struct DiscordEventHandlers")
eventHandlers.ready = ready_proxy
eventHandlers.disconnected = disconnected_proxy
eventHandlers.errored = errored_proxy
eventHandlers.joinGame = joinGame_proxy
eventHandlers.spectateGame = spectateGame_proxy
eventHandlers.joinRequest = joinRequest_proxy
discordRPClib.Discord_Initialize(applicationId, eventHandlers,
autoRegister and 1 or 0, optionalSteamId)
end
function discordRPC.shutdown()
discordRPClib.Discord_Shutdown()
end
function discordRPC.runCallbacks()
discordRPClib.Discord_RunCallbacks()
end
-- http://luajit.org/ext_ffi_semantics.html#callback :
-- It is not allowed, to let an FFI call into a C function (runCallbacks)
-- get JIT-compiled, which in turn calls a callback, calling into Lua again (e.g. discordRPC.ready).
-- Usually this attempt is caught by the interpreter first and the C function
-- is blacklisted for compilation.
-- solution:
-- "Then you'll need to manually turn off JIT-compilation with jit.off() for
-- the surrounding Lua function that invokes such a message polling function."
jit.off(discordRPC.runCallbacks)
function discordRPC.updatePresence(presence)
local func = "discordRPC.updatePresence"
checkArg(presence, "table", "presence", func)
-- -1 for string length because of 0-termination
checkStrArg(presence.state, 127, "presence.state", func, true)
checkStrArg(presence.details, 127, "presence.details", func, true)
checkIntArg(presence.startTimestamp, 64, "presence.startTimestamp", func, true)
checkIntArg(presence.endTimestamp, 64, "presence.endTimestamp", func, true)
checkStrArg(presence.largeImageKey, 31, "presence.largeImageKey", func, true)
checkStrArg(presence.largeImageText, 127, "presence.largeImageText", func, true)
checkStrArg(presence.smallImageKey, 31, "presence.smallImageKey", func, true)
checkStrArg(presence.smallImageText, 127, "presence.smallImageText", func, true)
checkStrArg(presence.partyId, 127, "presence.partyId", func, true)
checkIntArg(presence.partySize, 32, "presence.partySize", func, true)
checkIntArg(presence.partyMax, 32, "presence.partyMax", func, true)
checkStrArg(presence.matchSecret, 127, "presence.matchSecret", func, true)
checkStrArg(presence.joinSecret, 127, "presence.joinSecret", func, true)
checkStrArg(presence.spectateSecret, 127, "presence.spectateSecret", func, true)
checkIntArg(presence.instance, 8, "presence.instance", func, true)
local cpresence = ffi.new("struct DiscordRichPresence")
cpresence.state = presence.state
cpresence.details = presence.details
cpresence.startTimestamp = presence.startTimestamp or 0
cpresence.endTimestamp = presence.endTimestamp or 0
cpresence.largeImageKey = presence.largeImageKey
cpresence.largeImageText = presence.largeImageText
cpresence.smallImageKey = presence.smallImageKey
cpresence.smallImageText = presence.smallImageText
cpresence.partyId = presence.partyId
cpresence.partySize = presence.partySize or 0
cpresence.partyMax = presence.partyMax or 0
cpresence.matchSecret = presence.matchSecret
cpresence.joinSecret = presence.joinSecret
cpresence.spectateSecret = presence.spectateSecret
cpresence.instance = presence.instance or 0
discordRPClib.Discord_UpdatePresence(cpresence)
end
function discordRPC.clearPresence()
discordRPClib.Discord_ClearPresence()
end
local replyMap = {
no = 0,
yes = 1,
ignore = 2
}
-- maybe let reply take ints too (0, 1, 2) and add constants to the module
function discordRPC.respond(userId, reply)
checkStrArg(userId, nil, "userId", "discordRPC.respond")
assert(replyMap[reply], "Argument 'reply' to discordRPC.respond has to be one of \"yes\", \"no\" or \"ignore\"")
discordRPClib.Discord_Respond(userId, replyMap[reply])
end
-- garbage collection callback
getmetatable(discordRPC.gcDummy).__gc = function()
discordRPC.shutdown()
ready_proxy:free()
disconnected_proxy:free()
errored_proxy:free()
joinGame_proxy:free()
spectateGame_proxy:free()
joinRequest_proxy:free()
end
return discordRPC

View File

@@ -6,7 +6,7 @@ bgm = {
}
local current_bgm = nil
local bgm_locked = false
local bgm_locked = true
function switchBGM(sound, subsound)
if bgm_locked then return end

View File

@@ -20,35 +20,60 @@ backgrounds = {
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"),
input_config = love.graphics.newImage("res/backgrounds/options-pcb.png"),
game_config = love.graphics.newImage("res/backgrounds/options-gears.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"),
R = love.graphics.newImage("res/img/s1.png"),
O = love.graphics.newImage("res/img/s3.png"),
Y = love.graphics.newImage("res/img/s7.png"),
G = love.graphics.newImage("res/img/s6.png"),
C = love.graphics.newImage("res/img/s2.png"),
B = love.graphics.newImage("res/img/s4.png"),
M = love.graphics.newImage("res/img/s5.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"),
R = 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"),
Y = love.graphics.newImage("res/img/bone.png"),
G = love.graphics.newImage("res/img/bone.png"),
C = love.graphics.newImage("res/img/bone.png"),
B = love.graphics.newImage("res/img/bone.png"),
M = love.graphics.newImage("res/img/bone.png"),
X = love.graphics.newImage("res/img/bone.png"),
}
}
ColourSchemes = {
Arika = {
I = "R",
L = "O",
J = "B",
S = "M",
Z = "G",
O = "Y",
T = "C",
},
TTC = {
I = "C",
L = "O",
J = "B",
S = "G",
Z = "R",
O = "Y",
T = "M",
},
}
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"),

58
load/rpc.lua Normal file
View File

@@ -0,0 +1,58 @@
print("Loading discord RPC...")
DiscordRPC = {
loaded = false
}
local success, RPC = pcall(require, "libs.discordRPC")
if success then
DiscordRPC.loaded = true
DiscordRPC.appId = "599778517789573120"
function RPC.ready(userId, username, discriminator, avatar)
print(string.format("Discord: ready (%s, %s, %s, %s)", userId, username, discriminator, avatar))
end
function RPC.disconnected(errorCode, message)
print(string.format("Discord: disconnected (%d: %s)", errorCode, message))
end
function RPC.errored(errorCode, message)
print(string.format("Discord: error (%d: %s)", errorCode, message))
end
function RPC.joinGame(joinSecret)
print(string.format("Discord: join (%s)", joinSecret))
end
function RPC.spectateGame(spectateSecret)
print(string.format("Discord: spectate (%s)", spectateSecret))
end
function RPC.joinRequest(userId, username, discriminator, avatar)
print(string.format("Discord: join request (%s, %s, %s, %s)", userId, username, discriminator, avatar))
RPC.respond(userId, "yes")
end
RPC.initialize(DiscordRPC.appId, true)
local now = os.time(os.date("*t"))
DiscordRPC.RPC = RPC
print("DiscordRPC successfully loaded.")
else
print("DiscordRPC failed to load!")
print(RPC)
end
DiscordRPC.presence = {
startTimestamp = now,
details = "Loading game...",
state = "",
largeImageKey = "icon2",
largeImageText = "Arcade Stacker",
smallImageKey = "",
smallImageText = ""
}
function DiscordRPC:update(newstuff)
for k, v in pairs(newstuff) do self.presence[k] = v end
if self.loaded then self.RPC.updatePresence(self.presence) end
end

View File

@@ -10,6 +10,10 @@ sounds = {
},
move = love.audio.newSource("res/se/move.wav", "static"),
bottom = love.audio.newSource("res/se/bottom.wav", "static"),
cursor = love.audio.newSource("res/se/cursor.wav", "static"),
cursor_lr = love.audio.newSource("res/se/cursor_lr.wav", "static"),
main_decide = love.audio.newSource("res/se/main_decide.wav", "static"),
mode_decide = love.audio.newSource("res/se/mode_decide.wav", "static"),
}
function playSE(sound, subsound)

View File

@@ -1,6 +1,7 @@
function love.load()
math.randomseed(os.time())
highscores = {}
require "load.rpc"
require "load.graphics"
require "load.fonts"
require "load.sounds"
@@ -14,6 +15,13 @@ function love.load()
love.window.setMode(love.graphics.getWidth(), love.graphics.getHeight(), {resizable = true});
if not config.gamesettings then config.gamesettings = {} end
for _, option in ipairs(GameConfigScene.options) do
if not config.gamesettings[option[1]] then
config.gamesettings[option[1]] = 1
end
end
if not config.input then
config.input = {}
scene = InputConfigScene()
@@ -64,7 +72,7 @@ end
function love.draw()
love.graphics.push()
-- get offset matrix
love.graphics.setDefaultFilter("linear", "nearest")
local width = love.graphics.getWidth()

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

BIN
res/se/cursor.wav Normal file

Binary file not shown.

BIN
res/se/cursor_lr.wav Normal file

Binary file not shown.

BIN
res/se/main_decide.wav Normal file

Binary file not shown.

BIN
res/se/mode_decide.wav Normal file

Binary file not shown.

View File

@@ -7,8 +7,9 @@ function Scene:update() end
function Scene:render() end
function Scene:onKeyPress() end
ExitScene = require "scene.exit"
GameScene = require "scene.game"
ModeSelectScene = require "scene.mode_select"
InputConfigScene = require "scene.input_config"
ConfigScene = require "scene.config"
GameConfigScene = require "scene.game_config"
TitleScene = require "scene.title"

23
scene/exit.lua Normal file
View File

@@ -0,0 +1,23 @@
local ExitScene = Scene:extend()
require 'load.save'
ExitScene.title = "Exit Game"
function ExitScene:new()
end
function ExitScene:update()
love.event.quit()
end
function ExitScene:render()
end
function ExitScene:changeOption(rel)
end
function ExitScene:onKeyPress(e)
end
return ExitScene

View File

@@ -5,6 +5,10 @@ function GameScene:new(game_mode, ruleset)
self.game = game_mode()
self.ruleset = ruleset()
self.game:initialize(self.ruleset)
DiscordRPC:update({
details = self.game.rpc_details,
state = self.game.name,
})
end
function GameScene:update()
@@ -57,11 +61,21 @@ end
function GameScene:onKeyPress(e)
if (self.game.completed) and
e.scancode == "return" and e.isRepeat == false then
(e.scancode == "return" or e.scancode == "escape") 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()
elseif (e.scancode == config.input.retry) then
-- fuck this, this is hacky but the way this codebase is setup prevents anything else
-- it seems like all the values that get touched in the child gamemode class
-- stop being linked to the values of the GameMode superclass because of how `mt.__index` works
-- not even sure this is the actual problem, but I don't want to have to rebuild everything about
-- the core organisation of everything. this hacky way will have to do until someone figures out something.
love.keypressed("escape", "escape", false)
love.keypressed("return", "return", false)
elseif e.scancode == "escape" then
scene = ModeSelectScene()
end
end

79
scene/game_config.lua Normal file
View File

@@ -0,0 +1,79 @@
local ConfigScene = Scene:extend()
ConfigScene.title = "Game Settings"
require 'load.save'
ConfigScene.options = {
-- this serves as reference to what the options' values mean i guess?
{"manlock", "Manual locking", {"Per ruleset","Per gamemode","Harddrop", "Softdrop"}},
{"piece_colour", "Piece Colours", {"Per ruleset", "Arika", "TTC"}},
{"world_reverse", "World Reverse", {"No", "Yes"}},
}
local optioncount = #ConfigScene.options
function ConfigScene:new()
-- load current config
self.config = config.input
self.highlight = 1
DiscordRPC:update({
details = "In menus",
state = "Changing game settings",
})
end
function ConfigScene:update()
end
function ConfigScene:render()
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(
backgrounds["game_config"],
0, 0, 0,
0.5, 0.5
)
love.graphics.setFont(font_3x5_4)
love.graphics.print("GAME SETTINGS", 80, 40)
love.graphics.setColor(1, 1, 1, 0.5)
love.graphics.rectangle("fill", 20, 98 + self.highlight * 20, 170, 22)
love.graphics.setFont(font_3x5_2)
for i, option in ipairs(ConfigScene.options) do
love.graphics.setColor(1, 1, 1, 1)
love.graphics.printf(option[2], 40, 100 + i * 20, 150, "left")
for j, setting in ipairs(option[3]) do
love.graphics.setColor(1, 1, 1, config.gamesettings[option[1]] == j and 1 or 0.5)
love.graphics.printf(setting, 100 + 110 * j, 100 + i * 20, 100, "center")
end
end
end
function ConfigScene:onKeyPress(e)
if e.scancode == "return" and e.isRepeat == false then
playSE("mode_decide")
saveConfig()
scene = TitleScene()
elseif (e.scancode == config.input["up"] or e.scancode == "up") and e.isRepeat == false then
playSE("cursor")
self.highlight = Mod1(self.highlight-1, optioncount)
elseif (e.scancode == config.input["down"] or e.scancode == "down") and e.isRepeat == false then
playSE("cursor")
self.highlight = Mod1(self.highlight+1, optioncount)
elseif (e.scancode == config.input["left"] or e.scancode == "left") and e.isRepeat == false then
playSE("cursor_lr")
local option = ConfigScene.options[self.highlight]
config.gamesettings[option[1]] = Mod1(config.gamesettings[option[1]]-1, #option[3])
elseif (e.scancode == config.input["right"] or e.scancode == "right") and e.isRepeat == false then
playSE("cursor_lr")
local option = ConfigScene.options[self.highlight]
config.gamesettings[option[1]] = Mod1(config.gamesettings[option[1]]+1, #option[3])
elseif e.scancode == "escape" then
loadSave()
scene = TitleScene()
end
end
return ConfigScene

View File

@@ -15,22 +15,35 @@ local configurable_inputs = {
"rotate_right2",
"rotate_180",
"hold",
"retry",
}
function ConfigScene:new()
-- load current config
self.config = config.input
self.input_state = 1
DiscordRPC:update({
details = "In menus",
state = "Changing input config",
})
end
function ConfigScene:update()
end
function ConfigScene:render()
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(
backgrounds["input_config"],
0, 0, 0,
0.5, 0.5
)
love.graphics.setFont(font_3x5_2)
for i, input in pairs(configurable_inputs) do
love.graphics.printf(input, 40, 50 + i * 20, 200, "left")
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"
@@ -54,8 +67,13 @@ function ConfigScene:onKeyPress(e)
self.input_state = 1
end
else
config.input[configurable_inputs[self.input_state]] = e.scancode
self.input_state = self.input_state + 1
if e.scancode == "escape" then
loadSave()
scene = TitleScene()
else
config.input[configurable_inputs[self.input_state]] = e.scancode
self.input_state = self.input_state + 1
end
end
end

31
scene/mode_select.lua Normal file → Executable file
View File

@@ -8,27 +8,34 @@ current_ruleset = 1
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.strategy',
--require 'tetris.modes.interval_training',
--require 'tetris.modes.pacer_test',
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_ax4',
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.big_a2',
require 'tetris.modes.konoha',
}
rulesets = {
require 'tetris.rulesets.cambridge',
require 'tetris.rulesets.arika',
require 'tetris.rulesets.arika_ti',
require 'tetris.rulesets.ti_srs',
require 'tetris.rulesets.arika_ace',
require 'tetris.rulesets.arika_ace2',
require 'tetris.rulesets.arika_srs',
require 'tetris.rulesets.standard_exp',
--require 'tetris.rulesets.bonkers',
--require 'tetris.rulesets.shirase',
@@ -41,6 +48,10 @@ function ModeSelectScene:new()
ruleset = current_ruleset,
select = "mode",
}
DiscordRPC:update({
details = "In menus",
state = "Choosing a mode",
})
end
function ModeSelectScene:update()
@@ -52,7 +63,7 @@ function ModeSelectScene:render()
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
@@ -66,7 +77,7 @@ function ModeSelectScene:render()
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)
@@ -86,15 +97,21 @@ function ModeSelectScene:onKeyPress(e)
current_ruleset = self.menu_state.ruleset
config.current_mode = current_mode
config.current_ruleset = current_ruleset
playSE("mode_decide")
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)
playSE("cursor")
elseif (e.scancode == config.input["down"] or e.scancode == "down") and e.isRepeat == false then
self:changeOption(1)
playSE("cursor")
elseif (e.scancode == config.input["left"] or e.scancode == "left") or
(e.scancode == config.input["right"] or e.scancode == "right") then
self:switchSelect()
playSE("cursor_lr")
elseif e.scancode == "escape" then
scene = TitleScene()
end
end

View File

@@ -3,10 +3,31 @@ local TitleScene = Scene:extend()
local main_menu_screens = {
ModeSelectScene,
InputConfigScene,
GameConfigScene,
ExitScene,
}
local mainmenuidle = {
"Idle",
"On title screen",
"On main menu screen",
"Twiddling their thumbs",
"Admiring the main menu's BG",
"Waiting for spring to come",
"Actually not playing",
"Contemplating collecting stars",
"Preparing to put the block!!",
"Having a nap",
"In menus",
"Bottom text",
}
function TitleScene:new()
self.main_menu_state = 1
DiscordRPC:update({
details = "In menus",
state = mainmenuidle[math.random(#mainmenuidle)],
})
end
function TitleScene:update()
@@ -38,11 +59,16 @@ end
function TitleScene:onKeyPress(e)
if e.scancode == "return" and e.isRepeat == false then
playSE("main_decide")
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)
playSE("cursor")
elseif (e.scancode == config.input["down"] or e.scancode == "down") and e.isRepeat == false then
self:changeOption(1)
playSE("cursor")
elseif e.scancode == "escape" and e.isRepeat == false then
love.event.quit()
end
end

View File

@@ -3,6 +3,7 @@ local Object = require 'libs.classic'
local Grid = Object:extend()
local empty = { skin = "", colour = "" }
local oob = { skin = "", colour = "" }
function Grid:new()
self.grid = {}
@@ -26,8 +27,15 @@ function Grid:clear()
end
end
function Grid:getCell(x, y)
if x < 1 or x > 10 or y > 24 then return oob
elseif y < 1 then return empty
else return self.grid[y][x]
end
end
function Grid:isOccupied(x, y)
return self.grid[y+1][x+1] ~= empty
return self:getCell(x+1, y+1) ~= empty
end
function Grid:isRowFull(row)
@@ -38,11 +46,32 @@ 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
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
if self:isOccupied(x, y) then
return false
end
end
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 (
self:isOccupied(x * 2 + 0, y * 2 + 0)
or self:isOccupied(x * 2 + 1, y * 2 + 0)
or self:isOccupied(x * 2 + 0, y * 2 + 1)
or self:isOccupied(x * 2 + 1, y * 2 + 1)
) then
return false
end
end
@@ -50,11 +79,16 @@ function Grid:canPlacePiece(piece)
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
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
if y < 4 or self:isOccupied(x, y) ~= empty then
return false
end
end
@@ -109,24 +143,91 @@ function Grid:copyBottomRow()
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"
colour = "X"
}
end
return true
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
y = piece.position.y + offset.y
self.grid[y+1][x+1] = {
skin = piece.skin,
colour = piece.shape
}
if y + 1 > 0 then
self.grid[y+1][x+1] = {
skin = piece.skin,
colour = piece.colour
}
end
end
end
function Grid:applyBigPiece(piece)
offsets = piece:getBlockOffsets()
for index, offset in pairs(offsets) do
x = piece.position.x + offset.x
y = piece.position.y + offset.y
for a = 1, 2 do
for b = 1, 2 do
if y*2+a > 0 then
self.grid[y*2+a][x*2+b] = {
skin = piece.skin,
colour = piece.colour
}
end
end
end
end
end
function Grid:checkForBravo(cleared_row_count)
for i = 0, 23 - cleared_row_count do
for j = 0, 9 do
if self:isOccupied(j, i) then return false end
end
end
return true
end
function Grid:checkSecretGrade()
local sgrade = 0
for i=23,5,-1 do
local validLine = true
local emptyCell = 0
if i > 13 then
emptyCell = 23-i
end
if i <= 13 then
emptyCell = i-5
end
for j=0,9 do
if (not self:isOccupied(j,i) and j ~= emptyCell) or (j == emptyCell and self:isOccupied(j,i)) then
validLine = false
end
end
if not self:isOccupied(emptyCell,i-1) then
validLine = false
end
if(validLine) then
sgrade = sgrade + 1
else
-- return sgrade
end
end
--[[
if(sgrade == 0) then return ""
elseif(sgrade < 10) then return 10-sgrade
elseif(sgrade < 19) then return "S"..(sgrade-9) end
return "GM"
--]]
return sgrade
end
function Grid:update()
for y = 1, 24 do
for x = 1, 10 do
@@ -148,19 +249,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, colour, big)
self.shape = shape
self.rotation = rotation
self.position = position
@@ -10,8 +10,10 @@ function Piece:new(shape, rotation, position, block_offsets, gravity, lock_delay
self.gravity = gravity
self.lock_delay = lock_delay
self.skin = skin
self.colour = colour
self.ghost = false
self.locked = false
self.big = big
end
-- Functions that return a new piece to test in rotation systems.
@@ -20,7 +22,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.colour, self.big
)
end
@@ -30,7 +32,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.colour, self.big
)
end
@@ -96,7 +98,7 @@ end
function Piece:dropToBottom(grid)
local piece_y = self.position.y
self:dropSquares(20, grid)
self:dropSquares(24, grid)
self.gravity = 0
if self.position.y > piece_y then
-- if it got dropped any, also reset lock delay
@@ -138,14 +140,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.colour],
64+x*32+partial_das*2, 16+y*32+gravity_offset*2,
0, 2, 2
)
else
love.graphics.draw(
blocks[self.skin][self.colour],
64+x*16+partial_das, 16+y*16+gravity_offset
)
end
end
return false
end

366
tetris/modes/big_a2.lua Executable file
View File

@@ -0,0 +1,366 @@
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 = "Big A2"
MarathonA2Game.hash = "BigA2"
MarathonA2Game.tagline = "The points don't matter! Can you reach the invisible roll? Big mode too!"
function MarathonA2Game:new()
self.super:new()
self.big_mode = true
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 27
elseif self.level < 800 then return 18
else return 14 end
end
function MarathonA2Game:getLineARE()
if self.level < 600 then return 27
elseif self.level < 700 then return 18
elseif self.level < 800 then return 14
else return 8 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)
cleared_row_count = cleared_row_count / 2
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)
if self.grid:checkForBravo(cleared_lines) then self.bravo = 4 else self.bravo = 1 end
cleared_lines = cleared_lines / 2
self:updateGrade(cleared_lines)
if cleared_lines > 0 then
self.score = self.score + (
(math.ceil((level + cleared_lines) / 4) + 2 * drop_bonus) *
cleared_lines * self.combo * self.bravo
)
self.lines = self.lines + cleared_lines
self.combo = self.combo + 2 * (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] > frameTime(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 > frameTime(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 .. " " ..
strTrueValues(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

@@ -11,9 +11,6 @@ DemonModeGame.name = "Demon Mode"
DemonModeGame.hash = "DemonMode"
DemonModeGame.tagline = "Can you handle the ludicrous speed past level 20?"
function DemonModeGame:new()
DemonModeGame.super:new()
self.roll_frames = 0
@@ -29,6 +26,9 @@ function DemonModeGame:new()
self.enable_hold = true
self.lock_drop = true
self.next_queue_length = 3
if math.random() < 1/6.66 then
self.rpc_details = "Suffering"
end
end
function DemonModeGame:getARE()
@@ -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
@@ -159,18 +159,25 @@ function DemonModeGame:updateSectionTimes(old_level, new_level)
end
elseif old_level < 100 then
-- If section time is under cutoff, skip to level 500.
if self.frames < sp(1,00) then
if self.frames < frameTime(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)
self.skip_failed = true
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
else
self.level = math.min(new_level, 2500)
if self.skip_failed and new_level >= 500 then
self.level = 500
self.game_over = true
end
end
else
self.level = math.min(new_level, 2500)
@@ -226,10 +233,10 @@ function DemonModeGame:drawScoringInfo()
love.graphics.print(
self.das.direction .. " " ..
self.das.frames .. " " ..
st(self.prev_inputs)
strTrueValues(self.prev_inputs)
)
love.graphics.printf("NEXT", 64, 40, 40, "left")
love.graphics.printf("GRADE", 240, 120, 40, "left")
if self.grade ~= 0 then love.graphics.printf("GRADE", 240, 120, 40, "left") end
love.graphics.printf("SCORE", 240, 200, 40, "left")
love.graphics.printf("LEVEL", 240, 320, 40, "left")

View File

@@ -36,11 +36,16 @@ 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
self.rpc_details = "In game"
-- variables related to configurable parameters
self.drop_locked = false
self.hard_drop_locked = false
self.lock_on_soft_drop = false
self.lock_on_hard_drop = false
self.hold_queue = nil
self.held = false
self.section_start_time = 0
@@ -48,13 +53,6 @@ function GameMode:new()
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
@@ -64,6 +62,7 @@ function GameMode:getLineClearDelay() return 40 end
function GameMode:getDasLimit() return 15 end
function GameMode:getNextPiece(ruleset)
return {
skin = "2tie",
shape = self.randomizer:nextPiece(),
@@ -73,9 +72,12 @@ end
function GameMode:initialize(ruleset)
-- generate next queue
self:new()
for i = 1, self.next_queue_length do
table.insert(self.next_queue, self:getNextPiece(ruleset))
end
self.lock_on_soft_drop = ({ruleset.softdrop_lock, self.instant_soft_drop, false, true })[config.gamesettings.manlock]
self.lock_on_hard_drop = ({ruleset.harddrop_lock, self.instant_hard_drop, true, false})[config.gamesettings.manlock]
end
function GameMode:update(inputs, ruleset)
@@ -119,7 +121,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
@@ -128,7 +131,7 @@ function GameMode:update(inputs, ruleset)
self.piece:isDropBlocked(self.grid) and
not self.hard_drop_locked then
self:onHardDrop(piece_dy)
if self.instant_hard_drop then
if self.lock_on_hard_drop then
self.piece.locked = true
end
end
@@ -137,7 +140,7 @@ function GameMode:update(inputs, ruleset)
self:onSoftDrop(piece_dy)
if self.piece:isDropBlocked(self.grid) and
not self.drop_locked and
self.instant_soft_drop
self.lock_on_soft_drop
then
self.piece.locked = true
end
@@ -145,18 +148,18 @@ 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()
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 +194,7 @@ end
-- event functions
function GameMode:whilePieceActive() end
function GameMode:onPieceLock(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
@@ -298,7 +301,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
@@ -343,11 +346,12 @@ function GameMode:drawGhostPiece(ruleset)
end
function GameMode:drawNextQueue(ruleset)
local colourscheme = ({ruleset.colourscheme, ColourSchemes.Arika, ColourSchemes.TTC})[config.gamesettings.piece_colour]
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)
local x = offset.x + ruleset.spawn_positions[piece].x
local y = offset.y + 4.7
love.graphics.draw(blocks[skin][colourscheme[piece]], pos_x+x*16, pos_y+y*16)
end
end
for i = 1, self.next_queue_length do
@@ -389,7 +393,7 @@ function GameMode:drawScoringInfo()
love.graphics.print(
self.das.direction .. " " ..
self.das.frames .. " " ..
st(self.prev_inputs) ..
strTrueValues(self.prev_inputs) ..
self.drop_bonus
)

View File

@@ -120,7 +120,7 @@ function IntervalTrainingGame:drawScoringInfo()
love.graphics.print(
self.das.direction .. " " ..
self.das.frames .. " " ..
st(self.prev_inputs)
strTrueValues(self.prev_inputs)
)
love.graphics.printf("NEXT", 64, 40, 40, "left")
love.graphics.printf("TIME LEFT", 240, 250, 80, "left")
@@ -134,7 +134,7 @@ function IntervalTrainingGame:drawScoringInfo()
-- 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 not self.game_over and not self.clear and time_left < frameTime(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")

190
tetris/modes/konoha.lua Executable file
View File

@@ -0,0 +1,190 @@
require 'funcs'
local GameMode = require 'tetris.modes.gamemode'
local Piece = require 'tetris.components.piece'
local KonohaRandomizer = require 'tetris.randomizers.bag_konoha'
local KonohaGame = GameMode:extend()
KonohaGame.name = "All Clear A4"
KonohaGame.hash = "AllClearA4"
KonohaGame.tagline = "Get as many bravos as you can under the time limit!"
function KonohaGame:new()
KonohaGame.super:new()
self.randomizer = KonohaRandomizer()
self.bravos = 0
self.last_bonus_amount = 0
self.last_bonus_display_time = 0
self.time_limit = 10800
self.big_mode = true
self.enable_hold = true
self.next_queue_length = 3
end
function KonohaGame:getARE()
if self.level < 300 then return 30
elseif self.level < 400 then return 25
elseif self.level < 500 then return 20
elseif self.level < 600 then return 17
elseif self.level < 800 then return 15
elseif self.level < 900 then return 13
elseif self.level < 1000 then return 10
elseif self.level < 1300 then return 8
else return 6 end
end
function KonohaGame:getLineARE()
return self:getARE()
end
function KonohaGame:getDasLimit()
if self.level < 500 then return 10
elseif self.level < 800 then return 9
elseif self.level < 1000 then return 8
else return 7 end
end
function KonohaGame:getLineClearDelay()
if self.level < 200 then return 14
elseif self.level < 500 then return 9
elseif self.level < 800 then return 8
elseif self.level < 1000 then return 7
else return 6 end
end
function KonohaGame:getLockDelay()
if self.level < 500 then return 30
elseif self.level < 600 then return 25
elseif self.level < 700 then return 23
elseif self.level < 800 then return 20
elseif self.level < 900 then return 17
elseif self.level < 1000 then return 15
elseif self.level < 1200 then return 13
elseif self.level < 1300 then return 10
else return 8 end
end
function KonohaGame:getGravity()
if (self.level < 30) then return 4/256
elseif (self.level < 35) then return 8/256
elseif (self.level < 40) then return 12/256
elseif (self.level < 50) then return 16/256
elseif (self.level < 60) then return 32/256
elseif (self.level < 70) then return 48/256
elseif (self.level < 80) then return 64/256
elseif (self.level < 90) then return 128/256
elseif (self.level < 100) then return 192/256
elseif (self.level < 120) then return 1
elseif (self.level < 140) then return 2
elseif (self.level < 160) then return 3
elseif (self.level < 170) then return 4
elseif (self.level < 200) then return 5
else return 20 end
end
function KonohaGame:getSection()
return math.floor(level / 100) + 1
end
function KonohaGame:getSectionEndLevel()
return math.floor(self.level / 100 + 1) * 100
end
function KonohaGame:advanceOneFrame()
if self.ready_frames == 0 then
self.time_limit = self.time_limit - 1
self.frames = self.frames + 1
end
if self.time_limit <= 0 then
self.game_over = true
end
self.last_bonus_display_time = self.last_bonus_display_time - 1
end
function KonohaGame:onPieceEnter()
if (self.level % 100 ~= 99) and self.frames ~= 0 then
self.level = self.level + 1
end
end
function KonohaGame:drawGrid(ruleset)
self.grid:draw()
if self.piece ~= nil and self.level < 100 then
self:drawGhostPiece(ruleset)
end
end
local cleared_row_levels = {2, 4, 6, 12}
local bravo_bonus = {300, 480, 660, 900}
local non_bravo_bonus = {0, 0, 20, 40}
local bravo_ot_bonus = {0, 60, 120, 180}
function KonohaGame:onLineClear(cleared_row_count)
local oldtime = self.time_limit
self.level = self.level + cleared_row_levels[cleared_row_count / 2]
if self.grid:checkForBravo(cleared_row_count) then
self.bravos = self.bravos + 1
if self.level < 1000 then self.time_limit = self.time_limit + bravo_bonus[cleared_row_count / 2]
else self.time_limit = self.time_limit + bravo_ot_bonus[cleared_row_count / 2]
end
if self.bravos == 11 then self.randomizer.allowrepeat = true end
elseif self.level < 1000 then
self.time_limit = self.time_limit + non_bravo_bonus[cleared_row_count / 2]
end
local bonus = self.time_limit - oldtime
if bonus > 0 then
self.last_bonus_amount = bonus
self.last_bonus_display_time = 120
end
end
function KonohaGame:getBackground()
return math.floor(self.level / 100)
end
function KonohaGame:drawScoringInfo()
love.graphics.setColor(1, 1, 1, 1)
love.graphics.setFont(font_3x5_2)
love.graphics.print(
self.das.direction .. " " ..
self.das.frames .. " " ..
strTrueValues(self.prev_inputs)
)
love.graphics.printf("NEXT", 64, 40, 40, "left")
love.graphics.printf("TIME LIMIT", 240, 120, 120, "left")
love.graphics.printf("BRAVOS", 240, 200, 50, "left")
love.graphics.printf("LEVEL", 240, 320, 40, "left")
love.graphics.setFont(font_3x5_3)
if not self.game_over and self.time_limit < frameTime(0,10) and self.time_limit % 4 < 2 then
love.graphics.setColor(1, 0.3, 0.3, 1)
end
love.graphics.printf(formatTime(self.time_limit), 240, 140, 120, "right")
love.graphics.setColor(1, 1, 1, 1)
if self.last_bonus_display_time > 0 then
love.graphics.printf("+"..formatTime(self.last_bonus_amount), 240, 160, 120, "right")
end
love.graphics.printf(self.bravos, 240, 220, 90, "left")
love.graphics.printf(self.level, 240, 340, 50, "right")
love.graphics.printf(self:getSectionEndLevel(), 240, 370, 50, "right")
love.graphics.setFont(font_8x11)
love.graphics.printf(formatTime(self.frames), 64, 420, 160, "center")
end
function KonohaGame:getHighscoreData()
return {
bravos = self.bravos,
level = self.level,
frames = self.frames,
}
end
return KonohaGame

View File

@@ -151,11 +151,11 @@ function Marathon2020Game:advanceOneFrame()
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)
frameTime(0,45,00), frameTime(0,41,50), frameTime(0,38,50), frameTime(0,35,00), frameTime(0,32,50),
frameTime(0,29,20), frameTime(0,27,20), frameTime(0,24,80), frameTime(0,22,80), frameTime(0,20,60),
frameTime(0,19,60), frameTime(0,19,40), frameTime(0,19,40), frameTime(0,18,40), frameTime(0,18,20),
frameTime(0,16,20), frameTime(0,16,20), frameTime(0,16,20), frameTime(0,16,20), frameTime(0,16,20),
frameTime(0,15,20)
}
local levels_for_cleared_rows = { 1, 2, 4, 6 }
@@ -284,11 +284,11 @@ function Marathon2020Game:sectionPassed(old_level, new_level)
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
if section == 5 and self.frames < frameTime(6,00,00) then self.torikan_passed[500] = true end
if section == 9 and self.frames < frameTime(8,30,00) then self.torikan_passed[900] = true end
if section == 10 and self.frames < frameTime(8,45,00) then self.torikan_passed[1000] = true end
if section == 15 and self.frames < frameTime(11,30,00) then self.torikan_passed[1500] = true end
if section == 19 and self.frames < frameTime(13,15,00) then self.torikan_passed[1900] = true end
end
function Marathon2020Game:checkClear(level)

View File

@@ -18,11 +18,17 @@ function MarathonA1Game:new()
self.roll_frames = 0
self.combo = 1
self.bravos = 0
self.gm_conditions = {
level300 = false,
level500 = false,
level999 = false
}
self.SGnames = {
"9", "8", "7", "6", "5", "4", "3", "2", "1",
"S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9",
"GM"
}
self.randomizer = History4RollsRandomizer()
@@ -140,11 +146,15 @@ function MarathonA1Game:onLineClear(cleared_row_count)
end
function MarathonA1Game:updateScore(level, drop_bonus, cleared_lines)
if self.grid:checkForBravo(cleared_lines) then
self.bravo = 4
self.bravos = self.bravos + 1
else self.bravo = 1 end
if cleared_lines > 0 then
self.combo = self.combo + (cleared_lines - 1) * 2
self.score = self.score + (
(math.ceil((level + cleared_lines) / 4) + drop_bonus) *
cleared_lines * self.combo
cleared_lines * self.combo * self.bravo
)
self.lines = self.lines + cleared_lines
else
@@ -155,15 +165,15 @@ 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
if self.score > 12000 and self.frames <= frameTime(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
if self.score > 40000 and self.frames <= frameTime(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
if self.score > 126000 and self.frames <= frameTime(13,30) then
self.gm_conditions["level900"] = true
end
end
@@ -184,13 +194,19 @@ function MarathonA1Game:drawScoringInfo()
love.graphics.print(
self.das.direction .. " " ..
self.das.frames .. " " ..
st(self.prev_inputs)
strTrueValues(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")
local sg = self.grid:checkSecretGrade()
if sg >= 5 then
love.graphics.printf("SECRET GRADE", 240, 430, 180, "left")
end
if self.bravos > 0 then love.graphics.printf("BRAVO", 300, 120, 40, "left") end
love.graphics.setFont(font_3x5_3)
love.graphics.printf(self.score, 240, 220, 90, "left")
@@ -202,7 +218,11 @@ function MarathonA1Game:drawScoringInfo()
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")
if sg >= 5 then
love.graphics.printf(self.SGnames[sg], 240, 450, 180, "left")
end
if self.bravos > 0 then love.graphics.printf(self.bravos, 300, 140, 40, "left") end
love.graphics.setFont(font_8x11)
love.graphics.printf(formatTime(self.frames), 64, 420, 160, "center")
end

View File

@@ -27,6 +27,12 @@ function MarathonA2Game:new()
self.section_times = { [0] = 0 }
self.section_tetrises = { [0] = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
self.SGnames = {
"9", "8", "7", "6", "5", "4", "3", "2", "1",
"S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9",
"GM"
}
self.randomizer = History6RollsRandomizer()
self.lock_drop = false
@@ -136,13 +142,14 @@ end
function MarathonA2Game:updateScore(level, drop_bonus, cleared_lines)
self:updateGrade(cleared_lines)
if self.grid:checkForBravo(cleared_lines) then self.bravo = 4 else self.bravo = 1 end
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)
(math.ceil((level + cleared_lines) / 4) + 2 * drop_bonus) *
cleared_lines * self.combo * self.bravo
)
self.lines = self.lines + cleared_lines
self.combo = self.combo + cleared_lines - 1
self.combo = self.combo + (cleared_lines - 1) * 2
else
self.drop_bonus = 0
self.combo = 1
@@ -260,7 +267,7 @@ function MarathonA2Game:qualifiesForMRoll()
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
if self.section_times[section] > frameTime(1,05) then
return false
end
end
@@ -273,7 +280,7 @@ function MarathonA2Game:qualifiesForMRoll()
return false
end
end
if self.grade < 17 or self.frames > sp(8,45) then
if self.grade < 17 or self.frames > frameTime(9,30) then
return false
end
return true
@@ -325,18 +332,25 @@ function MarathonA2Game:drawScoringInfo()
love.graphics.print(
self.das.direction .. " " ..
self.das.frames .. " " ..
st(self.prev_inputs)
strTrueValues(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")
local sg = self.grid:checkSecretGrade()
if sg >= 5 then
love.graphics.printf("SECRET GRADE", 240, 430, 180, "left")
end
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")
if sg >= 5 then
love.graphics.printf(self.SGnames[sg], 240, 450, 180, "left")
end
love.graphics.setFont(font_8x11)
love.graphics.printf(formatTime(self.frames), 64, 420, 160, "center")

View File

@@ -32,10 +32,19 @@ function MarathonA3Game:new()
self.randomizer = History6RollsRandomizer()
self.SGnames = {
"9", "8", "7", "6", "5", "4", "3", "2", "1",
"S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9",
"GM"
}
self.lock_drop = true
self.lock_hard_drop = true
self.enable_hold = true
self.next_queue_length = 3
self.coolregret_message = "COOL!!"
self.coolregret_timer = 0
end
function MarathonA3Game:getARE()
@@ -67,7 +76,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 +128,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
@@ -158,13 +169,13 @@ function MarathonA3Game:onLineClear(cleared_row_count)
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),
frameTime(0,52), frameTime(0,52), frameTime(0,49), frameTime(0,45), frameTime(0,45),
frameTime(0,42), frameTime(0,42), frameTime(0,38), frameTime(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),
frameTime(0,90), frameTime(0,75), frameTime(0,75), frameTime(0,68), frameTime(0,60),
frameTime(0,60), frameTime(0,50), frameTime(0,50), frameTime(0,50), frameTime(0,50),
}
function MarathonA3Game:updateSectionTimes(old_level, new_level)
@@ -180,17 +191,23 @@ function MarathonA3Game:updateSectionTimes(old_level, new_level)
if section_time > regret_cutoffs[section] then
self.section_cool_grade = self.section_cool_grade - 1
table.insert(self.section_status, "regret")
self.coolregret_message = "REGRET!!"
self.coolregret_timer = 300
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")
self.coolregret_message = "COOL!!"
self.coolregret_timer = 300
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")
self.coolregret_message = "COOL!!"
self.coolregret_timer = 300
else
table.insert(self.section_status, "none")
end
@@ -370,12 +387,16 @@ function MarathonA3Game:drawScoringInfo()
love.graphics.print(
self.das.direction .. " " ..
self.das.frames .. " " ..
st(self.prev_inputs)
strTrueValues(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")
local sg = self.grid:checkSecretGrade()
if sg >= 5 then
love.graphics.printf("SECRET GRADE", 240, 430, 180, "left")
end
-- draw section time data
current_section = math.floor(self.level / 100) + 1
@@ -403,13 +424,21 @@ function MarathonA3Game:drawScoringInfo()
end
love.graphics.printf(formatTime(self.frames - self.section_start_time), current_x, 40 + 20 * current_section, 90, "left")
if(self.coolregret_timer > 0) then
love.graphics.printf(self.coolregret_message, 64, 400, 160, "center")
self.coolregret_timer = self.coolregret_timer - 1
end
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")
if sg >= 5 then
love.graphics.printf(self.SGnames[sg], 240, 450, 180, "left")
end
love.graphics.setFont(font_8x11)
love.graphics.printf(formatTime(self.frames), 64, 420, 160, "center")
end

View File

@@ -3,22 +3,20 @@ require 'funcs'
local GameMode = require 'tetris.modes.gamemode'
local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local Bag7NoSZOStartRandomizer = require 'tetris.randomizers.bag7noSZOstart'
local MarathonL1Game = GameMode:extend()
local MarathonAX4Game = GameMode:extend()
MarathonL1Game.name = "Line Attack"
MarathonL1Game.hash = "MarathonL1"
MarathonL1Game.tagline = "Can you clear the time hurdles when the game goes this fast?"
MarathonAX4Game.name = "Marathon AX4"
MarathonAX4Game.hash = "MarathonAX4"
MarathonAX4Game.tagline = "Can you clear the time hurdles when the game goes this fast?"
function MarathonL1Game:new()
MarathonL1Game.super:new()
function MarathonAX4Game:new()
MarathonAX4Game.super:new()
self.roll_frames = 0
self.randomizer = History6RollsRandomizer()
self.randomizer = Bag7NoSZOStartRandomizer()
self.section_time_limit = 3600
self.section_start_time = 0
@@ -30,7 +28,7 @@ function MarathonL1Game:new()
self.next_queue_length = 3
end
function MarathonL1Game:getARE()
function MarathonAX4Game:getARE()
if self.lines < 10 then return 18
elseif self.lines < 40 then return 14
elseif self.lines < 60 then return 12
@@ -40,24 +38,24 @@ function MarathonL1Game:getARE()
else return 6 end
end
function MarathonL1Game:getLineARE()
function MarathonAX4Game:getLineARE()
return self:getARE()
end
function MarathonL1Game:getDasLimit()
function MarathonAX4Game: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()
function MarathonAX4Game:getLineClearDelay()
if self.lines < 10 then return 14
elseif self.lines < 30 then return 9
else return 5 end
end
function MarathonL1Game:getLockDelay()
function MarathonAX4Game:getLockDelay()
if self.lines < 10 then return 28
elseif self.lines < 20 then return 24
elseif self.lines < 30 then return 22
@@ -67,15 +65,15 @@ function MarathonL1Game:getLockDelay()
else return 13 end
end
function MarathonL1Game:getGravity()
function MarathonAX4Game:getGravity()
return 20
end
function MarathonL1Game:getSection()
function MarathonAX4Game:getSection()
return math.floor(level / 100) + 1
end
function MarathonL1Game:advanceOneFrame()
function MarathonAX4Game:advanceOneFrame()
if self.clear then
self.roll_frames = self.roll_frames + 1
if self.roll_frames < 0 then
@@ -94,7 +92,7 @@ function MarathonL1Game:advanceOneFrame()
return true
end
function MarathonL1Game:onLineClear(cleared_row_count)
function MarathonAX4Game: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 +104,11 @@ function MarathonL1Game:onLineClear(cleared_row_count)
end
end
function MarathonL1Game:getSectionTime()
function MarathonAX4Game:getSectionTime()
return self.frames - self.section_start_time
end
function MarathonL1Game:updateSectionTimes(old_lines, new_lines)
function MarathonAX4Game:updateSectionTimes(old_lines, new_lines)
if math.floor(old_lines / 10) < math.floor(new_lines / 10) then
-- record new section
table.insert(self.section_times, self:getSectionTime())
@@ -119,23 +117,23 @@ function MarathonL1Game:updateSectionTimes(old_lines, new_lines)
end
end
function MarathonL1Game:onPieceEnter()
function MarathonAX4Game:onPieceEnter()
self.section_clear = false
end
function MarathonL1Game:drawGrid(ruleset)
function MarathonAX4Game:drawGrid(ruleset)
self.grid:draw()
end
function MarathonL1Game:getHighscoreData()
function MarathonAX4Game:getHighscoreData()
return {
lines = self.lines,
frames = self.frames,
}
end
function MarathonL1Game:drawScoringInfo()
MarathonL1Game.super.drawScoringInfo(self)
function MarathonAX4Game:drawScoringInfo()
MarathonAX4Game.super.drawScoringInfo(self)
love.graphics.setColor(1, 1, 1, 1)
@@ -143,7 +141,7 @@ function MarathonL1Game:drawScoringInfo()
love.graphics.print(
self.das.direction .. " " ..
self.das.frames .. " " ..
st(self.prev_inputs)
strTrueValues(self.prev_inputs)
)
love.graphics.printf("NEXT", 64, 40, 40, "left")
love.graphics.printf("TIME LEFT", 240, 250, 80, "left")
@@ -158,19 +156,19 @@ function MarathonL1Game:drawScoringInfo()
-- 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 not self.game_over and not self.clear and time_left < frameTime(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()
function MarathonAX4Game:getSectionEndLines()
return math.floor(self.lines / 10 + 1) * 10
end
function MarathonL1Game:getBackground()
function MarathonAX4Game:getBackground()
return math.floor(self.lines / 10)
end
return MarathonL1Game
return MarathonAX4Game

View File

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

@@ -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,14 +11,17 @@ 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()
self.lock_drop = true
self.next_queue_length = 1
self.SGnames = {
"9", "8", "7", "6", "5", "4", "3", "2", "1",
"S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9",
"GM"
}
self.roll_frames = 0
self.combo = 1
@@ -64,15 +67,15 @@ function PhantomManiaGame:getGravity()
end
function PhantomManiaGame:hitTorikan(old_level, new_level)
if old_level < 300 and new_level >= 300 and self.frames > sp(2,28) then
if old_level < 300 and new_level >= 300 and self.frames > frameTime(2,28) then
self.level = 300
return true
end
if old_level < 500 and new_level >= 500 and self.frames > sp(3,38) then
if old_level < 500 and new_level >= 500 and self.frames > frameTime(3,38) then
self.level = 500
return true
end
if old_level < 800 and new_level >= 800 and self.frames > sp(5,23) then
if old_level < 800 and new_level >= 800 and self.frames > frameTime(5,23) then
self.level = 800
return true
end
@@ -102,7 +105,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
@@ -164,12 +167,16 @@ function PhantomManiaGame:drawScoringInfo()
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")
if getLetterGrade(self.level, self.clear) ~= "" then love.graphics.printf("GRADE", text_x, 120, 40, "left") end
love.graphics.printf("SCORE", text_x, 200, 40, "left")
love.graphics.printf("LEVEL", text_x, 320, 40, "left")
local sg = self.grid:checkSecretGrade()
if sg >= 5 then
love.graphics.printf("SECRET GRADE", 240, 430, 180, "left")
end
love.graphics.setFont(font_3x5_3)
love.graphics.printf(getLetterGrade(self.level, self.clear), text_x, 140, 90, "left")
if getLetterGrade(self.level, self.clear) ~= "" then love.graphics.printf(getLetterGrade(self.level, self.clear), text_x, 140, 90, "left") end
love.graphics.printf(self.score, text_x, 220, 90, "left")
love.graphics.printf(self.level, text_x, 340, 40, "right")
if self.clear then
@@ -178,6 +185,9 @@ function PhantomManiaGame:drawScoringInfo()
love.graphics.printf(self:getSectionEndLevel(), text_x, 370, 40, "right")
end
if sg >= 5 then
love.graphics.printf(self.SGnames[sg], 240, 450, 180, "left")
end
end
function PhantomManiaGame:getSectionEndLevel()

View File

@@ -25,6 +25,14 @@ function PhantomMania2Game:new()
self.combo = 1
self.hold_age = 0
self.queue_age = 0
self.roll_points = 0
self.SGnames = {
"S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9",
"m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8", "m9",
"GM"
}
self.randomizer = History6RollsRandomizer()
self.lock_drop = true
@@ -84,19 +92,19 @@ function PhantomMania2Game:getNextPiece(ruleset)
end
function PhantomMania2Game:hitTorikan(old_level, new_level)
if old_level < 300 and new_level >= 300 and self.frames > sp(2,02) then
if old_level < 300 and new_level >= 300 and self.frames > frameTime(2,02) then
self.level = 300
return true
end
if old_level < 500 and new_level >= 500 and self.frames > sp(3,03) then
if old_level < 500 and new_level >= 500 and self.frames > frameTime(3,03) then
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 > frameTime(4,45) then
self.level = 800
return true
end
if old_level < 1000 and new_level >= 1000 and self.frames > sp(5,38) then
if old_level < 1000 and new_level >= 1000 and self.frames > frameTime(5,38) then
self.level = 1000
return true
end
@@ -135,7 +143,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 +152,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 +161,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 / 2]
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()
@@ -179,15 +194,15 @@ 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),
frameTime(0,36), frameTime(0,36), frameTime(0,36), frameTime(0,36), frameTime(0,36),
frameTime(0,30), frameTime(0,30), frameTime(0,30), frameTime(0,30), frameTime(0,30),
frameTime(0,27), frameTime(0,27), frameTime(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),
frameTime(0,50), frameTime(0,50), frameTime(0,50), frameTime(0,50), frameTime(0,50),
frameTime(0,40), frameTime(0,40), frameTime(0,40), frameTime(0,40), frameTime(0,40),
frameTime(0,35), frameTime(0,35), frameTime(0,35),
}
function PhantomMania2Game:updateSectionTimes(old_level, new_level)
@@ -225,7 +240,7 @@ PhantomMania2Game.garbageOpacityFunction = function(age)
end
function PhantomMania2Game:drawGrid()
if not (self.game_over or self.clear) then
if not (self.game_over or (self.clear and self.level < 1300)) then
self.grid:drawInvisible(self.rollOpacityFunction, self.garbageOpacityFunction)
else
self.grid:draw()
@@ -243,7 +258,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 +273,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)
@@ -276,6 +291,10 @@ function PhantomMania2Game:drawScoringInfo()
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")
local sg = self.grid:checkSecretGrade()
if sg >= 5 then
love.graphics.printf("SECRET GRADE", 240, 430, 180, "left")
end
love.graphics.setFont(font_3x5_3)
love.graphics.printf(getLetterGrade(math.floor(self.grade)), text_x, 140, 90, "left")
@@ -286,6 +305,10 @@ function PhantomMania2Game:drawScoringInfo()
else
love.graphics.printf(math.floor(self.level / 100 + 1) * 100, text_x, 370, 50, "right")
end
if sg >= 5 then
love.graphics.printf(self.SGnames[sg], 240, 450, 180, "left")
end
end
function PhantomMania2Game:getBackground()

View File

@@ -1,200 +1,22 @@
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()
self.SGnames = {
"M1", "M2", "M3", "M4", "M5", "M6", "M7", "M8", "M9",
"M10", "M11", "M12", "M13", "M14", "M15", "M16", "M17", "M18",
"GM"
}
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

@@ -3,24 +3,22 @@ require 'funcs'
local GameMode = require 'tetris.modes.gamemode'
local Piece = require 'tetris.components.piece'
local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'
local Bag7Randomiser = require 'tetris.randomizers.bag7noSZOstart'
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
self.pieces = 0
self.randomizer = History6RollsRandomizer()
self.randomizer = Bag7Randomiser()
self.roll_frames = 0
@@ -32,39 +30,39 @@ function LigneGame:new()
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()
function Race40Game:getARE()
return 0
end
function LigneGame:getLineARE()
function Race40Game:getLineARE()
return self:getARE()
end
function LigneGame:getDasLimit()
function Race40Game:getDasLimit()
return 6
end
function LigneGame:getLineClearDelay()
function Race40Game:getLineClearDelay()
return 0
end
function LigneGame:getLockDelay()
function Race40Game:getLockDelay()
return 15
end
function LigneGame:getGravity()
function Race40Game:getGravity()
return 1/64
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

@@ -102,15 +102,15 @@ function Survival2020Game:getNextPiece(ruleset)
end
function Survival2020Game:hitTorikan(old_level, new_level)
if old_level < 500 and new_level >= 500 and self.frames > sp(3,00) then
if old_level < 500 and new_level >= 500 and self.frames > frameTime(3,00) then
self.level = 500
return true
end
if old_level < 1000 and new_level >= 1000 and self.frames > sp(5,00) then
if old_level < 1000 and new_level >= 1000 and self.frames > frameTime(5,00) then
self.level = 1000
return true
end
if old_level < 1500 and new_level >= 1500 and self.frames > sp(7,00) then
if old_level < 1500 and new_level >= 1500 and self.frames > frameTime(7,00) then
self.level = 1500
return true
end
@@ -193,7 +193,7 @@ function Survival2020Game:updateSectionTimes(old_level, new_level)
section_time = self.frames - self.section_start_time
table.insert(self.section_times, section_time)
self.section_start_time = self.frames
if section_time <= sp(0,30) then
if section_time <= frameTime(0,30) then
self.grade = self.grade + 2
else
self.grade = self.grade + 1

View File

@@ -19,12 +19,20 @@ function SurvivalA1Game:new()
self.roll_frames = 0
self.combo = 1
self.bravos = 0
self.gm_conditions = {
level300 = false,
level500 = false,
level999 = false
}
self.SGnames = {
"9", "8", "7", "6", "5", "4", "3", "2", "1",
"S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9",
"GM"
}
self.randomizer = History4RollsRandomizer()
self.lock_drop = false
@@ -110,10 +118,14 @@ function SurvivalA1Game:onLineClear(cleared_row_count)
end
function SurvivalA1Game:updateScore(level, drop_bonus, cleared_lines)
if self.grid:checkForBravo(cleared_lines) then
self.bravo = 4
self.bravos = self.bravos + 1
else self.bravo = 1 end
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
cleared_lines * self.bravo * self.combo
)
self.lines = self.lines + cleared_lines
self.combo = self.combo + (cleared_lines - 1) * 2
@@ -125,15 +137,15 @@ end
function SurvivalA1Game: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
if self.score > 12000 and self.frames <= frameTime(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
if self.score > 40000 and self.frames <= frameTime(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
if self.score > 126000 and self.frames <= frameTime(13,30) then
self.gm_conditions["level900"] = true
end
end
@@ -151,13 +163,19 @@ function SurvivalA1Game:drawScoringInfo()
love.graphics.print(
self.das.direction .. " " ..
self.das.frames .. " " ..
st(self.prev_inputs)
strTrueValues(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")
local sg = self.grid:checkSecretGrade()
if sg >= 5 then
love.graphics.printf("SECRET GRADE", 240, 430, 180, "left")
end
if self.bravos > 0 then love.graphics.printf("BRAVO", 300, 120, 40, "left") end
love.graphics.setFont(font_3x5_3)
love.graphics.printf(self.score, 240, 220, 90, "left")
@@ -169,6 +187,10 @@ function SurvivalA1Game:drawScoringInfo()
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")
if sg >= 5 then
love.graphics.printf(self.SGnames[sg], 240, 450, 180, "left")
end
if self.bravos > 0 then love.graphics.printf(self.bravos, 300, 140, 40, "left") end
love.graphics.setFont(font_8x11)
love.graphics.printf(formatTime(self.frames), 64, 420, 160, "center")

View File

@@ -19,6 +19,12 @@ function SurvivalA2Game:new()
self.roll_frames = 0
self.combo = 1
self.randomizer = History6RollsRandomizer()
self.SGnames = {
"9", "8", "7", "6", "5", "4", "3", "2", "1",
"S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9",
"GM"
}
self.lock_drop = true
end
@@ -62,7 +68,7 @@ function SurvivalA2Game:getGravity()
end
function SurvivalA2Game:hitTorikan(old_level, new_level)
if old_level < 500 and new_level >= 500 and self.frames > sp(3,25) then
if old_level < 500 and new_level >= 500 and self.frames > frameTime(3,25) then
self.level = 500
return true
end
@@ -99,10 +105,11 @@ function SurvivalA2Game:onLineClear(cleared_row_count)
end
function SurvivalA2Game:updateScore(level, drop_bonus, cleared_lines)
if self.grid:checkForBravo(cleared_lines) then self.bravo = 4 else self.bravo = 1 end
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
cleared_lines * self.bravo * self.combo
)
self.lines = self.lines + cleared_lines
self.combo = self.combo + (cleared_lines - 1) * 2
@@ -132,18 +139,25 @@ function SurvivalA2Game:drawScoringInfo()
love.graphics.print(
self.das.direction .. " " ..
self.das.frames .. " " ..
st(self.prev_inputs)
strTrueValues(self.prev_inputs)
)
love.graphics.printf("NEXT", 64, 40, 40, "left")
love.graphics.printf("GRADE", text_x, 120, 40, "left")
if self:getLetterGrade() ~= "" then love.graphics.printf("GRADE", text_x, 120, 40, "left") end
love.graphics.printf("SCORE", text_x, 200, 40, "left")
love.graphics.printf("LEVEL", text_x, 320, 40, "left")
local sg = self.grid:checkSecretGrade()
if sg >= 5 then
love.graphics.printf("SECRET GRADE", 240, 430, 180, "left")
end
love.graphics.setFont(font_3x5_3)
love.graphics.printf(self.score, text_x, 220, 90, "left")
love.graphics.printf(self:getLetterGrade(), text_x, 140, 90, "left")
if self:getLetterGrade() ~= "" then love.graphics.printf(self:getLetterGrade(), text_x, 140, 90, "left") end
love.graphics.printf(self.level, text_x, 340, 40, "right")
love.graphics.printf(self:getSectionEndLevel(), text_x, 370, 40, "right")
if sg >= 5 then
love.graphics.printf(self.SGnames[sg], 240, 450, 180, "left")
end
end
function SurvivalA2Game:getSectionEndLevel()

View File

@@ -24,10 +24,27 @@ function SurvivalA3Game:new()
self.roll_frames = 0
self.combo = 1
self.randomizer = History6RollsRandomizer()
self.SGnames = {
"S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9",
"m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8", "m9",
"GM"
}
self.lock_drop = true
self.enable_hold = true
self.next_queue_length = 3
self.coolregret_message = "COOL!!"
self.coolregret_timer = 0
end
function SurvivalA3Game:initialize(ruleset)
self.torikan_time = frameTime(2,28)
if ruleset.world then self.torikan_time = frameTime(3,03) end
self.super.initialize(self, ruleset)
-- ^ notice the . here instead of the :
end
function SurvivalA3Game:getARE()
@@ -44,13 +61,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 +78,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()
@@ -84,11 +103,11 @@ function SurvivalA3Game:getNextPiece(ruleset)
end
function SurvivalA3Game:hitTorikan(old_level, new_level)
if old_level < 500 and new_level >= 500 and self.frames > sp(2,28) then
if old_level < 500 and new_level >= 500 and self.frames > self.torikan_time then
self.level = 500
return true
end
if old_level < 1000 and new_level >= 1000 and self.frames > sp(4,56) then
if old_level < 1000 and new_level >= 1000 and self.frames > self.torikan_time*2 then
self.level = 1000
return true
end
@@ -133,6 +152,7 @@ function SurvivalA3Game:onLineClear(cleared_row_count)
end
self.clear = true
self.grid:clear()
self.big_mode = true
self.roll_frames = -150
else
self.level = math.min(new_level, 1300)
@@ -141,8 +161,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)
@@ -165,8 +185,11 @@ function SurvivalA3Game:updateSectionTimes(old_level, new_level)
section_time = self.frames - self.section_start_time
table.insert(self.section_times, section_time)
self.section_start_time = self.frames
if section_time <= sp(1,00) then
if section_time <= frameTime(1,00) then
self.grade = self.grade + 1
else
self.coolregret_message = "REGRET!!"
self.coolregret_timer = 300
end
end
end
@@ -188,10 +211,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
@@ -206,6 +227,15 @@ function SurvivalA3Game:drawScoringInfo()
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")
local sg = self.grid:checkSecretGrade()
if sg >= 5 then
love.graphics.printf("SECRET GRADE", 240, 430, 180, "left")
end
if(self.coolregret_timer > 0) then
love.graphics.printf(self.coolregret_message, 64, 400, 160, "center")
self.coolregret_timer = self.coolregret_timer - 1
end
local current_section = math.floor(self.level / 100) + 1
self:drawSectionTimesWithSplits(current_section)
@@ -219,6 +249,9 @@ function SurvivalA3Game:drawScoringInfo()
else
love.graphics.printf(math.floor(self.level / 100 + 1) * 100, text_x, 370, 50, "right")
end
if sg >= 5 then
love.graphics.printf(self.SGnames[sg], 240, 450, 180, "left")
end
end
function SurvivalA3Game:getBackground()

View File

@@ -0,0 +1,9 @@
local Randomizer = require 'tetris.randomizers.randomizer'
local AlwaysRandomizer = Randomizer:extend()
function AlwaysRandomizer:generatePiece()
return "I"
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

@@ -0,0 +1,17 @@
local Randomizer = require 'tetris.randomizers.randomizer'
local Bag5Randomizer = Randomizer:extend()
function Bag5Randomizer:initialize()
self.bag = {"I", "J", "L", "O", "T"}
end
function Bag5Randomizer:generatePiece()
if next(self.bag) == nil then
self.bag = {"I", "J", "L", "O", "T"}
end
local x = math.random(table.getn(self.bag))
return table.remove(self.bag, x)
end
return Bag5Randomizer

24
tetris/randomizers/bag5alt.lua Executable file
View File

@@ -0,0 +1,24 @@
local Randomizer = require 'tetris.randomizers.randomizer'
local Bag5AltRandomizer = Randomizer:extend()
function Bag5AltRandomizer:initialize()
self.bag = {"I", "J", "L", "O", "T"}
self.prev = nil
end
function Bag5AltRandomizer:generatePiece()
if next(self.bag) == nil then
self.bag = {"I", "J", "L", "O", "T"}
end
local x = math.random(table.getn(self.bag))
local temp = table.remove(self.bag, x)
if temp == self.prev then
local y = math.random(table.getn(self.bag))
temp = table.remove(self.bag, y)
end
self.prev = temp
return temp
end
return Bag5AltRandomizer

View File

@@ -0,0 +1,31 @@
local Randomizer = require 'tetris.randomizers.randomizer'
local Bag7NoSZOStartRandomizer = Randomizer:extend()
function Bag7NoSZOStartRandomizer:shuffleBag()
local b = self.bag
local ln = #b
for i = 1, ln do
local j = math.random(i, ln)
b[i], b[j] = b[j], b[i]
end
end
local function isnotSZO(x) return not(x == "S" or x == "Z" or x == "O") end
function Bag7NoSZOStartRandomizer:initialize()
self.bag = {"I", "J", "L", "O", "S", "T", "Z"}
repeat
self:shuffleBag()
until isnotSZO(self.bag[7])
end
function Bag7NoSZOStartRandomizer:generatePiece()
if #self.bag == 0 then
self.bag = {"I", "J", "L", "O", "S", "T", "Z"}
self:shuffleBag()
end
return table.remove(self.bag)
end
return Bag7NoSZOStartRandomizer

View File

@@ -0,0 +1,24 @@
local Randomizer = require 'tetris.randomizers.randomizer'
local Bag7Randomizer = Randomizer:extend()
function Bag7Randomizer:initialize()
self.bag = {"I", "J", "L", "O", "S", "T", "Z"}
self.extra = {"I", "J", "L", "O", "S", "T", "Z"}
table.insert(self.bag, table.remove(self.extra, math.random(table.getn(self.extra))))
end
function Bag7Randomizer:generatePiece()
if next(self.extra) == nil then
self.extra = {"I", "J", "L", "O", "S", "T", "Z"}
end
if next(self.bag) == nil then
self.bag = {"I", "J", "L", "O", "S", "T", "Z"}
table.insert(self.bag, table.remove(self.extra, math.random(table.getn(self.extra))))
end
local x = math.random(table.getn(self.bag))
--print("Bag: "..table.concat(self.bag, ", ").." | Extra: "..table.concat(self.extra, ", "))
return table.remove(self.bag, x)
end
return Bag7Randomizer

View File

@@ -0,0 +1,28 @@
local Randomizer = require 'tetris.randomizers.randomizer'
local BagKonoha = Randomizer:extend()
function BagKonoha:initialize()
self.bag = {"I", "J", "L", "O", "T"}
self.prev = nil
self.allowrepeat = false
self.generated = 0
end
function BagKonoha:generatePiece()
self.generated = self.generated + 1
if #self.bag == 0 then
self.bag = {"I", "J", "L", "O", "T"}
end
local x = math.random(#self.bag)
local temp = table.remove(self.bag, x)
if temp == self.prev and not self.allowrepeat then
local y = math.random(#self.bag)
table.insert(self.bag, temp) -- should insert at the end of the bag, bag[y] doesnt change
temp = table.remove(self.bag, y)
end
self.prev = temp
return temp
end
return BagKonoha

View File

@@ -3,17 +3,23 @@ local Randomizer = require 'tetris.randomizers.randomizer'
local History4RollsRandomizer = Randomizer:extend()
function History4RollsRandomizer:initialize()
self.history = {"Z", "S", "Z", "S"}
self.history = {"Z", "Z", "Z", "Z"}
self.first = true
end
function History4RollsRandomizer:generatePiece()
local shapes = {"I", "J", "L", "O", "S", "T", "Z"}
for i = 1, 4 do
local x = math.random(7)
if not inHistory(shapes[x], self.history) or i == 4 then
return self:updateHistory(shapes[x])
end
end
if self.first then
self.first = false
return self:updateHistory(({"L", "J", "I", "T"})[math.random(4)])
else
local shapes = {"I", "J", "L", "O", "S", "T", "Z"}
for i = 1, 4 do
local x = math.random(7)
if not inHistory(shapes[x], self.history) or i == 4 then
return self:updateHistory(shapes[x])
end
end
end
end
function History4RollsRandomizer:updateHistory(shape)

View File

@@ -4,16 +4,22 @@ local History6RollsRandomizer = Randomizer:extend()
function History6RollsRandomizer:initialize()
self.history = {"Z", "S", "Z", "S"}
self.first = true
end
function History6RollsRandomizer:generatePiece()
local shapes = {"I", "J", "L", "O", "S", "T", "Z"}
for i = 1, 6 do
local x = math.random(7)
if not inHistory(shapes[x], self.history) or i == 6 then
return self:updateHistory(shapes[x])
end
end
if self.first then
self.first = false
return self:updateHistory(({"L", "J", "I", "T"})[math.random(4)])
else
local shapes = {"I", "J", "L", "O", "S", "T", "Z"}
for i = 1, 6 do
local x = math.random(7)
if not inHistory(shapes[x], self.history) or i == 6 then
return self:updateHistory(shapes[x])
end
end
end
end
function History6RollsRandomizer:updateHistory(shape)

View File

@@ -1,38 +1,70 @@
local Randomizer = require 'tetris.randomizers.randomizer'
local History6RollsRandomizer = Randomizer:extend()
local History6Rolls35PoolRandomizer = Randomizer:extend()
function History6RollsRandomizer:initialize()
function History6Rolls35PoolRandomizer:initialize()
self.first = true
self.history = {"Z", "S", "Z", "S"}
self.bag_counts = {
I = 5, J = 5, L = 5, O = 5, S = 3, T = 5, Z = 3
self.pool = {
"I", "I", "I", "I", "I",
"T", "T", "T", "T", "T",
"L", "L", "L", "L", "L",
"J", "J", "J", "J", "J",
"S", "S", "S", "S", "S",
"Z", "Z", "Z", "Z", "Z",
"O", "O", "O", "O", "O",
}
self.droughts = {
I = 0,
T = 0,
L = 0,
J = 0,
S = 0,
Z = 0,
O = 0,
}
end
function History6RollsRandomizer:getBagPiece(n)
for shape, count in pairs(self.bag_counts) do
n = n - count
if n <= 0 then
return shape
end
end
function History6Rolls35PoolRandomizer:generatePiece()
local index, x
if self.first then
local prevent = {"S", "Z", "O"}
repeat
index = math.random(#self.pool)
x = self.pool[index]
until not inHistory(x, prevent)
self.first = false
else
for i = 1, 6 do
index = math.random(#self.pool)
x = self.pool[index]
if not inHistory(x, self.history) or i == 6 then
break
end
end
end
self.pool[index] = self:updateHistory(x)
return x
end
function History6RollsRandomizer:generatePiece()
for i = 1, 6 do
local x = self:getBagPiece(math.random(31))
if not inHistory(x, self.history) or i == 6 then
return self:updateHistory(x)
end
end
end
function History6RollsRandomizer:updateHistory(shape)
self.bag_counts[shape] = self.bag_counts[shape] - 1
local replaced_piece = table.remove(self.history, 1)
function History6Rolls35PoolRandomizer:updateHistory(shape)
table.remove(self.history, 1)
table.insert(self.history, shape)
self.bag_counts[replaced_piece] = self.bag_counts[replaced_piece] + 1
return shape
local highdrought
local highdroughtcount = 0
for k, v in pairs(self.droughts) do
if k == shape then
self.droughts[k] = 0
else
self.droughts[k] = v + 1
if v >= highdroughtcount then
highdrought = k
highdroughtcount = v
end
end
end
return highdrought
end
function inHistory(piece, history)
@@ -44,4 +76,4 @@ function inHistory(piece, history)
return false
end
return History6RollsRandomizer
return History6Rolls35PoolRandomizer

View File

@@ -4,15 +4,10 @@ local Randomizer = Object:extend()
function Randomizer:new()
self:initialize()
self.next_queue = {}
for i = 1, 30 do
table.insert(self.next_queue, self:generatePiece())
end
end
function Randomizer:nextPiece()
table.insert(self.next_queue, self:generatePiece())
return table.remove(self.next_queue, 1)
return self:generatePiece()
end
function Randomizer:initialize()

View File

@@ -0,0 +1,30 @@
local Randomizer = require 'tetris.randomizers.randomizer'
local RecursiveRandomizer = Randomizer:extend()
function RecursiveRandomizer:initialize()
self.bag = {"I", "J", "L", "O", "S", "T", "Z"}
end
function RecursiveRandomizer:generatePiece()
--if next(self.bag) == nil then
-- self.bag = {"I", "J", "L", "O", "S", "T", "Z"}
--end
local x = math.random(table.getn(self.bag) + 1)
while x == table.getn(self.bag) + 1 do
--print("Refill piece pulled")
table.insert(self.bag, "I")
table.insert(self.bag, "J")
table.insert(self.bag, "L")
table.insert(self.bag, "O")
table.insert(self.bag, "S")
table.insert(self.bag, "T")
table.insert(self.bag, "Z")
x = math.random(table.getn(self.bag) + 1)
end
--print("Number of pieces in bag: "..table.getn(self.bag))
--print("Bag: "..table.concat(self.bag, ", "))
return table.remove(self.bag, x)
end
return RecursiveRandomizer

View File

@@ -0,0 +1,19 @@
local Randomizer = require 'tetris.randomizers.randomizer'
local SegaRandomizer = Randomizer:extend()
function SegaRandomizer:initialize()
self.bag = {"I", "J", "L", "O", "S", "T", "Z"}
self.sequence = {}
for i = 1, 1000 do
self.sequence[i] = self.bag[math.random(table.getn(self.bag))]
end
self.counter = 0
end
function SegaRandomizer:generatePiece()
self.counter = self.counter + 1
return self.sequence[self.counter % 1000 + 1]
end
return SegaRandomizer

View File

@@ -16,6 +16,16 @@ ARS.spawn_positions = {
Z = { x=4, y=5 },
}
ARS.big_spawn_positions = {
I = { x=3, y=2 },
J = { x=2, y=3 },
L = { x=2, y=3 },
O = { x=3, 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} },
@@ -71,11 +81,19 @@ function ARS:attemptWallkicks(piece, new_piece, rot_dir, grid)
piece.shape == "J" or piece.shape == "T" or piece.shape == "L"
) and (
piece.rotation == 0 or piece.rotation == 2
) and (
grid:isOccupied(piece.position.x, piece.position.y) or
grid:isOccupied(piece.position.x, piece.position.y - 1) or
grid:isOccupied(piece.position.x, piece.position.y - 2)
) then return end
) then
local offsets = new_piece:getBlockOffsets()
table.sort(offsets, function(A, B) return A.y < B.y or A.y == B.y and A.x < B.y end)
for index, offset in pairs(offsets) do
if grid:isOccupied(piece.position.x + offset.x, piece.position.y + offset.y) then
if offset.x == 0 then
return
else
break
end
end
end
end
-- kick right, kick left
if (grid:canPlacePiece(new_piece:withOffset({x=1, y=0}))) then
@@ -92,7 +110,7 @@ function ARS:onPieceDrop(piece, grid)
piece.lock_delay = 0 -- step reset
end
function ARS:get180RotationValue() return config["reverse_rotate"] and 1 or 3 end
function ARS:get180RotationValue() return 3 end
function ARS:getDefaultOrientation() return 3 end -- downward facing pieces by default
return ARS

191
tetris/rulesets/arika_ace.lua Executable file
View File

@@ -0,0 +1,191 @@
local Piece = require 'tetris.components.piece'
local Ruleset = require 'tetris.rulesets.ruleset'
local ARS = Ruleset:extend()
ARS.name = "ACE-ARS"
ARS.hash = "ArikaACE"
ARS.colourscheme = {
I = "C",
L = "O",
J = "B",
S = "G",
Z = "R",
O = "Y",
T = "M",
}
ARS.softdrop_lock = false
ARS.harddrop_lock = true
ARS.spawn_positions = {
I = { x=5, y=2 },
J = { x=4, y=3 },
L = { x=4, y=3 },
O = { x=5, y=3 },
S = { x=4, y=3 },
T = { x=4, y=3 },
Z = { x=4, y=3 },
}
ARS.big_spawn_positions = {
I = { x=3, y=0 },
J = { x=2, y=1 },
L = { x=2, y=1 },
O = { x=3, y=1 },
S = { x=2, y=1 },
T = { x=2, y=1 },
Z = { x=2, y=1 },
}
ARS.block_offsets = {
I={
{ {x=0, y=0}, {x=-1, y=0}, {x=-2, y=0}, {x=1, y=0} },
{ {x=0, y=0}, {x=0, y=-1}, {x=0, y=1}, {x=0, y=2} },
{ {x=0, y=0}, {x=-1, y=0}, {x=-2, y=0}, {x=1, y=0} },
{ {x=0, y=0}, {x=0, y=-1}, {x=0, y=1}, {x=0, y=2} },
},
J={
{ {x=0, y=0}, {x=-1, y=0}, {x=1, y=0}, {x=-1, y=-1} },
{ {x=0, y=-1}, {x=1, y=-2}, {x=0, y=-2}, {x=0, y=0} },
{ {x=0, y=-1}, {x=-1, y=-1}, {x=1, y=-1}, {x=1, y=0} },
{ {x=0, y=-1}, {x=0, y=-2}, {x=0, y=0}, {x=-1, y=0} },
},
L={
{ {x=0, y=0}, {x=-1, y=0}, {x=1, y=0}, {x=1, y=-1} },
{ {x=0, y=-2}, {x=0, y=-1}, {x=1, y=0}, {x=0, y=0} },
{ {x=0, y=-1}, {x=-1, y=-1}, {x=1, y=-1}, {x=-1, y=0} },
{ {x=0, y=-1}, {x=-1, y=-2}, {x=0, y=-2}, {x=0, y=0} },
},
O={
{ {x=0, y=0}, {x=-1, y=0}, {x=-1, y=-1}, {x=0, y=-1} },
{ {x=0, y=0}, {x=-1, y=0}, {x=-1, y=-1}, {x=0, y=-1} },
{ {x=0, y=0}, {x=-1, y=0}, {x=-1, y=-1}, {x=0, y=-1} },
{ {x=0, y=0}, {x=-1, y=0}, {x=-1, y=-1}, {x=0, y=-1} },
},
S={
{ {x=1, y=-1}, {x=0, y=-1}, {x=0, y=0}, {x=-1, y=0} },
{ {x=-1, y=-2}, {x=-1, y=-1}, {x=0, y=-1}, {x=0, y=0} },
{ {x=1, y=-1}, {x=0, y=-1}, {x=0, y=0}, {x=-1, y=0} },
{ {x=-1, y=-2}, {x=-1, y=-1}, {x=0, y=-1}, {x=0, y=0} },
},
T={
{ {x=0, y=0}, {x=-1, y=0}, {x=1, y=0}, {x=0, y=-1} },
{ {x=0, y=-1}, {x=0, y=0}, {x=1, y=-1}, {x=0, y=-2} },
{ {x=0, y=-1}, {x=-1, y=-1}, {x=1, y=-1}, {x=0, y=0} },
{ {x=0, y=-1}, {x=0, y=0}, {x=-1, y=-1}, {x=0, y=-2} },
},
Z={
{ {x=0, y=-1}, {x=-1, y=-1}, {x=1, y=0}, {x=0, y=0} },
{ {x=0, y=-1}, {x=0, y=0}, {x=1, y=-2}, {x=1, y=-1} },
{ {x=0, y=-1}, {x=-1, y=-1}, {x=1, y=0}, {x=0, y=0} },
{ {x=0, y=-1}, {x=0, y=0}, {x=1, y=-2}, {x=1, y=-1} },
}
}
-- Component functions.
function ARS:attemptWallkicks(piece, new_piece, rot_dir, grid)
-- O doesn't kick
if (piece.shape == "O") then return end
-- center column rule
if (
piece.shape == "J" or piece.shape == "T" or piece.shape == "L"
) and (
piece.rotation == 0 or piece.rotation == 2
) then
local offsets = new_piece:getBlockOffsets()
table.sort(offsets, function(A, B) return A.y < B.y or A.y == B.y and A.x < B.y end)
for index, offset in pairs(offsets) do
if grid:isOccupied(piece.position.x + offset.x, piece.position.y + offset.y) then
if offset.x == 0 then
return
else
break
end
end
end
end
if piece.shape == "I" then
-- special kick rules for I
if new_piece.rotation == 0 or new_piece.rotation == 2 then
-- kick right, right2, left
if grid:canPlacePiece(new_piece:withOffset({x=1, y=0})) then
piece:setRelativeRotation(rot_dir):setOffset({x=1, y=0})
self:onPieceRotate(piece, grid)
elseif grid:canPlacePiece(new_piece:withOffset({x=2, y=0})) then
piece:setRelativeRotation(rot_dir):setOffset({x=2, y=0})
self:onPieceRotate(piece, grid)
elseif grid:canPlacePiece(new_piece:withOffset({x=-1, y=0})) then
piece:setRelativeRotation(rot_dir):setOffset({x=-1, y=0})
self:onPieceRotate(piece, grid)
end
elseif piece:isDropBlocked(grid) and (new_piece.rotation == 1 or new_piece.rotation == 3) and piece.floorkick == 0 then
-- kick up, up2
if grid:canPlacePiece(new_piece:withOffset({x=0, y=-1})) then
piece:setRelativeRotation(rot_dir):setOffset({x=0, y=-1})
self:onPieceRotate(piece, grid)
piece.floorkick = 1
elseif grid:canPlacePiece(new_piece:withOffset({x=0, y=-2})) then
piece:setRelativeRotation(rot_dir):setOffset({x=0, y=-2})
self:onPieceRotate(piece, grid)
piece.floorkick = 1
end
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})
elseif piece.shape == "T"
and new_piece.rotation == 0
and piece.floorkick == 0
and grid:canPlacePiece(new_piece:withOffset({x=0, y=-1}))
then
-- T floorkick
piece.floorkick = piece.floorkick + 1
piece:setRelativeRotation(rot_dir):setOffset({x=0, y=-1})
end
end
end
function ARS:onPieceCreate(piece, grid)
piece.floorkick = 0
piece.manipulations = 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.manipulations = piece.manipulations + 1
if piece.manipulations >= 127 then
piece.locked = true
end
end
end
function ARS:onPieceRotate(piece, grid)
piece.lock_delay = 0 -- rotate reset
if piece:isDropBlocked(grid) then
piece.manipulations = piece.manipulations + 1
if piece.manipulations >= 127 then
piece.locked = true
end
end
end
function ARS:get180RotationValue() return 3 end
function ARS:getDefaultOrientation() return 3 end -- downward facing pieces by default
return ARS

View File

@@ -0,0 +1,178 @@
local Piece = require 'tetris.components.piece'
local Ruleset = require 'tetris.rulesets.ruleset'
local ARS = Ruleset:extend()
ARS.name = "ACE-ARS2"
ARS.hash = "ArikaACE2"
ARS.spawn_positions = {
I = { x=5, y=2 },
J = { x=4, y=3 },
L = { x=4, y=3 },
O = { x=5, y=3 },
S = { x=4, y=3 },
T = { x=4, y=3 },
Z = { x=4, y=3 },
}
ARS.big_spawn_positions = {
I = { x=3, y=0 },
J = { x=2, y=1 },
L = { x=2, y=1 },
O = { x=3, y=1 },
S = { x=2, y=1 },
T = { x=2, y=1 },
Z = { x=2, y=1 },
}
ARS.block_offsets = {
I={
{ {x=0, y=0}, {x=-1, y=0}, {x=-2, y=0}, {x=1, y=0} },
{ {x=0, y=0}, {x=0, y=-1}, {x=0, y=1}, {x=0, y=2} },
{ {x=0, y=0}, {x=-1, y=0}, {x=-2, y=0}, {x=1, y=0} },
{ {x=0, y=0}, {x=0, y=-1}, {x=0, y=1}, {x=0, y=2} },
},
J={
{ {x=0, y=0}, {x=-1, y=0}, {x=1, y=0}, {x=-1, y=-1} },
{ {x=0, y=-1}, {x=1, y=-2}, {x=0, y=-2}, {x=0, y=0} },
{ {x=0, y=-1}, {x=-1, y=-1}, {x=1, y=-1}, {x=1, y=0} },
{ {x=0, y=-1}, {x=0, y=-2}, {x=0, y=0}, {x=-1, y=0} },
},
L={
{ {x=0, y=0}, {x=-1, y=0}, {x=1, y=0}, {x=1, y=-1} },
{ {x=0, y=-2}, {x=0, y=-1}, {x=1, y=0}, {x=0, y=0} },
{ {x=0, y=-1}, {x=-1, y=-1}, {x=1, y=-1}, {x=-1, y=0} },
{ {x=0, y=-1}, {x=-1, y=-2}, {x=0, y=-2}, {x=0, y=0} },
},
O={
{ {x=0, y=0}, {x=-1, y=0}, {x=-1, y=-1}, {x=0, y=-1} },
{ {x=0, y=0}, {x=-1, y=0}, {x=-1, y=-1}, {x=0, y=-1} },
{ {x=0, y=0}, {x=-1, y=0}, {x=-1, y=-1}, {x=0, y=-1} },
{ {x=0, y=0}, {x=-1, y=0}, {x=-1, y=-1}, {x=0, y=-1} },
},
S={
{ {x=1, y=-1}, {x=0, y=-1}, {x=0, y=0}, {x=-1, y=0} },
{ {x=-1, y=-2}, {x=-1, y=-1}, {x=0, y=-1}, {x=0, y=0} },
{ {x=1, y=-1}, {x=0, y=-1}, {x=0, y=0}, {x=-1, y=0} },
{ {x=-1, y=-2}, {x=-1, y=-1}, {x=0, y=-1}, {x=0, y=0} },
},
T={
{ {x=0, y=0}, {x=-1, y=0}, {x=1, y=0}, {x=0, y=-1} },
{ {x=0, y=-1}, {x=0, y=0}, {x=1, y=-1}, {x=0, y=-2} },
{ {x=0, y=-1}, {x=-1, y=-1}, {x=1, y=-1}, {x=0, y=0} },
{ {x=0, y=-1}, {x=0, y=0}, {x=-1, y=-1}, {x=0, y=-2} },
},
Z={
{ {x=0, y=-1}, {x=-1, y=-1}, {x=1, y=0}, {x=0, y=0} },
{ {x=0, y=-1}, {x=0, y=0}, {x=1, y=-2}, {x=1, y=-1} },
{ {x=0, y=-1}, {x=-1, y=-1}, {x=1, y=0}, {x=0, y=0} },
{ {x=0, y=-1}, {x=0, y=0}, {x=1, y=-2}, {x=1, y=-1} },
}
}
-- Component functions.
function ARS:attemptWallkicks(piece, new_piece, rot_dir, grid)
-- O doesn't kick
if (piece.shape == "O") then return end
-- center column rule
if (
piece.shape == "J" or piece.shape == "T" or piece.shape == "L"
) and (
piece.rotation == 0 or piece.rotation == 2
) then
local offsets = new_piece:getBlockOffsets()
table.sort(offsets, function(A, B) return A.y < B.y or A.y == B.y and A.x < B.y end)
for index, offset in pairs(offsets) do
if grid:isOccupied(piece.position.x + offset.x, piece.position.y + offset.y) then
if offset.x == 0 then
return
else
break
end
end
end
end
if piece.shape == "I" then
-- special kick rules for I
if new_piece.rotation == 0 or new_piece.rotation == 2 then
-- kick right, right2, left
if grid:canPlacePiece(new_piece:withOffset({x=1, y=0})) then
piece:setRelativeRotation(rot_dir):setOffset({x=1, y=0})
self:onPieceRotate(piece, grid)
elseif grid:canPlacePiece(new_piece:withOffset({x=2, y=0})) then
piece:setRelativeRotation(rot_dir):setOffset({x=2, y=0})
self:onPieceRotate(piece, grid)
elseif grid:canPlacePiece(new_piece:withOffset({x=-1, y=0})) then
piece:setRelativeRotation(rot_dir):setOffset({x=-1, y=0})
self:onPieceRotate(piece, grid)
end
elseif piece:isDropBlocked(grid) and (new_piece.rotation == 1 or new_piece.rotation == 3) and piece.floorkick == 0 then
-- kick up, up2
if grid:canPlacePiece(new_piece:withOffset({x=0, y=-1})) then
piece:setRelativeRotation(rot_dir):setOffset({x=0, y=-1})
self:onPieceRotate(piece, grid)
piece.floorkick = 1
elseif grid:canPlacePiece(new_piece:withOffset({x=0, y=-2})) then
piece:setRelativeRotation(rot_dir):setOffset({x=0, y=-2})
self:onPieceRotate(piece, grid)
piece.floorkick = 1
end
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})
elseif piece.shape == "T"
and new_piece.rotation == 0
and piece.floorkick == 0
and grid:canPlacePiece(new_piece:withOffset({x=0, y=-1}))
then
-- T floorkick
piece.floorkick = piece.floorkick + 1
piece:setRelativeRotation(rot_dir):setOffset({x=0, y=-1})
end
end
end
function ARS:onPieceCreate(piece, grid)
piece.floorkick = 0
piece.manipulations = 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.manipulations = piece.manipulations + 1
if piece.manipulations >= 127 then
piece.locked = true
end
end
end
function ARS:onPieceRotate(piece, grid)
piece.lock_delay = 0 -- rotate reset
if piece:isDropBlocked(grid) then
piece.manipulations = piece.manipulations + 1
if piece.manipulations >= 127 then
piece.locked = true
end
end
end
function ARS:get180RotationValue() return 3 end
function ARS:getDefaultOrientation() return 3 end -- downward facing pieces by default
return ARS

190
tetris/rulesets/arika_srs.lua Executable file
View File

@@ -0,0 +1,190 @@
local Piece = require 'tetris.components.piece'
local Ruleset = require 'tetris.rulesets.ruleset'
local SRS = Ruleset:extend()
SRS.name = "ACE-SRS"
SRS.hash = "ACE Standard"
SRS.world = true
SRS.colourscheme = {
I = "C",
L = "O",
J = "B",
S = "G",
Z = "R",
O = "Y",
T = "M",
}
SRS.softdrop_lock = false
SRS.harddrop_lock = true
SRS.enable_IRS_wallkicks = true
SRS.spawn_positions = {
I = { x=5, y=2 },
J = { x=4, y=3 },
L = { x=4, y=3 },
O = { x=5, y=3 },
S = { x=4, y=3 },
T = { x=4, y=3 },
Z = { x=4, y=3 },
}
SRS.big_spawn_positions = {
I = { x=3, y=0 },
J = { x=2, y=1 },
L = { x=2, y=1 },
O = { x=3, y=1 },
S = { x=2, y=1 },
T = { x=2, y=1 },
Z = { x=2, y=1 },
}
SRS.block_offsets = {
I={
{ {x=0, y=0}, {x=-1, y=0}, {x=-2, y=0}, {x=1, y=0} },
{ {x=0, y=0}, {x=0, y=-1}, {x=0, y=1}, {x=0, y=2} },
{ {x=0, y=1}, {x=-1, y=1}, {x=-2, y=1}, {x=1, y=1} },
{ {x=-1, y=0}, {x=-1, y=-1}, {x=-1, y=1}, {x=-1, y=2} },
},
J={
{ {x=0, y=0}, {x=-1, y=0}, {x=1, y=0}, {x=-1, y=-1} },
{ {x=0, y=0}, {x=0, y=-1}, {x=0, y=1} , {x=1, y=-1} },
{ {x=0, y=0}, {x=1, y=0}, {x=-1, y=0}, {x=1, y=1} },
{ {x=0, y=0}, {x=0, y=1}, {x=0, y=-1}, {x=-1, y=1} },
},
L={
{ {x=0, y=0}, {x=-1, y=0}, {x=1, y=0}, {x=1, y=-1} },
{ {x=0, y=0}, {x=0, y=-1}, {x=0, y=1}, {x=1, y=1} },
{ {x=0, y=0}, {x=1, y=0}, {x=-1, y=0}, {x=-1, y=1} },
{ {x=0, y=0}, {x=0, y=1}, {x=0, y=-1}, {x=-1, y=-1} },
},
O={
{ {x=0, y=0}, {x=-1, y=0}, {x=-1, y=-1}, {x=0, y=-1} },
{ {x=0, y=0}, {x=-1, y=0}, {x=-1, y=-1}, {x=0, y=-1} },
{ {x=0, y=0}, {x=-1, y=0}, {x=-1, y=-1}, {x=0, y=-1} },
{ {x=0, y=0}, {x=-1, y=0}, {x=-1, y=-1}, {x=0, y=-1} },
},
S={
{ {x=1, y=-1}, {x=0, y=-1}, {x=0, y=0}, {x=-1, y=0} },
{ {x=1, y=1}, {x=1, y=0}, {x=0, y=0}, {x=0, y=-1} },
{ {x=-1, y=1}, {x=0, y=1}, {x=0, y=0}, {x=1, y=0} },
{ {x=-1, y=-1}, {x=-1, y=0}, {x=0, y=0}, {x=0, y=1} },
},
T={
{ {x=0, y=0}, {x=-1, y=0}, {x=1, y=0}, {x=0, y=-1} },
{ {x=0, y=0}, {x=0, y=-1}, {x=0, y=1}, {x=1, y=0} },
{ {x=0, y=0}, {x=1, y=0}, {x=-1, y=0}, {x=0, y=1} },
{ {x=0, y=0}, {x=0, y=1}, {x=0, y=-1}, {x=-1, y=0} },
},
Z={
{ {x=-1, y=-1}, {x=0, y=-1}, {x=0, y=0}, {x=1, y=0} },
{ {x=1, y=-1}, {x=1, y=0}, {x=0, y=0}, {x=0, y=1} },
{ {x=1, y=1}, {x=0, y=1}, {x=0, y=0}, {x=-1, y=0} },
{ {x=-1, y=1}, {x=-1, y=0}, {x=0, y=0}, {x=0, y=-1} },
}
}
SRS.wallkicks_3x3 = {
[0]={
[1]={{x=-1, y=0}, {x=-1, y=-1}, {x=0, y=2}, {x=-1, y=2}},
[2]={{x=0, y=1}, {x=0, y=-1}},
[3]={{x=1, y=0}, {x=1, y=-1}, {x=0, y=2}, {x=1, y=2}},
},
[1]={
[0]={{x=1, y=0}, {x=1, y=1}, {x=0, y=-2}, {x=1, y=-2}},
[2]={{x=1, y=0}, {x=1, y=1}, {x=0, y=-2}, {x=1, y=-2}},
[3]={{x=0, y=1}, {x=0, y=-1}},
},
[2]={
[0]={{x=0, y=1}, {x=0, y=-1}},
[1]={{x=-1, y=0}, {x=-1, y=-1}, {x=0, y=2}, {x=-1, y=2}},
[3]={{x=1, y=0}, {x=1, y=-1}, {x=0, y=2}, {x=1, y=2}},
},
[3]={
[0]={{x=-1, y=0}, {x=-1, y=1}, {x=0, y=-2}, {x=-1, y=-2}},
[1]={{x=0, y=1}, {x=0, y=-1}},
[2]={{x=-1, y=0}, {x=-1, y=1}, {x=0, y=-2}, {x=-1, y=-2}},
},
};
SRS.wallkicks_line = {
[0]={
[1]={{x=-2, y= 0}, {x= 1, y= 0}, {x= 1, y=-2}, {x=-2, y= 1}},
[2]={},
[3]={{x= 2, y= 0}, {x=-1, y= 0}, {x=-1, y=-2}, {x= 2, y= 1}},
},
[1]={
[0]={{x= 2, y= 0}, {x=-1, y= 0}, {x= 2, y=-1}, {x=-1, y= 2}},
[2]={{x=-1, y= 0}, {x= 2, y= 0}, {x=-1, y=-2}, {x= 2, y= 1}},
[3]={},
},
[2]={
[0]={},
[1]={{x=-2, y= 0}, {x= 1, y= 0}, {x=-2, y=-1}, {x= 1, y= 1}},
[3]={{x= 2, y= 0}, {x=-1, y= 0}, {x= 2, y=-1}, {x=-1, y= 1}},
},
[3]={
[0]={{x=-2, y= 0}, {x= 1, y= 0}, {x=-2, y=-1}, {x= 1, y= 2}},
[1]={},
[2]={{x= 1, y= 0}, {x=-2, y= 0}, {x= 1, y=-2}, {x=-2, y= 1}},
},
};
-- Component functions.
function SRS:attemptWallkicks(piece, new_piece, rot_dir, grid)
local kicks
if piece.shape == "O" then
return
elseif piece.shape == "I" then
kicks = SRS.wallkicks_line[piece.rotation][new_piece.rotation]
else
kicks = SRS.wallkicks_3x3[piece.rotation][new_piece.rotation]
end
assert(piece.rotation ~= new_piece.rotation)
for idx, offset in pairs(kicks) do
kicked_piece = new_piece:withOffset(offset)
if grid:canPlacePiece(kicked_piece) then
piece:setRelativeRotation(rot_dir)
piece:setOffset(offset)
self:onPieceRotate(piece, grid)
return
end
end
end
function SRS:onPieceCreate(piece, grid)
piece.manipulations = 0
end
function SRS:onPieceDrop(piece, grid)
piece.lock_delay = 0 -- step reset
end
function SRS:onPieceMove(piece, grid)
piece.lock_delay = 0 -- move reset
if piece:isDropBlocked(grid) then
piece.manipulations = piece.manipulations + 1
if piece.manipulations >= 127 then
piece.locked = true
end
end
end
function SRS:onPieceRotate(piece, grid)
piece.lock_delay = 0 -- rotate reset
if piece:isDropBlocked(grid) then
piece.manipulations = piece.manipulations + 1
if piece.manipulations >= 127 then
piece.locked = true
end
end
end
function SRS:get180RotationValue() return 3 end
return SRS

View File

@@ -16,6 +16,16 @@ ARS.spawn_positions = {
Z = { x=4, y=5 },
}
ARS.big_spawn_positions = {
I = { x=3, y=2 },
J = { x=2, y=3 },
L = { x=2, y=3 },
O = { x=3, 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} },
@@ -74,11 +84,19 @@ function ARS:attemptWallkicks(piece, new_piece, rot_dir, grid)
piece.shape == "J" or piece.shape == "T" or piece.shape == "L"
) and (
piece.rotation == 0 or piece.rotation == 2
) and (
grid:isOccupied(piece.position.x, piece.position.y) or
grid:isOccupied(piece.position.x, piece.position.y - 1) or
grid:isOccupied(piece.position.x, piece.position.y - 2)
) then return end
) then
local offsets = new_piece:getBlockOffsets()
table.sort(offsets, function(A, B) return A.y < B.y or A.y == B.y and A.x < B.y end)
for index, offset in pairs(offsets) do
if grid:isOccupied(piece.position.x + offset.x, piece.position.y + offset.y) then
if offset.x == 0 then
return
else
break
end
end
end
end
if piece.shape == "I" then
-- special kick rules for I
@@ -106,14 +124,21 @@ function ARS:attemptWallkicks(piece, new_piece, rot_dir, grid)
piece.floorkick = 1
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
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})
elseif piece.shape == "T"
and new_piece.rotation == 0
and piece.floorkick == 0
and grid:canPlacePiece(new_piece:withOffset({x=0, y=-1}))
then
-- T floorkick
piece.floorkick = piece.floorkick + 1
piece:setRelativeRotation(rot_dir):setOffset({x=0, y=-1})
end
end
end
@@ -126,7 +151,7 @@ function ARS:onPieceDrop(piece, grid)
piece.lock_delay = 0 -- step reset
end
function ARS:get180RotationValue() return config["reverse_rotate"] and 1 or 3 end
function ARS:get180RotationValue() return 3 end
function ARS:getDefaultOrientation() return 3 end -- downward facing pieces by default
return ARS

View File

@@ -5,15 +5,26 @@ local CRS = Ruleset:extend()
CRS.name = "Cambridge"
CRS.hash = "Cambridge"
CRS.world = true
CRS.spawn_positions = {
I = { x=5, y=4 },
J = { x=4, y=5 },
L = { x=4, y=5 },
O = { x=5, y=5 },
S = { x=4, y=4 },
S = { x=4, y=5 },
T = { x=4, y=5 },
Z = { x=4, y=4 },
Z = { x=4, y=5 },
}
CRS.big_spawn_positions = {
I = { x=3, y=2 },
J = { x=2, y=3 },
L = { x=2, y=3 },
O = { x=3, y=3 },
S = { x=2, y=3 },
T = { x=2, y=3 },
Z = { x=2, y=3 },
}
CRS.block_offsets = {
@@ -42,10 +53,10 @@ CRS.block_offsets = {
{ {x=0, y=0}, {x=-1, y=0}, {x=-1, y=-1}, {x=0, y=-1} },
},
S={
{ {x=-1, y=1}, {x=0, y=1}, {x=0, y=0}, {x=1, y=0} },
{ {x=0, y=1}, {x=0, y=0}, {x=-1, y=0}, {x=-1, y=-1} },
{ {x=-1, y=1}, {x=0, y=1}, {x=0, y=0}, {x=1, y=0} },
{ {x=0, y=1}, {x=0, y=0}, {x=-1, y=0}, {x=-1, y=-1} },
{ {x=-1, y=0}, {x=0, y=0}, {x=0, y=-1}, {x=1, y=-1} },
{ {x=0, y=0}, {x=0, y=-1}, {x=-1, y=-1}, {x=-1, y=-2} },
{ {x=-1, y=0}, {x=0, y=0}, {x=0, y=-1}, {x=1, y=-1} },
{ {x=0, y=0}, {x=0, y=-1}, {x=-1, y=-1}, {x=-1, y=-2} },
},
T={
{ {x=0, y=0}, {x=-1, y=0}, {x=1, y=0}, {x=0, y=-1} },
@@ -54,10 +65,10 @@ CRS.block_offsets = {
{ {x=0, y=0}, {x=0, y=1}, {x=0, y=-1}, {x=-1, y=0} },
},
Z={
{ {x=1, y=1}, {x=0, y=1}, {x=0, y=0}, {x=-1, y=0} },
{ {x=1, y=-1}, {x=1, y=0}, {x=0, y=0}, {x=0, y=1} },
{ {x=1, y=1}, {x=0, y=1}, {x=0, y=0}, {x=-1, y=0} },
{ {x=1, y=-1}, {x=1, y=0}, {x=0, y=0}, {x=0, y=1} },
{ {x=1, y=0}, {x=0, y=0}, {x=0, y=-1}, {x=-1, y=-1} },
{ {x=1, y=-2}, {x=1, y=-1}, {x=0, y=-1}, {x=0, y=0} },
{ {x=1, y=0}, {x=0, y=0}, {x=0, y=-1}, {x=-1, y=-1} },
{ {x=1, y=-2}, {x=1, y=-1}, {x=0, y=-1}, {x=0, y=0} },
}
}
@@ -352,6 +363,10 @@ function CRS:attemptRotate(new_inputs, piece, grid, initial)
end
if rot_dir == 0 then return end
if self.world and config.gamesettings.world_reverse == 2 then
rot_dir = 4 - rot_dir
end
local new_piece = piece:withRelativeRotation(rot_dir)
self:attemptWallkicks(piece, new_piece, rot_dir, grid)

View File

@@ -6,6 +6,20 @@ local Ruleset = Object:extend()
Ruleset.name = ""
Ruleset.hash = ""
-- Arika-type ruleset defaults
Ruleset.world = false
Ruleset.colourscheme = {
I = "R",
L = "O",
J = "B",
S = "M",
Z = "G",
O = "Y",
T = "C",
}
Ruleset.softdrop_lock = true
Ruleset.harddrop_lock = false
Ruleset.enable_IRS_wallkicks = false
-- Component functions.
@@ -39,6 +53,9 @@ function Ruleset:attemptRotate(new_inputs, piece, grid, initial)
end
if rot_dir == 0 then return end
if self.world and config.gamesettings.world_reverse == 2 then
rot_dir = 4 - rot_dir
end
local new_piece = piece:withRelativeRotation(rot_dir)
@@ -72,10 +89,17 @@ function Ruleset:movePiece(piece, grid, move)
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 +126,21 @@ 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 colours = ({self.colourscheme, ColourSchemes.Arika, ColourSchemes.TTC})[config.gamesettings.piece_colour]
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, colours[data.shape], 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 +153,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:dropPiece(
inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked,
hard_drop_enabled, additive_gravity
)
self:lockPiece(piece, grid, lock_delay)
end

38
tetris/rulesets/standard_exp.lua Normal file → Executable file
View File

@@ -3,19 +3,41 @@ local Ruleset = require 'tetris.rulesets.ruleset'
local SRS = Ruleset:extend()
SRS.name = "SRS"
SRS.name = "Guideline SRS"
SRS.hash = "Standard"
SRS.world = true
SRS.colourscheme = {
I = "C",
L = "O",
J = "B",
S = "G",
Z = "R",
O = "Y",
T = "M",
}
SRS.softdrop_lock = false
SRS.harddrop_lock = true
SRS.enable_IRS_wallkicks = true
SRS.spawn_positions = {
I = { x=5, y=4 },
J = { x=4, y=5 },
L = { x=4, y=5 },
O = { x=5, y=5 },
S = { x=4, y=5 },
T = { x=4, y=5 },
Z = { x=4, y=5 },
I = { x=5, y=2 },
J = { x=4, y=3 },
L = { x=4, y=3 },
O = { x=5, y=3 },
S = { x=4, y=3 },
T = { x=4, y=3 },
Z = { x=4, y=3 },
}
SRS.big_spawn_positions = {
I = { x=3, y=0 },
J = { x=2, y=1 },
L = { x=2, y=1 },
O = { x=3, y=1 },
S = { x=2, y=1 },
T = { x=2, y=1 },
Z = { x=2, y=1 },
}
SRS.block_offsets = {

191
tetris/rulesets/ti_srs.lua Normal file
View File

@@ -0,0 +1,191 @@
local Piece = require 'tetris.components.piece'
local Ruleset = require 'tetris.rulesets.ruleset'
local SRS = Ruleset:extend()
SRS.name = "Ti-World"
SRS.hash = "Bad I-kicks"
SRS.world = true
SRS.colourscheme = {
I = "C",
L = "O",
J = "B",
S = "G",
Z = "R",
O = "Y",
T = "M",
}
SRS.softdrop_lock = false
SRS.harddrop_lock = true
SRS.enable_IRS_wallkicks = true
SRS.spawn_positions = {
I = { x=5, y=4 },
J = { x=4, y=5 },
L = { x=4, y=5 },
O = { x=5, y=5 },
S = { x=4, y=5 },
T = { x=4, y=5 },
Z = { x=4, y=5 },
}
SRS.big_spawn_positions = {
I = { x=3, y=2 },
J = { x=2, y=3 },
L = { x=2, y=3 },
O = { x=3, 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} },
{ {x=0, y=0}, {x=0, y=-1}, {x=0, y=1}, {x=0, y=2} },
{ {x=0, y=1}, {x=-1, y=1}, {x=-2, y=1}, {x=1, y=1} },
{ {x=-1, y=0}, {x=-1, y=-1}, {x=-1, y=1}, {x=-1, y=2} },
},
J={
{ {x=0, y=0}, {x=-1, y=0}, {x=1, y=0}, {x=-1, y=-1} },
{ {x=0, y=0}, {x=0, y=-1}, {x=0, y=1} , {x=1, y=-1} },
{ {x=0, y=0}, {x=1, y=0}, {x=-1, y=0}, {x=1, y=1} },
{ {x=0, y=0}, {x=0, y=1}, {x=0, y=-1}, {x=-1, y=1} },
},
L={
{ {x=0, y=0}, {x=-1, y=0}, {x=1, y=0}, {x=1, y=-1} },
{ {x=0, y=0}, {x=0, y=-1}, {x=0, y=1}, {x=1, y=1} },
{ {x=0, y=0}, {x=1, y=0}, {x=-1, y=0}, {x=-1, y=1} },
{ {x=0, y=0}, {x=0, y=1}, {x=0, y=-1}, {x=-1, y=-1} },
},
O={
{ {x=0, y=0}, {x=-1, y=0}, {x=-1, y=-1}, {x=0, y=-1} },
{ {x=0, y=0}, {x=-1, y=0}, {x=-1, y=-1}, {x=0, y=-1} },
{ {x=0, y=0}, {x=-1, y=0}, {x=-1, y=-1}, {x=0, y=-1} },
{ {x=0, y=0}, {x=-1, y=0}, {x=-1, y=-1}, {x=0, y=-1} },
},
S={
{ {x=1, y=-1}, {x=0, y=-1}, {x=0, y=0}, {x=-1, y=0} },
{ {x=1, y=1}, {x=1, y=0}, {x=0, y=0}, {x=0, y=-1} },
{ {x=-1, y=1}, {x=0, y=1}, {x=0, y=0}, {x=1, y=0} },
{ {x=-1, y=-1}, {x=-1, y=0}, {x=0, y=0}, {x=0, y=1} },
},
T={
{ {x=0, y=0}, {x=-1, y=0}, {x=1, y=0}, {x=0, y=-1} },
{ {x=0, y=0}, {x=0, y=-1}, {x=0, y=1}, {x=1, y=0} },
{ {x=0, y=0}, {x=1, y=0}, {x=-1, y=0}, {x=0, y=1} },
{ {x=0, y=0}, {x=0, y=1}, {x=0, y=-1}, {x=-1, y=0} },
},
Z={
{ {x=-1, y=-1}, {x=0, y=-1}, {x=0, y=0}, {x=1, y=0} },
{ {x=1, y=-1}, {x=1, y=0}, {x=0, y=0}, {x=0, y=1} },
{ {x=1, y=1}, {x=0, y=1}, {x=0, y=0}, {x=-1, y=0} },
{ {x=-1, y=1}, {x=-1, y=0}, {x=0, y=0}, {x=0, y=-1} },
}
}
SRS.wallkicks_3x3 = {
[0]={
[1]={{x=-1, y=0}, {x=-1, y=-1}, {x=0, y=2}, {x=-1, y=2}},
[2]={{x=0, y=1}, {x=0, y=-1}},
[3]={{x=1, y=0}, {x=1, y=-1}, {x=0, y=2}, {x=1, y=2}},
},
[1]={
[0]={{x=1, y=0}, {x=1, y=1}, {x=0, y=-2}, {x=1, y=-2}},
[2]={{x=1, y=0}, {x=1, y=1}, {x=0, y=-2}, {x=1, y=-2}},
[3]={{x=0, y=1}, {x=0, y=-1}},
},
[2]={
[0]={{x=0, y=1}, {x=0, y=-1}},
[1]={{x=-1, y=0}, {x=-1, y=-1}, {x=0, y=2}, {x=-1, y=2}},
[3]={{x=1, y=0}, {x=1, y=-1}, {x=0, y=2}, {x=1, y=2}},
},
[3]={
[0]={{x=-1, y=0}, {x=-1, y=1}, {x=0, y=-2}, {x=-1, y=-2}},
[1]={{x=0, y=1}, {x=0, y=-1}},
[2]={{x=-1, y=0}, {x=-1, y=1}, {x=0, y=-2}, {x=-1, y=-2}},
},
};
SRS.wallkicks_line = {
[0]={
[1]={{x=-2, y= 0}, {x= 1, y= 0}, {x= 1, y=-2}, {x=-2, y= 1}},
[2]={},
[3]={{x= 2, y= 0}, {x=-1, y= 0}, {x=-1, y=-2}, {x= 2, y= 1}},
},
[1]={
[0]={{x= 2, y= 0}, {x=-1, y= 0}, {x= 2, y=-1}, {x=-1, y= 2}},
[2]={{x=-1, y= 0}, {x= 2, y= 0}, {x=-1, y=-2}, {x= 2, y= 1}},
[3]={},
},
[2]={
[0]={},
[1]={{x=-2, y= 0}, {x= 1, y= 0}, {x=-2, y=-1}, {x= 1, y= 1}},
[3]={{x= 2, y= 0}, {x=-1, y= 0}, {x= 2, y=-1}, {x=-1, y= 1}},
},
[3]={
[0]={{x=-2, y= 0}, {x= 1, y= 0}, {x=-2, y=-1}, {x= 1, y= 2}},
[1]={},
[2]={{x= 1, y= 0}, {x=-2, y= 0}, {x= 1, y=-2}, {x=-2, y= 1}},
},
};
-- Component functions.
function SRS:attemptWallkicks(piece, new_piece, rot_dir, grid)
local kicks
if piece.shape == "O" then
return
elseif piece.shape == "I" then
kicks = SRS.wallkicks_line[piece.rotation][new_piece.rotation]
else
kicks = SRS.wallkicks_3x3[piece.rotation][new_piece.rotation]
end
assert(piece.rotation ~= new_piece.rotation)
for idx, offset in pairs(kicks) do
kicked_piece = new_piece:withOffset(offset)
if grid:canPlacePiece(kicked_piece) then
piece:setRelativeRotation(rot_dir)
piece:setOffset(offset)
self:onPieceRotate(piece, grid)
return
end
end
end
function SRS:onPieceCreate(piece, grid)
piece.manipulations = 0
piece.rotations = 0
end
function SRS:onPieceDrop(piece, grid)
piece.lock_delay = 0 -- step reset
end
function SRS:onPieceMove(piece, grid)
piece.lock_delay = 0 -- move reset
if piece:isDropBlocked(grid) then
piece.manipulations = piece.manipulations + 1
if piece.manipulations >= 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.rotations = piece.rotations + 1
if piece.rotations >= 8 then
piece.locked = true
end
end
end
function SRS:get180RotationValue() return 3 end
return SRS