commit c973929e0c3f72b8b4830762ad88708e13b92682 Author: Joe Z Date: Wed May 22 23:57:34 2019 -0400 First bundled release. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0bdafc3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.sav +*.love +dist/*.zip +dist/**/cambridge.exe diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..be7be2f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,18 @@ +Coding conventions +------------------ + +* Use tabs to indent, spaces to align. + * Specifically, spaces should not appear at the beginning of a line, and tabs should not appear _except_ at the beginning of a line. + * The sole exception is in a multiline `if` statement; the initial `if` should have four spaces before it to align it with an `elseif` on the next line. + * Comments with lines at the end of code must be one line long. Multi-line comments must appeare in their own block. +* Use `snake_case` for variables, `camelCase` for functions. + + +Contributor's License Agreement +------------------------------- + +By contributing source code or other assets (e.g. music, artwork, graphics) to Cambridge, through a pull request or otherwise, you provide me with a non-exclusive, royalty-free, worldwide, perpetual license to use, copy, modify, distribute, sublicense, publicly perform, and create derivative works of the assets for any purpose. + +You also waive all moral rights to your contributions insofar as they are used in the Cambridge repository or in any code or works deriving therefrom. + +(Regarding the above clause, I will still make my best effort to provide sufficient attribution to all contributions. At the very least you'll get documentation of your contributions under SOURCES, and probably a special place in the credit roll as well.) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d86694c --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +The code in Cambridge is licensed under the MIT license. + +Copyright (c) 2018 Joe Zeng + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------- + +Some code and assets in this repository are contributed by members of the +community, as well as borrowed from other places, either with licensing +or as placeholders until suitable material can be found that is properly +licensed. Their original sources, and copyright notices if applicable, are +listed in the file SOURCES. \ No newline at end of file diff --git a/SOURCES.md b/SOURCES.md new file mode 100644 index 0000000..6851c4e --- /dev/null +++ b/SOURCES.md @@ -0,0 +1,108 @@ +Asset Sources +====== + +The assets in Cambridge are generally modified or formatted versions of externally gathered assets. Their original sources, if applicable, are listed below. + +Some of the assets are used without proper licenses. We aim to have fully licensed or public-domain assets by version 1.0. + +Backgrounds +----------- + +1. Title: "Motus Glacies." Contributed by Daniel "Explo" McCarthy. + +1. *Gameplay level 0: "Quantum foam." Alex Sukontsev. https://www.flickr.com/photos/control9/14957509814/ +2. *Gameplay level 1: No name. http://www.onekind.tv/univision-mqb/q5mqh5brlvuuj2nhdx7ch7eum183uu +3. Gameplay level 2: "M81 Galaxy is Pretty in Pink." Jet Propulsion Lab, NASA. https://www.jpl.nasa.gov/spaceimages/details.php?id=pia09579 +4. Gameplay level 3: "Formation of the Solar System." NASA. https://www.nasa.gov/images/content/149890main_BetaPictDiskbMac.jpg +5. *Gameplay level 4: No name. Dana Berry. https://news.nationalgeographic.com/news/2014/06/140605-earth-moon-theia-evidence-space-science/ +6. *Gameplay level 5: "Fauna of the Burgess Shale." Carel Brest van Kempen. https://www.science-art.com/image/?id=7200&pagename=Fauna_of_the_Burgess_Shale +7. *Gameplay level 6: No name. National Geographic. http://images.nationalgeographic.com/wpf/media-live/photos/000/009/cache/cretaceous-collection_907_990x742.jpg +8. *Gameplay level 7: [original URL not found.] +9. *Gameplay level 8: [original URL not found.] +10. *Gameplay level 9: [original URL not found.] +11. Gameplay level 10: "Leif Eriksson Discovers America." Christian Krohg (1893). https://commons.wikimedia.org/wiki/File:Christian-krohg-leiv-eriksson.jpg +12. Gameplay level 11: "Taking of Jerusalem by the Crusaders, 15th July 1099." Émile Signol (1847). https://commons.wikimedia.org/wiki/File:Counquest_of_Jeusalem_(1099).jpg +13. *Gameplay level 12: [original URL not found.] +14. *Gameplay level 13: "Black Death Village." Jonas Hassibi, deviantART. https://jonashassibi.deviantart.com/art/Black-Death-Village-617757443 +15. Gameplay level 14: "Landing of Columbus." Architect of the Capitol, Flickr. https://commons.wikimedia.org/wiki/File:Flickr_-_USCapitol_-_Landing_of_Columbus_(1).jpg +16. *Gameplay level 15: [original URL not found.] +17. Gameplay level 16: "Galileo Donato." H. J. Detouche. https://commons.wikimedia.org/wiki/File:Galileo_Donato.jpg +18. Gameplay level 17: "The Death of General Mercer at the Battle of Princeton, January 3, 1777." John Trumbull. https://commons.wikimedia.org/wiki/File:The_Death_of_General_Mercer_at_the_Battle_of_Princeton_January_3_1777.jpeg +19. Gameplay level 18: "Царскосельская железная дорога". Unknown. https://commons.wikimedia.org/wiki/File:%D0%A6%D0%B0%D1%80%D1%81%D0%BA%D0%BE%D1%81%D0%B5%D0%BB%D1%8C%D1%81%D0%BA%D0%B0%D1%8F_%D0%B6%D0%B5%D0%BB%D0%B5%D0%B7%D0%BD%D0%B0%D1%8F_%D0%B4%D0%BE%D1%80%D0%BE%D0%B3%D0%B0.jpg +20. *Gameplay level 19: [original URL not found.] + +Backgrounds marked with a * are placeholders that will be replaced in later versions due to incompatible licenses. We are generally aiming for public domain background images, but will also accept backgrounds given proper licenses to be included within Cambridge. + + +Music +----- + +1. TGM3 credit roll music. +2. The FitnessGram™ Pacer Test. + +All background music is (currently) only unofficially included. In later releases they may be replaced with specifically licensed music as applicable. + + +Fonts +----- + +1. System/general font: Kilo-4 3x5. Created by Joe Zeng. +2. Timer font: Big Number 8x12. Created by Joe Zeng. + +All fonts are original works, licensed under the MIT license as applicable. + + +Libraries +========= + +Cambridge makes use of the following libraries and third-party programs: + + +LÖVE (https://love2d.org/) +---------------- + +Copyright © 2006-2016 LÖVE Development Team + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + + +classic (https://github.com/rxi/classic) +---------------------- + +Copyright (c) 2014, rxi + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +binser (https://github.com/bakpakin/binser) +-------------------- + +Copyright (c) 2015 Calvin Rose + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/conf.lua b/conf.lua new file mode 100644 index 0000000..8fdb686 --- /dev/null +++ b/conf.lua @@ -0,0 +1,8 @@ +function love.conf(t) + t.identity = "cambridge" + + t.window.title = "Cambridge" + t.window.width = 640 + t.window.height = 480 + t.window.vsync = false +end diff --git a/dist/win32/love.exe b/dist/win32/love.exe new file mode 100755 index 0000000..bcf9607 Binary files /dev/null and b/dist/win32/love.exe differ diff --git a/dist/windows/love.exe b/dist/windows/love.exe new file mode 100755 index 0000000..480cc0f Binary files /dev/null and b/dist/windows/love.exe differ diff --git a/docs/gamemodes.md b/docs/gamemodes.md new file mode 100644 index 0000000..77aaafb --- /dev/null +++ b/docs/gamemodes.md @@ -0,0 +1,43 @@ +Game modes +========== + +There are several classes of game modes. + + +MARATHON +-------- + +Modes in which the goal is to play as well as possible over a limited game interval, to ultimately achieve the title of Grand Master. + +* **MARATHON**: Get to level 999! +* **MARATHON 2020**: 2020 levels of pure pain. + +From other games: +* **MARATHON A1**: Tetris the Grand Master 1. +* **MARATHON A2**: Tetris the Grand Master 2 (TAP Master). +* **MARATHON A3**: Tetris the Grand Master 3 (no exams). + +* **MARATHON N1**: NES Tetris A-type. + + +SURVIVAL +-------- + +Modes that concentrate on how long you can survive an increasingly fast and difficult game. + +* **SURVIVAL**: + +From other games: +* **SURVIVAL A2**: T.A. Death. +* **SURVIVAL A3**: Ti Shirase. + + +RACE +---- + +Modes in which the goal is to achieve a fixed goal in the shortest time. + +* **RACE 40L**: Clear 40 lines as fast as possible. +* **RACE 100L**: Clear 100 lines as fast as possible. +* **RACE 5K**: Clear 5,000 lines as fast as possible. Don't worry about topping out! +* **RACE 10K**: Clear 10,000 lines as fast as possible. Don't worry about topping out! diff --git a/docs/new_modes/marathon_2020.md b/docs/new_modes/marathon_2020.md new file mode 100644 index 0000000..0700336 --- /dev/null +++ b/docs/new_modes/marathon_2020.md @@ -0,0 +1,207 @@ +Marathon 2020 +============= + +To celebrate the coming of the year 2020, I've created a new "extended Tetris the Grand Master" mode where the level counter goes up twice as far as normal, all the way up to 2020. + + +Gameplay +-------- + +The goal of this game is to reach the end at level 2020. + +Every piece placed increases the level by 1, and every line cleared also increases the level by 1, with bonuses for large numbers of lines: + +| Lines cleared | Levels advanced | +|---------------|-----------------| +| 1 | 1 | +| 2 | 2 | +| 3 | 4 | +| 4 | 6 | + +When the current level reaches one less than the level at the bottom of the display (usually a multiple of 100), the level will not advance until a line is cleared. + + +Levels +------ + +Each section is 100 levels long, except for the last section, whose levels go from 1900 all the way to 2020. + +However, it is possible to be stopped early on if you do not play fast enough. + + +### Torikans + +There are certain checkpoints at which your current time will be checked and you will be stopped if your time is over a set objective time. + +| Level | Time limit | +|-------|------------| +| 500 | 6:00.00 | +| 900 | 8:30.00 | +| 1000 | 8:45.00 | +| 1500 | 11:30.00 | +| 1900 | 13:15.00 | + +At levels 500, 1000, and 1500, you will be stopped immediately if your time is not under the objective. + +At levels 900 and 1900, the next section will be capped at 999 or 1999 respectively, and you will get a short credit roll when the section is over. + + +Speed +----- + +Marathon 2020 gets faster in two different, independent ways, the gravity curve and the delay curve. + +The gravity curve is always the same at a particular level, while the delay curve can vary based on your previous section time. + +### Gravity Curve + +The gravity curve is the same as it is in the original TGM and TAP. + +### Delay Curve + +The delay curve is shown as in the following table. Line ARE is always equal to ARE. + +If your time in a particular section from 0 to 70 is smaller than the "cool" requirement at that level, your delay curve will be bumped up an extra level at the end of the section. + +The delay curve always advances at least 1 level past level 500, and if you get a section cool when the level is past 500, it will advance 2 levels instead. + +| Level | ARE | Lock | DAS | Line | Cool | +|-------|------|------|------|------|------| +|0|27|30|15|40|45.00| +|100|24|30|12|25|41.50| +|200|21|30|12|25|38.50| +|300|18|30|9|20|35.00| +|400|16|30|9|15|32.50| +|500|14|30|8|12|29.20| +|600|12|26|8|12|27.20| +|700|10|22|8|8|24.80| +|800|8|19|7|8|22.80| +|900|6|17|7|6|20.60| +|1000|6|15|6|6|19.60| +|1100|6|15|6|4|19.40| +|1200|6|15|6|4|19.40| +|1300|5|15|5|4|18.40| +|1400|5|15|5|2|18.20| +|1500|4|15|4|2|16.20| +|1600|4|13|4|2|16.20| +|1700|4|11|4|2|16.20| +|1800|4|10|4|2|16.20| +|1900|4|9|4|2|16.20| +|2000|4|8|3|2|15.20| + +In order to get a section cool, your 0-70 section time must be below the cutoff *and* no more than 2 seconds slower than your previous 0-70 time. + + +Grading +------- + + + + +### Basic grades + +Internally, the grade counter is a number that can range from 0 to 30. + +At the beginning of the game, it starts at 0. To increase it, you must bring an internal grade point counter past a certain threshold. + +The threshold is set at 50 points for the first grade, then 100 more points for the next grade, then 150 points for the grade after that, and so on. You reach the maximum level of 30 at a total of 23,250 points. + +A table of the thresholds in grade points required to reach each level is provided below: + +Grade|Threshold +-|- +0|0 +1|50 +2|150 +3|300 +4|500 +5|750 +6|1050 +7|1400 +8|1800 +9|2250 +10|2750 +11|3300 +12|3900 +13|4550 +14|5250 +15|6000 +16|6800 +17|7650 +18|8550 +19|9500 +20|10500 +21|11550 +22|12650 +23|13800 +24|15000 +25|16250 +26|17550 +27|18900 +28|20300 +29|21750 +**30**|**23250** + +Points are given according to a different scale, the point level. The point level is calculated by taking your current actual level, and adding (100 * the delay level) to it. + +The points given for clearing certain amounts of lines is given as follows: + +Level| x1 | x2 | x3 | x4 +-|-|-|-|- +0|10|20|30|40 +100|10|20|30|40 +200|10|20|30|48 +300|10|20|30|60 +400|10|20|36|72 +500|10|21|42|84 +600|10|24|48|96 +700|10|27|54|108 +800|10|30|60|120 +900|11|33|66|140 +1000|12|36|72|160 +1100|13|39|81|180 +1200|14|42|90|200 +1300|15|45|99|220 +1400|16|48|108|240 +1500|17|52|117|260 +1600|18|56|126|280 +1700|19|60|135|300 +1800|20|64|144|320 +1900|21|68|153|340 +2000|22|72|162|360 +2100|23|76|171|380 +2200|24|80|180|400 +2300|25|84|189|420 +2400|26|88|198|440 +2500|27|92|207|460 +2600|28|96|216|480 +2700|29|100|225|500 +2800|30|104|234|520 +2900|31|108|243|540 +3000|32|112|252|560 +3100|33|116|261|580 +3200|34|120|270|600 +3300|35|124|279|620 +3400|36|128|288|640 +3500|37|132|297|660 +3600|38|136|306|680 +3700|39|140|315|700 +3800|40|144|324|720 +3900|41|148|333|740 + +Past level 1000, a 4-line clear will always give (30 * current grade), regardless of point level. + +The remaining 20 grades come from section cools. Every section cool you get boosts your score by one grade. There are no regrets. + +Points are also taken away with the time it takes to lock down a piece. The delay counter starts at 0, and increases by (current grade + 2) every frame. When the counter reaches or exceeds 240, it resets to 0, and 1 grade point is taken away. + +Grades are based on the maximum grade points achieved. Once a grade has been attained, it cannot be lost even if grade points drop below the threshold for that grade. + + + +Stats +----- + +* Fewest number of lines/pieces to reach 2020: 1263 pieces / 505 lines [all Tetrises] + +* Most number of lines/pieces to reach 2020: 1448 pieces / 572 lines [all singles / doubles, full board] diff --git a/docs/new_modes/phantom_mania2.md b/docs/new_modes/phantom_mania2.md new file mode 100644 index 0000000..080fcc5 --- /dev/null +++ b/docs/new_modes/phantom_mania2.md @@ -0,0 +1,104 @@ +Phantom Mania 2 +=============== + +The Phantom Mania mode in Nullpomino is based on T.A. Death (Speed Mania), but where everything is invisible. The obvious sequel to such a mode is Phantom Mania 2, which is based on Shirase (Speed Mania 2), but again where everything is invisible. + + + +Gameplay +-------- + +The goal of this game is to reach the end at level 1300, and then score as many grades as possible in the ending credit roll. + +Each piece disappears as soon as it is placed down. Only the active piece is visible at any given time. + +Every piece placed increases the level by 1, and every line cleared also increases the level by 1, with bonuses for large numbers of lines: + +| Lines cleared | Levels advanced | +|---------------|-----------------| +| 1 | 1 | +| 2 | 2 | +| 3 | 4 | +| 4 | 6 | + +When the current level reaches one less than the level at the bottom of the display (usually a multiple of 100), the level will not advance until a line is cleared. + + +Levels +------ + +Each section is 100 levels long. At certain levels, the speed may get faster, or the difficulty may increase some other way. + +| Level | What happens | +|-------|------------| +| 500 | Rows of garbage start to advance on the board. The bottom row is copied, appearing for a brief flash before disappearing again. | +| 1000 | The garbage stops, but the hold queue turns invisible. | +| 1100 | The first next preview turns invisible. | +| 1200 | The second next preview turns invisible. | + +Unlike in Shirase mode, the speed of the game does not get faster past level 1000. + +### Torikans + +There are certain checkpoints at which your current time will be checked and you will be stopped if your time is over a set objective time. + +| Level | Time limit | +|-------|------------| +| 300 | 2:02.00 | +| 500 | 3:03.00 | +| 800 | 4:45.00 | +| 1000 | 5:38.00 | + +At each torikan, you will get a 54-second invisible roll which is worth grade points. + +Only the 1300 roll is in Big Mode. + + +Grading +------- + +Your grade starts at 1, and goes on from S1 to S9, and then M1 upwards indefinitely. + +Your grade advances each section based on your section time. If you achieve a section COOL, you get 2 grade points. If you instead incur a section REGRET, you get no grade points. The normal section time (in between COOL and REGRET) is worth one grade point. + +| Level | Section COOL | Section REGRET | +|-------|--------------|----------------| +| 0-4 | 36.00 | 48.00 | +| 5-9 | 30.00 | 39.00 | +| 10-12 | 27.00 | 35.00 | + + +Then, the invisible roll is also worth grade points. + +| Lines cleared | Grade points | +|---------------|--------------| +| 1 | 0.02 | +| 2 | 0.06 | +| 3 | 0.15 | +| 4 | 0.40 | +| Clear | 1.00 | + + +The number of grade points you earn correspond to your grade as follows: + +| Points | Grade | +|--------|--------| +| 0 | 1 | +| 1 | S1 | +| 2 | S2 | +| 3 | S3 | +| 4 | S4 | +| 5 | S5 | +| 6 | S6 | +| 7 | S7 | +| 8 | S8 | +| 9 | S9 | +| 10 | m1 | +| 11 | m2 | +| 12 | m3 | +| 13 | m4 | +| 14 | m5 | + +And so on. For *x* > 9 grade points, your grade is m(*x*-9). + +If you achieve a section COOL on each section, your rank will be m17 entering the credit roll. diff --git a/docs/new_modes/phantom_mania_comparison.md b/docs/new_modes/phantom_mania_comparison.md new file mode 100644 index 0000000..27d48e2 --- /dev/null +++ b/docs/new_modes/phantom_mania_comparison.md @@ -0,0 +1,32 @@ +Phantom Mania comparison +======================== + +## Features + +| | PM | PM2 | +|------------|-----|------| +| Max level | 999 | 1300 | +| Piece preview | 1 | 3 | +| Hold | No | Yes | +| Graded roll | No | Yes | + +## Torikans + +| Level | PM | PM2 | +|------------|-------|-------| +| 300 | 2'28" | 2'02" | +| 500 | 3'38" | 3'03" | +| 800 | 5'23" | 4'40" | +| 1000 | --- | 5'38" | + +## Speed Curve + +| Level | PM | PM2 | +|------------|-------|-------| +| 0 | 16/30 | 10/18 | +| 100 | 12/26 | 10/18 | +| 200 | 6/22 | 10/17 | +| 300 | 6/18 | 4/15 | +| 400 | 5/15 | 4/15 | +| 500 | 4/15 | 4/13 | +| 600+ | 4/15 | 4/12 | \ No newline at end of file diff --git a/docs/rulesets.md b/docs/rulesets.md new file mode 100644 index 0000000..6ff8fe8 --- /dev/null +++ b/docs/rulesets.md @@ -0,0 +1,33 @@ +Rulesets +======== + +A **ruleset** is a set of rules that apply to any game mode. + +A ruleset consists of the following things: + + * A *rotation system*, which defines how pieces move and rotate. + * A *lock delay reset system*, which defines how pieces lock when they can no longer move or rotate. + +If you're used to Nullpomino, you may notice a few things missing from that definition. For example, piece previews, hold queues, and randomizers have been moved to being game-specific rules, rather than rules that are changeable with the ruleset you use. Soft and hard drop behaviour is also game-specific now, so that times can be more plausibly compared across rulesets. + + +Rotation system +--------------- +A rotation system defines the following things: + * The block offsets of each piece orientation. + * The wall or floor kicks that will be attempted for each type of rotation. + +There are three main classes/families of rotation systems: + +* **ARIKA**, commonly known as ARS. + * **ARIKA-CLASSIC**, commonly known as Classic ARS. + * **ARIKA-TI**, commonly known as Ti-ARS, or "ARS with floorkicks". +* **STANDARD**, commonly known as SRS. + * **STANDARD**, or normal SRS. + * **STANDARD-EXP**, known as SRS-X in its original Heboris incarnation. + * **STANDARD-WORLD**, known as World Rule in TGM3. +* **CLASSIC**, commonly known as ORS or NRS (Nintendo). Also houses some traditional rotation systems. + * **CLASSIC-1989**, the no-wallkick system used by NES Tetris. + * **CLASSIC-1984**, the Electonika-60 system, where the I piece is one space higher than in CLASSIC-1989. + * **CLASSIC-SEGA**, the original Sega rotation system that spawned Arika. + * **CLASSIC-TENGEN**, the weird one with orientation problems. diff --git a/funcs.lua b/funcs.lua new file mode 100644 index 0000000..c837fc2 --- /dev/null +++ b/funcs.lua @@ -0,0 +1,55 @@ +function copy(t) + if type(t) ~= "table" then return t end + local meta = getmetatable(t) + local target = {} + for k, v in pairs(t) do target[k] = v end + setmetatable(target, meta) + return target +end + +function st(tbl) + str = "" + for k, v in pairs(tbl) do + if v == true then + str = str .. k .. " " + end + end + return str +end + +function sp(m, s, f) + if m == nil then m = 0 end + if s == nil then s = 0 end + if f == nil then f = 0 end + return m*3600 + s*60 + math.ceil(f * 0.6) +end + +function vAdd(v1, v2) + return { + x = v1.x + v2.x, + y = v1.y + v2.y + } +end + +function vNeg(v) + return { + x = -v.x, + y = -v.y + } +end + +function formatTime(frames) + if frames < 0 then return formatTime(0) end + str = string.format("%02d", math.floor(frames / 3600)) .. ":" + .. string.format("%02d", math.floor(frames / 60) % 60) .. "." + .. string.format("%02d", math.floor(frames / 0.6) % 100) + return str +end + +function formatBigNum(number) + local s = string.format("%d", number) + local pos = string.len(s) % 3 + if pos == 0 then pos = 3 end + return string.sub(s, 1, pos) + .. string.gsub(string.sub(s, pos+1), "(...)", ",%1") +end \ No newline at end of file diff --git a/libs/binser.lua b/libs/binser.lua new file mode 100644 index 0000000..421da6e --- /dev/null +++ b/libs/binser.lua @@ -0,0 +1,689 @@ +-- binser.lua + +--[[ +Copyright (c) 2016 Calvin Rose + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +]] + +local assert = assert +local error = error +local select = select +local pairs = pairs +local getmetatable = getmetatable +local setmetatable = setmetatable +local tonumber = tonumber +local type = type +local loadstring = loadstring or load +local concat = table.concat +local char = string.char +local byte = string.byte +local format = string.format +local sub = string.sub +local dump = string.dump +local floor = math.floor +local frexp = math.frexp +local unpack = unpack or table.unpack + +-- Lua 5.3 frexp polyfill +-- From https://github.com/excessive/cpml/blob/master/modules/utils.lua +if not frexp then + local log, abs, floor = math.log, math.abs, math.floor + local log2 = log(2) + frexp = function(x) + if x == 0 then return 0, 0 end + local e = floor(log(abs(x)) / log2 + 1) + return x / 2 ^ e, e + end +end + +-- NIL = 202 +-- FLOAT = 203 +-- TRUE = 204 +-- FALSE = 205 +-- STRING = 206 +-- TABLE = 207 +-- REFERENCE = 208 +-- CONSTRUCTOR = 209 +-- FUNCTION = 210 +-- RESOURCE = 211 +-- INT64 = 212 + +local mts = {} +local ids = {} +local serializers = {} +local deserializers = {} +local resources = {} +local resources_by_name = {} + +local function pack(...) + return {...}, select("#", ...) +end + +local function not_array_index(x, len) + return type(x) ~= "number" or x < 1 or x > len or x ~= floor(x) +end + +local function type_check(x, tp, name) + assert(type(x) == tp, + format("Expected parameter %q to be of type %q.", name, tp)) +end + +local bigIntSupport = false +local isInteger +if math.type then -- Detect Lua 5.3 + local mtype = math.type + bigIntSupport = loadstring[[ + local char = string.char + return function(n) + local nn = n < 0 and -(n + 1) or n + local b1 = nn // 0x100000000000000 + local b2 = nn // 0x1000000000000 % 0x100 + local b3 = nn // 0x10000000000 % 0x100 + local b4 = nn // 0x100000000 % 0x100 + local b5 = nn // 0x1000000 % 0x100 + local b6 = nn // 0x10000 % 0x100 + local b7 = nn // 0x100 % 0x100 + local b8 = nn % 0x100 + if n < 0 then + b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4 + b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8 + end + return char(212, b1, b2, b3, b4, b5, b6, b7, b8) + end]]() + isInteger = function(x) + return mtype(x) == 'integer' + end +else + isInteger = function(x) + return floor(x) == x + end +end + +-- Copyright (C) 2012-2015 Francois Perrad. +-- number serialization code modified from https://github.com/fperrad/lua-MessagePack +-- Encode a number as a big-endian ieee-754 double, big-endian signed 64 bit integer, or a small integer +local function number_to_str(n) + if isInteger(n) then -- int + if n <= 100 and n >= -27 then -- 1 byte, 7 bits of data + return char(n + 27) + elseif n <= 8191 and n >= -8192 then -- 2 bytes, 14 bits of data + n = n + 8192 + return char(128 + (floor(n / 0x100) % 0x100), n % 0x100) + elseif bigIntSupport then + return bigIntSupport(n) + end + end + local sign = 0 + if n < 0.0 then + sign = 0x80 + n = -n + end + local m, e = frexp(n) -- mantissa, exponent + if m ~= m then + return char(203, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + elseif m == 1/0 then + if sign == 0 then + return char(203, 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + else + return char(203, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + end + end + e = e + 0x3FE + if e < 1 then -- denormalized numbers + m = m * 2 ^ (52 + e) + e = 0 + else + m = (m * 2 - 1) * 2 ^ 52 + end + return char(203, + sign + floor(e / 0x10), + (e % 0x10) * 0x10 + floor(m / 0x1000000000000), + floor(m / 0x10000000000) % 0x100, + floor(m / 0x100000000) % 0x100, + floor(m / 0x1000000) % 0x100, + floor(m / 0x10000) % 0x100, + floor(m / 0x100) % 0x100, + m % 0x100) +end + +-- Copyright (C) 2012-2015 Francois Perrad. +-- number deserialization code also modified from https://github.com/fperrad/lua-MessagePack +local function number_from_str(str, index) + local b = byte(str, index) + if b < 128 then + return b - 27, index + 1 + elseif b < 192 then + return byte(str, index + 1) + 0x100 * (b - 128) - 8192, index + 2 + end + local b1, b2, b3, b4, b5, b6, b7, b8 = byte(str, index + 1, index + 8) + if b == 212 then + local flip = b1 >= 128 + if flip then -- negative + b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4 + b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8 + end + local n = ((((((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8 + if flip then + return (-n) - 1, index + 9 + else + return n, index + 9 + end + end + local sign = b1 > 0x7F and -1 or 1 + local e = (b1 % 0x80) * 0x10 + floor(b2 / 0x10) + local m = ((((((b2 % 0x10) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8 + local n + if e == 0 then + if m == 0 then + n = sign * 0.0 + else + n = sign * (m / 2 ^ 52) * 2 ^ -1022 + end + elseif e == 0x7FF then + if m == 0 then + n = sign * (1/0) + else + n = 0.0/0.0 + end + else + n = sign * (1.0 + m / 2 ^ 52) * 2 ^ (e - 0x3FF) + end + return n, index + 9 +end + +local types = {} + +types["nil"] = function(x, visited, accum) + accum[#accum + 1] = "\202" +end + +function types.number(x, visited, accum) + accum[#accum + 1] = number_to_str(x) +end + +function types.boolean(x, visited, accum) + accum[#accum + 1] = x and "\204" or "\205" +end + +function types.string(x, visited, accum) + local alen = #accum + if visited[x] then + accum[alen + 1] = "\208" + accum[alen + 2] = number_to_str(visited[x]) + else + visited[x] = visited.next + visited.next = visited.next + 1 + accum[alen + 1] = "\206" + accum[alen + 2] = number_to_str(#x) + accum[alen + 3] = x + end +end + +local function check_custom_type(x, visited, accum) + local res = resources[x] + if res then + accum[#accum + 1] = "\211" + types[type(res)](res, visited, accum) + return true + end + local mt = getmetatable(x) + local id = mt and ids[mt] + if id then + if x == visited.temp then + error("Infinite loop in constructor.") + end + visited.temp = x + accum[#accum + 1] = "\209" + types[type(id)](id, visited, accum) + local args, len = pack(serializers[id](x)) + accum[#accum + 1] = number_to_str(len) + for i = 1, len do + local arg = args[i] + types[type(arg)](arg, visited, accum) + end + visited[x] = visited.next + visited.next = visited.next + 1 + return true + end +end + +function types.userdata(x, visited, accum) + if visited[x] then + accum[#accum + 1] = "\208" + accum[#accum + 1] = number_to_str(visited[x]) + else + if check_custom_type(x, visited, accum) then return end + error("Cannot serialize this userdata.") + end +end + +function types.table(x, visited, accum) + if visited[x] then + accum[#accum + 1] = "\208" + accum[#accum + 1] = number_to_str(visited[x]) + else + if check_custom_type(x, visited, accum) then return end + visited[x] = visited.next + visited.next = visited.next + 1 + local xlen = #x + accum[#accum + 1] = "\207" + accum[#accum + 1] = number_to_str(xlen) + for i = 1, xlen do + local v = x[i] + types[type(v)](v, visited, accum) + end + local key_count = 0 + for k in pairs(x) do + if not_array_index(k, xlen) then + key_count = key_count + 1 + end + end + accum[#accum + 1] = number_to_str(key_count) + for k, v in pairs(x) do + if not_array_index(k, xlen) then + types[type(k)](k, visited, accum) + types[type(v)](v, visited, accum) + end + end + end +end + +types["function"] = function(x, visited, accum) + if visited[x] then + accum[#accum + 1] = "\208" + accum[#accum + 1] = number_to_str(visited[x]) + else + if check_custom_type(x, visited, accum) then return end + visited[x] = visited.next + visited.next = visited.next + 1 + local str = dump(x) + accum[#accum + 1] = "\210" + accum[#accum + 1] = number_to_str(#str) + accum[#accum + 1] = str + end +end + +types.cdata = function(x, visited, accum) + if visited[x] then + accum[#accum + 1] = "\208" + accum[#accum + 1] = number_to_str(visited[x]) + else + if check_custom_type(x, visited, #accum) then return end + error("Cannot serialize this cdata.") + end +end + +types.thread = function() error("Cannot serialize threads.") end + +local function deserialize_value(str, index, visited) + local t = byte(str, index) + if not t then return end + if t < 128 then + return t - 27, index + 1 + elseif t < 192 then + return byte(str, index + 1) + 0x100 * (t - 128) - 8192, index + 2 + elseif t == 202 then + return nil, index + 1 + elseif t == 203 then + return number_from_str(str, index) + elseif t == 204 then + return true, index + 1 + elseif t == 205 then + return false, index + 1 + elseif t == 206 then + local length, dataindex = deserialize_value(str, index + 1, visited) + local nextindex = dataindex + length + local substr = sub(str, dataindex, nextindex - 1) + visited[#visited + 1] = substr + return substr, nextindex + elseif t == 207 then + local count, nextindex = number_from_str(str, index + 1) + local ret = {} + visited[#visited + 1] = ret + for i = 1, count do + ret[i], nextindex = deserialize_value(str, nextindex, visited) + end + count, nextindex = number_from_str(str, nextindex) + for i = 1, count do + local k, v + k, nextindex = deserialize_value(str, nextindex, visited) + v, nextindex = deserialize_value(str, nextindex, visited) + ret[k] = v + end + return ret, nextindex + elseif t == 208 then + local ref, nextindex = number_from_str(str, index + 1) + return visited[ref], nextindex + elseif t == 209 then + local count + local name, nextindex = deserialize_value(str, index + 1, visited) + count, nextindex = number_from_str(str, nextindex) + local args = {} + for i = 1, count do + args[i], nextindex = deserialize_value(str, nextindex, visited) + end + local ret = deserializers[name](unpack(args)) + visited[#visited + 1] = ret + return ret, nextindex + elseif t == 210 then + local length, dataindex = deserialize_value(str, index + 1, visited) + local nextindex = dataindex + length + local ret = loadstring(sub(str, dataindex, nextindex - 1)) + visited[#visited + 1] = ret + return ret, nextindex + elseif t == 211 then + local res, nextindex = deserialize_value(str, index + 1, visited) + return resources_by_name[res], nextindex + elseif t == 212 then + return number_from_str(str, index) + else + error("Could not deserialize type byte " .. t .. ".") + end +end + +local function serialize(...) + local visited = {next = 1} + local accum = {} + for i = 1, select("#", ...) do + local x = select(i, ...) + types[type(x)](x, visited, accum) + end + return concat(accum) +end + +local function make_file_writer(file) + return setmetatable({}, { + __newindex = function(_, _, v) + file:write(v) + end + }) +end + +local function serialize_to_file(path, mode, ...) + local file, err = io.open(path, mode) + assert(file, err) + local visited = {next = 1} + local accum = make_file_writer(file) + for i = 1, select("#", ...) do + local x = select(i, ...) + types[type(x)](x, visited, accum) + end + -- flush the writer + file:flush() + file:close() +end + +local function writeFile(path, ...) + return serialize_to_file(path, "wb", ...) +end + +local function appendFile(path, ...) + return serialize_to_file(path, "ab", ...) +end + +local function deserialize(str, index) + assert(type(str) == "string", "Expected string to deserialize.") + local vals = {} + index = index or 1 + local visited = {} + local len = 0 + local val + while index do + val, index = deserialize_value(str, index, visited) + if index then + len = len + 1 + vals[len] = val + end + end + return vals, len +end + +local function deserializeN(str, n, index) + assert(type(str) == "string", "Expected string to deserialize.") + n = n or 1 + assert(type(n) == "number", "Expected a number for parameter n.") + assert(n > 0 and floor(n) == n, "N must be a poitive integer.") + local vals = {} + index = index or 1 + local visited = {} + local len = 0 + local val + while index and len < n do + val, index = deserialize_value(str, index, visited) + if index then + len = len + 1 + vals[len] = val + end + end + vals[len + 1] = index + return unpack(vals, 1, n + 1) +end + +local function readFile(path) + local file, err = io.open(path, "rb") + if file == nil then + return nil, 0 + end + local str = file:read("*all") + file:close() + return deserialize(str) +end + +local function default_deserialize(metatable) + return function(...) + local ret = {} + for i = 1, select("#", ...), 2 do + ret[select(i, ...)] = select(i + 1, ...) + end + return setmetatable(ret, metatable) + end +end + +local function default_serialize(x) + assert(type(x) == "table", + "Default serialization for custom types only works for tables.") + local args = {} + local len = 0 + for k, v in pairs(x) do + args[len + 1], args[len + 2] = k, v + len = len + 2 + end + return unpack(args, 1, len) +end + +-- Templating + +local function normalize_template(template) + local ret = {} + for i = 1, #template do + ret[i] = template[i] + end + local non_array_part = {} + -- The non-array part of the template (nested templates) have to be deterministic, so they are sorted. + -- This means that inherently non deterministicly sortable keys (tables, functions) should NOT be used + -- in templates. Looking for way around this. + for k in pairs(template) do + if not_array_index(k, #template) then + non_array_part[#non_array_part + 1] = k + end + end + table.sort(non_array_part) + for i = 1, #non_array_part do + local name = non_array_part[i] + ret[#ret + 1] = {name, normalize_template(template[name])} + end + return ret +end + +local function templatepart_serialize(part, argaccum, x, len) + local extras = {} + local extracount = 0 + for k, v in pairs(x) do + extras[k] = v + extracount = extracount + 1 + end + for i = 1, #part do + extracount = extracount - 1 + if type(part[i]) == "table" then + extras[part[i][1]] = nil + len = templatepart_serialize(part[i][2], argaccum, x[part[i][1]], len) + else + extras[part[i]] = nil + len = len + 1 + argaccum[len] = x[part[i]] + end + end + if extracount > 0 then + argaccum[len + 1] = extras + else + argaccum[len + 1] = nil + end + return len + 1 +end + +local function templatepart_deserialize(ret, part, values, vindex) + for i = 1, #part do + local name = part[i] + if type(name) == "table" then + local newret = {} + ret[name[1]] = newret + vindex = templatepart_deserialize(newret, name[2], values, vindex) + else + ret[name] = values[vindex] + vindex = vindex + 1 + end + end + local extras = values[vindex] + if extras then + for k, v in pairs(extras) do + ret[k] = v + end + end + return vindex + 1 +end + +local function template_serializer_and_deserializer(metatable, template) + return function(x) + argaccum = {} + local len = templatepart_serialize(template, argaccum, x, 0) + return unpack(argaccum, 1, len) + end, function(...) + local ret = {} + local len = select("#", ...) + local args = {...} + templatepart_deserialize(ret, template, args, 1) + return setmetatable(ret, metatable) + end +end + +local function register(metatable, name, serialize, deserialize) + name = name or metatable.name + serialize = serialize or metatable._serialize + deserialize = deserialize or metatable._deserialize + if not serialize then + if metatable._template then + local t = normalize_template(metatable._template) + serialize, deserialize = template_serializer_and_deserializer(metatable, t) + elseif not deserialize then + serialize = default_serialize + deserialize = default_deserialize(metatable) + else + serialize = metatable + end + end + type_check(metatable, "table", "metatable") + type_check(name, "string", "name") + type_check(serialize, "function", "serialize") + type_check(deserialize, "function", "deserialize") + assert(not ids[metatable], "Metatable already registered.") + assert(not mts[name], ("Name %q already registered."):format(name)) + mts[name] = metatable + ids[metatable] = name + serializers[name] = serialize + deserializers[name] = deserialize + return metatable +end + +local function unregister(item) + local name, metatable + if type(item) == "string" then -- assume name + name, metatable = item, mts[item] + else -- assume metatable + name, metatable = ids[item], item + end + type_check(name, "string", "name") + type_check(metatable, "table", "metatable") + mts[name] = nil + ids[metatable] = nil + serializers[name] = nil + deserializers[name] = nil + return metatable +end + +local function registerClass(class, name) + name = name or class.name + if class.__instanceDict then -- middleclass + register(class.__instanceDict, name) + else -- assume 30log or similar library + register(class, name) + end + return class +end + +local function registerResource(resource, name) + type_check(name, "string", "name") + assert(not resources[resource], + "Resource already registered.") + assert(not resources_by_name[name], + format("Resource %q already exists.", name)) + resources_by_name[name] = resource + resources[resource] = name + return resource +end + +local function unregisterResource(name) + type_check(name, "string", "name") + assert(resources_by_name[name], format("Resource %q does not exist.", name)) + local resource = resources_by_name[name] + resources_by_name[name] = nil + resources[resource] = nil + return resource +end + +return { + -- aliases + s = serialize, + d = deserialize, + dn = deserializeN, + r = readFile, + w = writeFile, + a = appendFile, + + serialize = serialize, + deserialize = deserialize, + deserializeN = deserializeN, + readFile = readFile, + writeFile = writeFile, + appendFile = appendFile, + register = register, + unregister = unregister, + registerResource = registerResource, + unregisterResource = unregisterResource, + registerClass = registerClass +} diff --git a/libs/classic.lua b/libs/classic.lua new file mode 100644 index 0000000..cbd6f81 --- /dev/null +++ b/libs/classic.lua @@ -0,0 +1,68 @@ +-- +-- classic +-- +-- Copyright (c) 2014, rxi +-- +-- This module is free software; you can redistribute it and/or modify it under +-- the terms of the MIT license. See LICENSE for details. +-- + + +local Object = {} +Object.__index = Object + + +function Object:new() +end + + +function Object:extend() + local cls = {} + for k, v in pairs(self) do + if k:find("__") == 1 then + cls[k] = v + end + end + cls.__index = cls + cls.super = self + setmetatable(cls, self) + return cls +end + + +function Object:implement(...) + for _, cls in pairs({...}) do + for k, v in pairs(cls) do + if self[k] == nil and type(v) == "function" then + self[k] = v + end + end + end +end + + +function Object:is(T) + local mt = getmetatable(self) + while mt do + if mt == T then + return true + end + mt = getmetatable(mt) + end + return false +end + + +function Object:__tostring() + return "Object" +end + + +function Object:__call(...) + local obj = setmetatable({}, self) + obj:new(...) + return obj +end + + +return Object diff --git a/load/bgm.lua b/load/bgm.lua new file mode 100644 index 0000000..560c649 --- /dev/null +++ b/load/bgm.lua @@ -0,0 +1,76 @@ +bgm = { + credit_roll = { + gm3 = love.audio.newSource("res/bgm/tgm_credit_roll.mp3", "stream"), + }, + pacer_test = love.audio.newSource("res/bgm/pacer_test.mp3", "stream"), +} + +local current_bgm = nil +local bgm_locked = false + +function switchBGM(sound, subsound) + if bgm_locked then return end + if current_bgm ~= nil then + current_bgm:stop() + end + if subsound ~= nil then + current_bgm = bgm[sound][subsound] + resetBGMFadeout() + elseif sound ~= nil then + current_bgm = bgm[sound] + resetBGMFadeout() + else + current_bgm = nil + end +end + +function switchBGMLoop(sound, subsound) + if bgm_locked then return end + switchBGM(sound, subsound) + current_bgm:setLooping(true) +end + +function lockBGM() + bgm_locked = true +end + +local fading_bgm = false +local fadeout_time = 0 +local total_fadeout_time = 0 + +function fadeoutBGM(time) + if fading_bgm == false then + fading_bgm = true + fadeout_time = time + total_fadeout_time = time + end +end + +function resetBGMFadeout(time) + current_bgm:setVolume(1) + fading_bgm = false + current_bgm:play() +end + +function processBGMFadeout(dt) + if fading_bgm then + fadeout_time = fadeout_time - dt + if fadeout_time < 0 then + fadeout_time = 0 + fading_bgm = false + end + current_bgm:setVolume(fadeout_time / total_fadeout_time) + end +end + +function pauseBGM() + if current_bgm ~= nil then + current_bgm:pause() + end +end + +function resumeBGM() + if current_bgm ~= nil then + current_bgm:play() + end +end diff --git a/load/fonts.lua b/load/fonts.lua new file mode 100644 index 0000000..36ae0c3 --- /dev/null +++ b/load/fonts.lua @@ -0,0 +1,33 @@ +font_3x5 = love.graphics.newImageFont( + "res/fonts/3x5.png", + " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" .. + "`abcdefghijklmnopqrstuvwxyz{|}~™", + -1 +) + +font_3x5_2 = love.graphics.newImageFont( + "res/fonts/3x5_double.png", + " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" .. + "`abcdefghijklmnopqrstuvwxyz{|}~™", + -2 +) + +font_3x5_3 = love.graphics.newImageFont( + "res/fonts/3x5_medium.png", + " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" .. + "`abcdefghijklmnopqrstuvwxyz{|}~", + -3 +) + +font_3x5_4 = love.graphics.newImageFont( + "res/fonts/3x5_large.png", + " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" .. + "`abcdefghijklmnopqrstuvwxyz{|}~", + -4 +) + +font_8x11 = love.graphics.newImageFont( + "res/fonts/8x11_medium.png", + "0123456789:.", + 1 +) diff --git a/load/graphics.lua b/load/graphics.lua new file mode 100644 index 0000000..a51f38d --- /dev/null +++ b/load/graphics.lua @@ -0,0 +1,58 @@ +backgrounds = { + [0] = love.graphics.newImage("res/backgrounds/0-quantum-foam.png"), + love.graphics.newImage("res/backgrounds/100-big-bang.png"), + love.graphics.newImage("res/backgrounds/200-spiral-galaxy.png"), + love.graphics.newImage("res/backgrounds/300-sun-and-dust.png"), + love.graphics.newImage("res/backgrounds/400-earth-and-moon.png"), + love.graphics.newImage("res/backgrounds/500-cambrian-explosion.png"), + love.graphics.newImage("res/backgrounds/600-dinosaurs.png"), + love.graphics.newImage("res/backgrounds/700-asteroid.png"), + love.graphics.newImage("res/backgrounds/800-human-fire.png"), + love.graphics.newImage("res/backgrounds/900-early-civilization.png"), + love.graphics.newImage("res/backgrounds/1000-vikings.png"), + love.graphics.newImage("res/backgrounds/1100-crusades.png"), + love.graphics.newImage("res/backgrounds/1200-genghis-khan.png"), + love.graphics.newImage("res/backgrounds/1300-black-death.png"), + love.graphics.newImage("res/backgrounds/1400-columbus-discovery.png"), + love.graphics.newImage("res/backgrounds/1500-aztecas.png"), + love.graphics.newImage("res/backgrounds/1600-telescope.png"), + love.graphics.newImage("res/backgrounds/1700-american-revolution.png"), + love.graphics.newImage("res/backgrounds/1800-railways.png"), + love.graphics.newImage("res/backgrounds/1900-world-wide-web.png"), + title = love.graphics.newImage("res/backgrounds/title_v0.1.png"), +} + +blocks = { + ["2tie"] = { + I = love.graphics.newImage("res/img/s1.png"), + J = love.graphics.newImage("res/img/s4.png"), + L = love.graphics.newImage("res/img/s3.png"), + O = love.graphics.newImage("res/img/s7.png"), + S = love.graphics.newImage("res/img/s5.png"), + T = love.graphics.newImage("res/img/s2.png"), + Z = love.graphics.newImage("res/img/s6.png"), + F = love.graphics.newImage("res/img/s9.png"), + G = love.graphics.newImage("res/img/s9.png"), + X = love.graphics.newImage("res/img/s9.png"), + }, + ["bone"] = { + I = love.graphics.newImage("res/img/bone.png"), + J = love.graphics.newImage("res/img/bone.png"), + L = love.graphics.newImage("res/img/bone.png"), + O = love.graphics.newImage("res/img/bone.png"), + S = love.graphics.newImage("res/img/bone.png"), + T = love.graphics.newImage("res/img/bone.png"), + Z = love.graphics.newImage("res/img/bone.png"), + F = love.graphics.newImage("res/img/bone.png"), + G = love.graphics.newImage("res/img/bone.png"), + X = love.graphics.newImage("res/img/bone.png"), + } +} + +misc_graphics = { + frame = love.graphics.newImage("res/img/frame.png"), + ready = love.graphics.newImage("res/img/ready.png"), + go = love.graphics.newImage("res/img/go.png"), + select_mode = love.graphics.newImage("res/img/select_mode.png"), + strike = love.graphics.newImage("res/img/strike.png"), +} diff --git a/load/save.lua b/load/save.lua new file mode 100644 index 0000000..92cdb68 --- /dev/null +++ b/load/save.lua @@ -0,0 +1,24 @@ +local binser = require 'libs.binser' + +function loadSave() + config = loadFromFile('config.sav') + highscores = loadFromFile('highscores.sav') +end + +function loadFromFile(filename) + local save_data, len = binser.readFile(filename) + if save_data == nil then + return {} -- new object + end + return save_data[1] +end + + + +function saveConfig() + binser.writeFile('config.sav', config) +end + +function saveHighscores() + binser.writeFile('highscores.sav', highscores) +end diff --git a/load/sounds.lua b/load/sounds.lua new file mode 100644 index 0000000..6da3486 --- /dev/null +++ b/load/sounds.lua @@ -0,0 +1,29 @@ +sounds = { + blocks = { + I = love.audio.newSource("res/se/piece_i.wav", "static"), + J = love.audio.newSource("res/se/piece_j.wav", "static"), + L = love.audio.newSource("res/se/piece_l.wav", "static"), + O = love.audio.newSource("res/se/piece_o.wav", "static"), + S = love.audio.newSource("res/se/piece_s.wav", "static"), + T = love.audio.newSource("res/se/piece_t.wav", "static"), + Z = love.audio.newSource("res/se/piece_z.wav", "static") + }, + move = love.audio.newSource("res/se/move.wav", "static"), + bottom = love.audio.newSource("res/se/bottom.wav", "static"), +} + +function playSE(sound, subsound) + if subsound == nil then + sounds[sound]:setVolume(0.1) + if sounds[sound]:isPlaying() then + sounds[sound]:stop() + end + sounds[sound]:play() + else + sounds[sound][subsound]:setVolume(0.1) + if sounds[sound][subsound]:isPlaying() then + sounds[sound][subsound]:stop() + end + sounds[sound][subsound]:play() + end +end diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..d60f6d8 --- /dev/null +++ b/main.lua @@ -0,0 +1,96 @@ +function love.load() + math.randomseed(os.time()) + highscores = {} + require "load.graphics" + require "load.fonts" + require "load.sounds" + require "load.bgm" + require "load.save" + loadSave() + require "scene" + config["side_next"] = false + config["reverse_rotate"] = true + config["fullscreen"] = false + if not config.input then + config.input = {} + scene = InputConfigScene() + else + if config.current_mode then current_mode = config.current_mode end + if config.current_ruleset then current_ruleset = config.current_ruleset end + scene = TitleScene() + end +end + +local TARGET_FPS = 60 +local SAMPLE_SIZE = 60 + +local rolling_samples = {} +local rolling_total = 0 +local average_n = 0 +local frame = 0 + +function getSmoothedDt(dt) + rolling_total = rolling_total + dt + frame = frame + 1 + if frame > SAMPLE_SIZE then frame = frame - SAMPLE_SIZE end + if average_n == SAMPLE_SIZE then + rolling_total = rolling_total - rolling_samples[frame] + else + average_n = average_n + 1 + end + rolling_samples[frame] = dt + return rolling_total / average_n +end + +local update_time = 0.52 + +function love.update(dt) + processBGMFadeout(dt) + local old_update_time = update_time + update_time = update_time + getSmoothedDt(dt) * TARGET_FPS + updates = 0 + while (update_time >= 1.02) do + scene:update() + updates = updates + 1 + update_time = update_time - 1 + end + if math.abs(update_time - old_update_time) < 0.02 then + update_time = old_update_time + end +end + +function love.draw() + love.graphics.push() + if love.window.getFullscreen() then + -- get offset matrix + love.graphics.setDefaultFilter("linear", "nearest") + local width = love.graphics.getWidth() + local height = love.graphics.getHeight() + local scale_factor = math.min(width / 640, height / 480) + love.graphics.translate( + (width - scale_factor * 640) / 2, + (height - scale_factor * 480) / 2 + ) + love.graphics.scale(scale_factor) + end + scene:render() + love.graphics.pop() +end + +function love.keypressed(key, scancode, isrepeat) + -- global hotkeys + if scancode == "f4" then + config["fullscreen"] = not config["fullscreen"] + love.window.setFullscreen(config["fullscreen"]) + else + scene:onKeyPress({key=key, scancode=scancode, isRepeat=isrepeat}) + end +end + +function love.focus(f) + if f then + resumeBGM() + else + pauseBGM() + end +end diff --git a/package b/package new file mode 100755 index 0000000..ed6f662 --- /dev/null +++ b/package @@ -0,0 +1 @@ +zip -r cambridge.love libs load res scene tetris conf.lua main.lua scene.lua funcs.lua diff --git a/release b/release new file mode 100755 index 0000000..b1dcee3 --- /dev/null +++ b/release @@ -0,0 +1,9 @@ +./package +mkdir dist +mkdir dist/windows +mkdir dist/win32 +cp cambridge.love dist/ +cat dist/windows/love.exe cambridge.love > dist/windows/cambridge.exe +zip dist/cambridge-windows.zip dist/windows/* SOURCES.md LICENSE +cat dist/win32/love.exe cambridge.love > dist/win32/cambridge.exe +zip dist/cambridge-win32.zip dist/win32/* SOURCES.md LICENSE diff --git a/res/backgrounds/0-quantum-foam.png b/res/backgrounds/0-quantum-foam.png new file mode 100755 index 0000000..b98018f Binary files /dev/null and b/res/backgrounds/0-quantum-foam.png differ diff --git a/res/backgrounds/100-big-bang.png b/res/backgrounds/100-big-bang.png new file mode 100755 index 0000000..f6a31eb Binary files /dev/null and b/res/backgrounds/100-big-bang.png differ diff --git a/res/backgrounds/1000-vikings.png b/res/backgrounds/1000-vikings.png new file mode 100755 index 0000000..eba1b66 Binary files /dev/null and b/res/backgrounds/1000-vikings.png differ diff --git a/res/backgrounds/1100-crusades.png b/res/backgrounds/1100-crusades.png new file mode 100755 index 0000000..5ebf439 Binary files /dev/null and b/res/backgrounds/1100-crusades.png differ diff --git a/res/backgrounds/1200-genghis-khan.png b/res/backgrounds/1200-genghis-khan.png new file mode 100755 index 0000000..d5a46c5 Binary files /dev/null and b/res/backgrounds/1200-genghis-khan.png differ diff --git a/res/backgrounds/1300-black-death.png b/res/backgrounds/1300-black-death.png new file mode 100755 index 0000000..02107e0 Binary files /dev/null and b/res/backgrounds/1300-black-death.png differ diff --git a/res/backgrounds/1400-columbus-discovery.png b/res/backgrounds/1400-columbus-discovery.png new file mode 100755 index 0000000..c20eb22 Binary files /dev/null and b/res/backgrounds/1400-columbus-discovery.png differ diff --git a/res/backgrounds/1500-aztecas.png b/res/backgrounds/1500-aztecas.png new file mode 100755 index 0000000..a650fb8 Binary files /dev/null and b/res/backgrounds/1500-aztecas.png differ diff --git a/res/backgrounds/1600-telescope.png b/res/backgrounds/1600-telescope.png new file mode 100755 index 0000000..4ad327d Binary files /dev/null and b/res/backgrounds/1600-telescope.png differ diff --git a/res/backgrounds/1700-american-revolution.png b/res/backgrounds/1700-american-revolution.png new file mode 100755 index 0000000..1a40145 Binary files /dev/null and b/res/backgrounds/1700-american-revolution.png differ diff --git a/res/backgrounds/1800-railways.png b/res/backgrounds/1800-railways.png new file mode 100755 index 0000000..ae73d08 Binary files /dev/null and b/res/backgrounds/1800-railways.png differ diff --git a/res/backgrounds/1900-world-wide-web.png b/res/backgrounds/1900-world-wide-web.png new file mode 100755 index 0000000..7b3678b Binary files /dev/null and b/res/backgrounds/1900-world-wide-web.png differ diff --git a/res/backgrounds/200-spiral-galaxy.png b/res/backgrounds/200-spiral-galaxy.png new file mode 100755 index 0000000..c819014 Binary files /dev/null and b/res/backgrounds/200-spiral-galaxy.png differ diff --git a/res/backgrounds/300-sun-and-dust.png b/res/backgrounds/300-sun-and-dust.png new file mode 100755 index 0000000..e7f5c30 Binary files /dev/null and b/res/backgrounds/300-sun-and-dust.png differ diff --git a/res/backgrounds/400-earth-and-moon.png b/res/backgrounds/400-earth-and-moon.png new file mode 100755 index 0000000..13a792d Binary files /dev/null and b/res/backgrounds/400-earth-and-moon.png differ diff --git a/res/backgrounds/500-cambrian-explosion.png b/res/backgrounds/500-cambrian-explosion.png new file mode 100755 index 0000000..05f4b3e Binary files /dev/null and b/res/backgrounds/500-cambrian-explosion.png differ diff --git a/res/backgrounds/600-dinosaurs.png b/res/backgrounds/600-dinosaurs.png new file mode 100755 index 0000000..57fe589 Binary files /dev/null and b/res/backgrounds/600-dinosaurs.png differ diff --git a/res/backgrounds/700-asteroid.png b/res/backgrounds/700-asteroid.png new file mode 100755 index 0000000..5db620e Binary files /dev/null and b/res/backgrounds/700-asteroid.png differ diff --git a/res/backgrounds/800-human-fire.png b/res/backgrounds/800-human-fire.png new file mode 100755 index 0000000..743da83 Binary files /dev/null and b/res/backgrounds/800-human-fire.png differ diff --git a/res/backgrounds/900-early-civilization.png b/res/backgrounds/900-early-civilization.png new file mode 100755 index 0000000..6920945 Binary files /dev/null and b/res/backgrounds/900-early-civilization.png differ diff --git a/res/backgrounds/title_v0.1.png b/res/backgrounds/title_v0.1.png new file mode 100644 index 0000000..065b50d Binary files /dev/null and b/res/backgrounds/title_v0.1.png differ diff --git a/res/bgm/highscores.wav b/res/bgm/highscores.wav new file mode 100644 index 0000000..4bbda8e Binary files /dev/null and b/res/bgm/highscores.wav differ diff --git a/res/bgm/pacer_test.mp3 b/res/bgm/pacer_test.mp3 new file mode 100644 index 0000000..7d2b5f8 Binary files /dev/null and b/res/bgm/pacer_test.mp3 differ diff --git a/res/bgm/tgm_credit_roll.mp3 b/res/bgm/tgm_credit_roll.mp3 new file mode 100644 index 0000000..38b9c3a Binary files /dev/null and b/res/bgm/tgm_credit_roll.mp3 differ diff --git a/res/fonts/3x5.png b/res/fonts/3x5.png new file mode 100644 index 0000000..866cc91 Binary files /dev/null and b/res/fonts/3x5.png differ diff --git a/res/fonts/3x5.xcf b/res/fonts/3x5.xcf new file mode 100644 index 0000000..831587b Binary files /dev/null and b/res/fonts/3x5.xcf differ diff --git a/res/fonts/3x5_double.png b/res/fonts/3x5_double.png new file mode 100644 index 0000000..2e75c87 Binary files /dev/null and b/res/fonts/3x5_double.png differ diff --git a/res/fonts/3x5_large.png b/res/fonts/3x5_large.png new file mode 100644 index 0000000..5a08b92 Binary files /dev/null and b/res/fonts/3x5_large.png differ diff --git a/res/fonts/3x5_medium.png b/res/fonts/3x5_medium.png new file mode 100644 index 0000000..cbb51ff Binary files /dev/null and b/res/fonts/3x5_medium.png differ diff --git a/res/fonts/8x11.png b/res/fonts/8x11.png new file mode 100644 index 0000000..2693141 Binary files /dev/null and b/res/fonts/8x11.png differ diff --git a/res/fonts/8x11_medium.png b/res/fonts/8x11_medium.png new file mode 100644 index 0000000..54d170b Binary files /dev/null and b/res/fonts/8x11_medium.png differ diff --git a/res/fonts/8x12.xcf b/res/fonts/8x12.xcf new file mode 100644 index 0000000..746c877 Binary files /dev/null and b/res/fonts/8x12.xcf differ diff --git a/res/img/bone.png b/res/img/bone.png new file mode 100644 index 0000000..fd78d35 Binary files /dev/null and b/res/img/bone.png differ diff --git a/res/img/frame.png b/res/img/frame.png new file mode 100644 index 0000000..38fd30c Binary files /dev/null and b/res/img/frame.png differ diff --git a/res/img/go.png b/res/img/go.png new file mode 100644 index 0000000..02f9658 Binary files /dev/null and b/res/img/go.png differ diff --git a/res/img/ready.png b/res/img/ready.png new file mode 100644 index 0000000..91e01cd Binary files /dev/null and b/res/img/ready.png differ diff --git a/res/img/s1.png b/res/img/s1.png new file mode 100644 index 0000000..be247be Binary files /dev/null and b/res/img/s1.png differ diff --git a/res/img/s2.png b/res/img/s2.png new file mode 100644 index 0000000..19e9c2d Binary files /dev/null and b/res/img/s2.png differ diff --git a/res/img/s3.png b/res/img/s3.png new file mode 100644 index 0000000..4758d21 Binary files /dev/null and b/res/img/s3.png differ diff --git a/res/img/s4.png b/res/img/s4.png new file mode 100644 index 0000000..642d662 Binary files /dev/null and b/res/img/s4.png differ diff --git a/res/img/s5.png b/res/img/s5.png new file mode 100644 index 0000000..9bfbc6f Binary files /dev/null and b/res/img/s5.png differ diff --git a/res/img/s6.png b/res/img/s6.png new file mode 100644 index 0000000..6688b63 Binary files /dev/null and b/res/img/s6.png differ diff --git a/res/img/s7.png b/res/img/s7.png new file mode 100644 index 0000000..d9cf80b Binary files /dev/null and b/res/img/s7.png differ diff --git a/res/img/s9.png b/res/img/s9.png new file mode 100644 index 0000000..c08aee2 Binary files /dev/null and b/res/img/s9.png differ diff --git a/res/img/select_mode.png b/res/img/select_mode.png new file mode 100644 index 0000000..18a5421 Binary files /dev/null and b/res/img/select_mode.png differ diff --git a/res/img/strike.png b/res/img/strike.png new file mode 100644 index 0000000..eda2b97 Binary files /dev/null and b/res/img/strike.png differ diff --git a/res/img/torikan.png b/res/img/torikan.png new file mode 100644 index 0000000..269de1b Binary files /dev/null and b/res/img/torikan.png differ diff --git a/res/se/bottom.wav b/res/se/bottom.wav new file mode 100644 index 0000000..73a804f Binary files /dev/null and b/res/se/bottom.wav differ diff --git a/res/se/move.wav b/res/se/move.wav new file mode 100644 index 0000000..6d4adb6 Binary files /dev/null and b/res/se/move.wav differ diff --git a/res/se/piece_i.wav b/res/se/piece_i.wav new file mode 100644 index 0000000..a155c9d Binary files /dev/null and b/res/se/piece_i.wav differ diff --git a/res/se/piece_j.wav b/res/se/piece_j.wav new file mode 100644 index 0000000..158365e Binary files /dev/null and b/res/se/piece_j.wav differ diff --git a/res/se/piece_l.wav b/res/se/piece_l.wav new file mode 100644 index 0000000..148a82f Binary files /dev/null and b/res/se/piece_l.wav differ diff --git a/res/se/piece_o.wav b/res/se/piece_o.wav new file mode 100644 index 0000000..26dab85 Binary files /dev/null and b/res/se/piece_o.wav differ diff --git a/res/se/piece_s.wav b/res/se/piece_s.wav new file mode 100644 index 0000000..3e23b06 Binary files /dev/null and b/res/se/piece_s.wav differ diff --git a/res/se/piece_t.wav b/res/se/piece_t.wav new file mode 100644 index 0000000..07da609 Binary files /dev/null and b/res/se/piece_t.wav differ diff --git a/res/se/piece_z.wav b/res/se/piece_z.wav new file mode 100644 index 0000000..469e39c Binary files /dev/null and b/res/se/piece_z.wav differ diff --git a/scene.lua b/scene.lua new file mode 100644 index 0000000..8711e1b --- /dev/null +++ b/scene.lua @@ -0,0 +1,14 @@ +local Object = require "libs.classic" + +Scene = Object:extend() + +function Scene:new() end +function Scene:update() end +function Scene:render() end +function Scene:onKeyPress() end + +GameScene = require "scene.game" +ModeSelectScene = require "scene.mode_select" +InputConfigScene = require "scene.input_config" +ConfigScene = require "scene.config" +TitleScene = require "scene.title" diff --git a/scene/config.lua b/scene/config.lua new file mode 100644 index 0000000..630cf63 --- /dev/null +++ b/scene/config.lua @@ -0,0 +1,24 @@ +local ConfigScene = Scene:extend() +require 'load.save' + + + +function ConfigScene:new() +end + +function ConfigScene:update() +end + +function ConfigScene:render() +end + +function ConfigScene:changeOption(rel) + local len = table.getn(main_menu_screens) + self.main_menu_state = (self.main_menu_state + len + rel - 1) % len + 1 +end + +function ConfigScene:onKeyPress(e) +end + +return ConfigScene + diff --git a/scene/game.lua b/scene/game.lua new file mode 100644 index 0000000..2df6660 --- /dev/null +++ b/scene/game.lua @@ -0,0 +1,74 @@ +local GameScene = Scene:extend() +require 'load.save' + +function GameScene:new(game_mode, ruleset) + self.game = game_mode() + self.ruleset = ruleset() + self.game:initialize(self.ruleset) +end + +function GameScene:update() + if love.window.hasFocus() then + self.game:update({ + left = love.keyboard.isScancodeDown(config.input.left), + right = love.keyboard.isScancodeDown(config.input.right), + up = love.keyboard.isScancodeDown(config.input.up), + down = love.keyboard.isScancodeDown(config.input.down), + rotate_left = love.keyboard.isScancodeDown(config.input.rotate_left), + rotate_left2 = love.keyboard.isScancodeDown(config.input.rotate_left2), + rotate_right = love.keyboard.isScancodeDown(config.input.rotate_right), + rotate_right2 = love.keyboard.isScancodeDown(config.input.rotate_right2), + rotate_180 = love.keyboard.isScancodeDown(config.input.rotate_180), + hold = love.keyboard.isScancodeDown(config.input.hold), + }, self.ruleset) + end + + self.game.grid:update() +end + +function GameScene:render() + love.graphics.setColor(1, 1, 1, 1) + love.graphics.draw( + backgrounds[self.game:getBackground()], + 0, 0, 0, + 0.5, 0.5 + ) + + -- game frame + love.graphics.draw(misc_graphics["frame"], 48, 64) + love.graphics.setColor(0, 0, 0, 200) + love.graphics.rectangle("fill", 64, 80, 160, 320) + + self.game:drawGrid() + self.game:drawPiece() + self.game:drawNextQueue(self.ruleset) + self.game:drawScoringInfo() + + -- ready/go graphics + if self.game.ready_frames <= 100 and self.game.ready_frames > 52 then + love.graphics.draw(misc_graphics["ready"], 144 - 50, 240 - 14) + elseif self.game.ready_frames <= 50 and self.game.ready_frames > 2 then + love.graphics.draw(misc_graphics["go"], 144 - 27, 240 - 14) + end + + self.game:drawCustom() + +end + +function GameScene:onKeyPress(e) + if (self.game.completed) and + e.scancode == "return" and e.isRepeat == false then + highscore_entry = self.game:getHighscoreData() + highscore_hash = self.game.hash .. "-" .. self.ruleset.hash + submitHighscore(highscore_hash, highscore_entry) + scene = ModeSelectScene() + end +end + +function submitHighscore(hash, data) + if not highscores[hash] then highscores[hash] = {} end + table.insert(highscores[hash], data) + saveHighscores() +end + +return GameScene diff --git a/scene/input_config.lua b/scene/input_config.lua new file mode 100644 index 0000000..d232750 --- /dev/null +++ b/scene/input_config.lua @@ -0,0 +1,62 @@ +local ConfigScene = Scene:extend() + +ConfigScene.title = "Input Config" + +require 'load.save' + +local configurable_inputs = { + "left", + "right", + "up", + "down", + "rotate_left", + "rotate_left2", + "rotate_right", + "rotate_right2", + "rotate_180", + "hold", +} + +function ConfigScene:new() + -- load current config + self.config = config.input + self.input_state = 1 +end + +function ConfigScene:update() +end + +function ConfigScene:render() + love.graphics.setFont(font_3x5_2) + for i, input in pairs(configurable_inputs) do + if config.input[input] then + love.graphics.printf(input, 40, 50 + i * 20, 200, "left") + love.graphics.printf( + love.keyboard.getKeyFromScancode(config.input[input]) .. " (" .. config.input[input] .. ")", + 240, 50 + i * 20, 200, "left" + ) + end + end + if self.input_state > table.getn(configurable_inputs) then + love.graphics.print("press enter to confirm, delete to retry") + else + love.graphics.print("press key for " .. configurable_inputs[self.input_state]) + end +end + +function ConfigScene:onKeyPress(e) + if self.input_state > table.getn(configurable_inputs) then + if e.scancode == "return" then + -- save, then load next scene + saveConfig() + scene = TitleScene() + elseif e.scancode == "delete" or e.scancode == "backspace" then + self.input_state = 1 + end + else + config.input[configurable_inputs[self.input_state]] = e.scancode + self.input_state = self.input_state + 1 + end +end + +return ConfigScene diff --git a/scene/mode_select.lua b/scene/mode_select.lua new file mode 100644 index 0000000..0dfb268 --- /dev/null +++ b/scene/mode_select.lua @@ -0,0 +1,127 @@ +local ModeSelectScene = Scene:extend() + +ModeSelectScene.title = "Game Start" + +current_mode = 1 +current_ruleset = 1 + +game_modes = { + require 'tetris.modes.marathon_2020', + require 'tetris.modes.survival_2020', + require 'tetris.modes.demon_mode', + require 'tetris.modes.strategy', + require 'tetris.modes.interval_training', + require 'tetris.modes.pacer_test', + require 'tetris.modes.phantom_mania', + require 'tetris.modes.phantom_mania2', + require 'tetris.modes.phantom_mania_n', + require 'tetris.modes.ligne', + require 'tetris.modes.marathon_a1', + require 'tetris.modes.marathon_a2', + require 'tetris.modes.marathon_a3', + require 'tetris.modes.survival_a1', + require 'tetris.modes.survival_a2', + require 'tetris.modes.survival_a3', + require 'tetris.modes.marathon_l1', +} + +rulesets = { + require 'tetris.rulesets.cambridge', + require 'tetris.rulesets.arika', + require 'tetris.rulesets.arika_ti', + require 'tetris.rulesets.standard_exp', + --require 'tetris.rulesets.bonkers', + --require 'tetris.rulesets.shirase', + --require 'tetris.rulesets.super302', +} + +function ModeSelectScene:new() + self.menu_state = { + mode = current_mode, + ruleset = current_ruleset, + select = "mode", + } +end + +function ModeSelectScene:update() +end + +function ModeSelectScene:render() + love.graphics.draw( + backgrounds[0], + 0, 0, 0, + 0.5, 0.5 + ) + + if self.menu_state.select == "mode" then + love.graphics.setColor(1, 1, 1, 0.5) + elseif self.menu_state.select == "ruleset" then + love.graphics.setColor(1, 1, 1, 0.25) + end + love.graphics.rectangle("fill", 20, 78 + 20 * self.menu_state.mode, 240, 22) + + if self.menu_state.select == "mode" then + love.graphics.setColor(1, 1, 1, 0.25) + elseif self.menu_state.select == "ruleset" then + love.graphics.setColor(1, 1, 1, 0.5) + end + love.graphics.rectangle("fill", 340, 78 + 20 * self.menu_state.ruleset, 200, 22) + + love.graphics.setColor(1, 1, 1, 1) + + love.graphics.draw(misc_graphics["select_mode"], 20, 40) + + love.graphics.setFont(font_3x5_2) + for idx, mode in pairs(game_modes) do + love.graphics.printf(mode.name, 40, 80 + 20 * idx, 200, "left") + end + for idx, ruleset in pairs(rulesets) do + love.graphics.printf(ruleset.name, 360, 80 + 20 * idx, 160, "left") + end +end + +function ModeSelectScene:onKeyPress(e) + if e.scancode == "return" and e.isRepeat == false then + current_mode = self.menu_state.mode + current_ruleset = self.menu_state.ruleset + config.current_mode = current_mode + config.current_ruleset = current_ruleset + saveConfig() + scene = GameScene(game_modes[self.menu_state.mode], rulesets[self.menu_state.ruleset]) + elseif (e.scancode == config.input["up"] or e.scancode == "up") and e.isRepeat == false then + self:changeOption(-1) + elseif (e.scancode == config.input["down"] or e.scancode == "down") and e.isRepeat == false then + self:changeOption(1) + elseif (e.scancode == config.input["left"] or e.scancode == "left") or + (e.scancode == config.input["right"] or e.scancode == "right") then + self:switchSelect() + end +end + +function ModeSelectScene:changeOption(rel) + if self.menu_state.select == "mode" then + self:changeMode(rel) + elseif self.menu_state.select == "ruleset" then + self:changeRuleset(rel) + end +end + +function ModeSelectScene:switchSelect(rel) + if self.menu_state.select == "mode" then + self.menu_state.select = "ruleset" + elseif self.menu_state.select == "ruleset" then + self.menu_state.select = "mode" + end +end + +function ModeSelectScene:changeMode(rel) + local len = table.getn(game_modes) + self.menu_state.mode = (self.menu_state.mode + len + rel - 1) % len + 1 +end + +function ModeSelectScene:changeRuleset(rel) + local len = table.getn(rulesets) + self.menu_state.ruleset = (self.menu_state.ruleset + len + rel - 1) % len + 1 +end + +return ModeSelectScene diff --git a/scene/title.lua b/scene/title.lua new file mode 100644 index 0000000..f2a7514 --- /dev/null +++ b/scene/title.lua @@ -0,0 +1,49 @@ +local TitleScene = Scene:extend() + +local main_menu_screens = { + ModeSelectScene, + InputConfigScene, +} + +function TitleScene:new() + self.main_menu_state = 1 +end + +function TitleScene:update() +end + +function TitleScene:render() + love.graphics.setFont(font_3x5_2) + + love.graphics.draw( + backgrounds["title"], + 0, 0, 0, + 0.5, 0.5 + ) + + love.graphics.setColor(1, 1, 1, 0.5) + love.graphics.rectangle("fill", 20, 278 + 20 * self.main_menu_state, 160, 22) + + love.graphics.setColor(1, 1, 1, 1) + for i, screen in pairs(main_menu_screens) do + love.graphics.printf(screen.title, 40, 280 + 20 * i, 120, "left") + end + +end + +function TitleScene:changeOption(rel) + local len = table.getn(main_menu_screens) + self.main_menu_state = (self.main_menu_state + len + rel - 1) % len + 1 +end + +function TitleScene:onKeyPress(e) + if e.scancode == "return" and e.isRepeat == false then + scene = main_menu_screens[self.main_menu_state]() + elseif (e.scancode == config.input["up"] or e.scancode == "up") and e.isRepeat == false then + self:changeOption(-1) + elseif (e.scancode == config.input["down"] or e.scancode == "down") and e.isRepeat == false then + self:changeOption(1) + end +end + +return TitleScene diff --git a/tetris/components/grid.lua b/tetris/components/grid.lua new file mode 100644 index 0000000..ea5c183 --- /dev/null +++ b/tetris/components/grid.lua @@ -0,0 +1,204 @@ +local Object = require 'libs.classic' + +local Grid = Object:extend() + +local empty = { skin = "", colour = "" } + +function Grid:new() + self.grid = {} + self.grid_age = {} + for y = 1, 24 do + self.grid[y] = {} + self.grid_age[y] = {} + for x = 1, 10 do + self.grid[y][x] = empty + self.grid_age[y][x] = 0 + end + end +end + +function Grid:clear() + for y = 1, 24 do + for x = 1, 10 do + self.grid[y][x] = empty + self.grid_age[y][x] = 0 + end + end +end + +function Grid:isOccupied(x, y) + return self.grid[y+1][x+1] ~= empty +end + +function Grid:isRowFull(row) + for index, square in pairs(self.grid[row]) do + if square == empty then return false end + end + return true +end + +function Grid:canPlacePiece(piece) + local offsets = piece:getBlockOffsets() + for index, offset in pairs(offsets) do + local x = piece.position.x + offset.x + local y = piece.position.y + offset.y + if x >= 10 or x < 0 or y >= 24 or y < 0 or self.grid[y+1][x+1] ~= empty then + return false + end + end + return true +end + +function Grid:canPlacePieceInVisibleGrid(piece) + local offsets = piece:getBlockOffsets() + for index, offset in pairs(offsets) do + local x = piece.position.x + offset.x + local y = piece.position.y + offset.y + if x >= 10 or x < 0 or y >= 24 or y < 4 or self.grid[y+1][x+1] ~= empty then + return false + end + end + return true +end + +function Grid:getClearedRowCount() + local count = 0 + for row = 1, 24 do + if self:isRowFull(row) then + count = count + 1 + end + end + return count +end + +function Grid:markClearedRows() + for row = 1, 24 do + if self:isRowFull(row) then + for x = 1, 10 do + self.grid[row][x] = { + skin = self.grid[row][x].skin, + colour = "X" + } + end + end + end + return true +end + +function Grid:clearClearedRows() + for row = 1, 24 do + if self:isRowFull(row) then + for above_row = row, 2, -1 do + self.grid[above_row] = self.grid[above_row - 1] + self.grid_age[above_row] = self.grid_age[above_row - 1] + end + self.grid[1] = {empty, empty, empty, empty, empty, empty, empty, empty, empty, empty} + self.grid_age[1] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + end + end + return true +end + +function Grid:copyBottomRow() + for row = 1, 23 do + self.grid[row] = self.grid[row+1] + self.grid_age[row] = self.grid_age[row+1] + end + self.grid[24] = {empty, empty, empty, empty, empty, empty, empty, empty, empty, empty} + self.grid_age[24] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + for col = 1, 10 do + self.grid[24][col] = (self.grid[23][col] == empty) and empty or { + skin = self.grid[23][col].skin, + colour = "G" + } + end + return true +end + +function Grid:applyPiece(piece) + offsets = piece:getBlockOffsets() + for index, offset in pairs(offsets) do + x = piece.position.x + offset.x + y = piece.position.y + offset.y + self.grid[y+1][x+1] = { + skin = piece.skin, + colour = piece.shape + } + end +end + +function Grid:update() + for y = 1, 24 do + for x = 1, 10 do + if self.grid[y][x] ~= empty then + self.grid_age[y][x] = self.grid_age[y][x] + 1 + end + end + end +end + +function Grid:draw() + for y = 1, 24 do + for x = 1, 10 do + if self.grid[y][x] ~= empty then + if self.grid_age[y][x] < 1 then + love.graphics.setColor(1, 1, 1, 1) + love.graphics.draw(blocks[self.grid[y][x].skin]["F"], 48+x*16, y*16) + else + love.graphics.setColor(0.5, 0.5, 0.5, 1) + love.graphics.draw(blocks[self.grid[y][x].skin][self.grid[y][x].colour], 48+x*16, y*16) + end + love.graphics.setColor(0.8, 0.8, 0.8, 1) + love.graphics.setLineWidth(1) + if y > 1 and self.grid[y-1][x] == empty then + love.graphics.line(48.0+x*16, -0.5+y*16, 64.0+x*16, -0.5+y*16) + end + if y < 24 and self.grid[y+1][x] == empty then + love.graphics.line(48.0+x*16, 16.5+y*16, 64.0+x*16, 16.5+y*16) + end + if x > 1 and self.grid[y][x-1] == empty then + love.graphics.line(47.5+x*16, -0.0+y*16, 47.5+x*16, 16.0+y*16) + end + if x < 10 and self.grid[y][x+1] == empty then + love.graphics.line(64.5+x*16, -0.0+y*16, 64.5+x*16, 16.0+y*16) + end + end + end + end +end + +function Grid:drawInvisible(opacity_function, garbage_opacity_function) + for y = 1, 24 do + for x = 1, 10 do + if self.grid[y][x] ~= empty then + if self.grid[y][x].colour == "X" then + opacity = 1 + elseif garbage_opacity_function and self.grid[y][x].colour == "G" then + opacity = garbage_opacity_function(self.grid_age[y][x]) + else + opacity = opacity_function(self.grid_age[y][x]) + end + love.graphics.setColor(0.5, 0.5, 0.5, opacity) + love.graphics.draw(blocks[self.grid[y][x].skin][self.grid[y][x].colour], 48+x*16, y*16) + if opacity > 0 and self.grid[y][x].colour ~= "X" then + love.graphics.setColor(0.64, 0.64, 0.64) + love.graphics.setLineWidth(1) + if y > 1 and self.grid[y-1][x] == empty then + love.graphics.line(48.0+x*16, -0.5+y*16, 64.0+x*16, -0.5+y*16) + end + if y < 24 and self.grid[y+1][x] == empty then + love.graphics.line(48.0+x*16, 16.5+y*16, 64.0+x*16, 16.5+y*16) + end + if x > 1 and self.grid[y][x-1] == empty then + love.graphics.line(47.5+x*16, -0.0+y*16, 47.5+x*16, 16.0+y*16) + end + if x < 10 and self.grid[y][x+1] == empty then + love.graphics.line(64.5+x*16, -0.0+y*16, 64.5+x*16, 16.0+y*16) + end + end + end + end + end +end + +return Grid diff --git a/tetris/components/piece.lua b/tetris/components/piece.lua new file mode 100644 index 0000000..3ed1524 --- /dev/null +++ b/tetris/components/piece.lua @@ -0,0 +1,153 @@ +local Object = require 'libs.classic' + +local Piece = Object:extend() + +function Piece:new(shape, rotation, position, block_offsets, gravity, lock_delay, skin) + self.shape = shape + self.rotation = rotation + self.position = position + self.block_offsets = block_offsets + self.gravity = gravity + self.lock_delay = lock_delay + self.skin = skin + self.ghost = false + self.locked = false +end + +-- Functions that return a new piece to test in rotation systems. + +function Piece:withOffset(offset) + return Piece( + self.shape, self.rotation, + {x = self.position.x + offset.x, y = self.position.y + offset.y}, + self.block_offsets, self.gravity, self.lock_delay, self.skin + ) +end + +function Piece:withRelativeRotation(rot) + local new_rot = self.rotation + rot + while new_rot < 0 do new_rot = new_rot + 4 end + while new_rot >= 4 do new_rot = new_rot - 4 end + return Piece( + self.shape, new_rot, self.position, + self.block_offsets, self.gravity, self.lock_delay, self.skin + ) +end + +-- Functions that return predicates relative to a grid. + +function Piece:getBlockOffsets() + return self.block_offsets[self.shape][self.rotation + 1] +end + +function Piece:occupiesSquare(x, y) + local offsets = self:getBlockOffsets() + for index, offset in pairs(offsets) do + local new_offset = {x = self.position.x + offset.x, y = self.position.y + offset.y} + if new_offset.x == x and new_offset.y == y then + return true + end + end + return false +end + +function Piece:isMoveBlocked(grid, offset) + local moved_piece = self:withOffset(offset) + return not grid:canPlacePiece(moved_piece) +end + +function Piece:isDropBlocked(grid) + return self:isMoveBlocked(grid, { x=0, y=1 }) +end + +-- Procedures to actually do stuff to pieces. + +function Piece:setOffset(offset) + self.position.x = self.position.x + offset.x + self.position.y = self.position.y + offset.y + return self +end + +function Piece:setRelativeRotation(rot) + new_rot = self.rotation + rot + while new_rot < 0 do new_rot = new_rot + 4 end + while new_rot >= 4 do new_rot = new_rot - 4 end + self.rotation = new_rot + return self +end + +function Piece:moveInGrid(step, squares, grid) + local moved = false + for x = 1, squares do + if grid:canPlacePiece(self:withOffset(step)) then + moved = true + self:setOffset(step) + else + break + end + end + if moved and step.y == 0 then playSE("move") end + return self +end + +function Piece:dropSquares(dropped_squares, grid) + self:moveInGrid({ x = 0, y = 1 }, dropped_squares, grid) +end + +function Piece:dropToBottom(grid) + local piece_y = self.position.y + self:dropSquares(20, grid) + self.gravity = 0 + if self.position.y > piece_y then + -- if it got dropped any, also reset lock delay + if self.ghost == false then playSE("bottom") end + self.lock_delay = 0 + end + return self +end + +function Piece:lockIfBottomed(grid) + if self:isDropBlocked(grid) then + self.locked = true + end + return self +end + +function Piece:addGravity(gravity, grid) + local new_gravity = self.gravity + gravity + if self:isDropBlocked(grid) then + self.gravity = math.min(1, new_gravity) + self.lock_delay = self.lock_delay + 1 + else + local dropped_squares = math.floor(new_gravity) + local new_frac_gravity = new_gravity - dropped_squares + self.gravity = new_frac_gravity + self:dropSquares(dropped_squares, grid) + if self:isDropBlocked(grid) then + playSE("bottom") + end + end + return self +end + +-- Procedures for drawing. + +function Piece:draw(opacity, brightness, grid, partial_das) + if opacity == nil then opacity = 1 end + if brightness == nil then brightness = 1 end + love.graphics.setColor(brightness, brightness, brightness, opacity) + local offsets = self:getBlockOffsets() + local gravity_offset = 0 + if grid ~= nil and not self:isDropBlocked(grid) then + gravity_offset = self.gravity * 16 + end + if partial_das == nil then partial_das = 0 end + for index, offset in pairs(offsets) do + local x = self.position.x + offset.x + local y = self.position.y + offset.y + love.graphics.draw(blocks[self.skin][self.shape], 64+x*16+partial_das, 16+y*16+gravity_offset) + end + return false +end + +return Piece diff --git a/tetris/modes/demon_mode.lua b/tetris/modes/demon_mode.lua new file mode 100644 index 0000000..a26f156 --- /dev/null +++ b/tetris/modes/demon_mode.lua @@ -0,0 +1,258 @@ +require 'funcs' + +local GameMode = require 'tetris.modes.gamemode' +local Piece = require 'tetris.components.piece' + +local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls' + +local DemonModeGame = GameMode:extend() + +DemonModeGame.name = "Demon Mode" +DemonModeGame.hash = "DemonMode" +DemonModeGame.tagline = "Can you handle the ludicrous speed past level 20?" + +DemonModeGame.arr = 1 +DemonModeGame.drop_speed = 1 + +function DemonModeGame:new() + DemonModeGame.super:new() + self.roll_frames = 0 + self.combo = 1 + self.randomizer = History6RollsRandomizer() + + self.grade = 0 + self.section_start_time = 0 + self.section_times = { [0] = 0 } + self.section_tetris_count = 0 + self.section_tries = 0 + + self.enable_hold = true + self.lock_drop = true + self.next_queue_length = 3 +end + +function DemonModeGame:getARE() + if self.level < 500 then return 30 + elseif self.level < 600 then return 25 + elseif self.level < 700 then return 15 + elseif self.level < 800 then return 14 + elseif self.level < 900 then return 12 + elseif self.level < 1000 then return 11 + elseif self.level < 1100 then return 10 + elseif self.level < 1300 then return 8 + elseif self.level < 1400 then return 6 + elseif self.level < 1700 then return 4 + elseif self.level < 1800 then return 3 + elseif self.level < 1900 then return 2 + elseif self.level < 2000 then return 1 + else return 0 end +end + +function DemonModeGame:getLineARE() + return self:getARE() +end + +function DemonModeGame:getDasLimit() + if self.level < 500 then return 15 + elseif self.level < 1000 then return 10 + elseif self.level < 1500 then return 5 + elseif self.level < 1700 then return 4 + elseif self.level < 1900 then return 3 + elseif self.level < 2000 then return 2 + else return 1 end +end + +function DemonModeGame:getLineClearDelay() + if self.level < 600 then return 15 + elseif self.level < 800 then return 10 + elseif self.level < 1000 then return 8 + elseif self.level < 1500 then return 5 + elseif self.level < 1700 then return 3 + elseif self.level < 1900 then return 2 + elseif self.level < 2000 then return 1 + else return 0 end +end + +function DemonModeGame:getLockDelay() + if self.level < 100 then return 30 + elseif self.level < 200 then return 25 + elseif self.level < 300 then return 22 + elseif self.level < 400 then return 20 + elseif self.level < 1000 then return 15 + elseif self.level < 1200 then return 10 + elseif self.level < 1400 then return 9 + elseif self.level < 1500 then return 8 + elseif self.level < 1600 then return 7 + elseif self.level < 1700 then return 6 + elseif self.level < 1800 then return 5 + elseif self.level < 1900 then return 4 + elseif self.level < 2000 then return 3 + else return 2 end +end + +function DemonModeGame:getGravity() + return 20 +end + +local function getSectionForLevel(level) + return math.floor(level / 100) + 1 +end + +local cleared_row_levels = {1, 3, 6, 10} + +function DemonModeGame:advanceOneFrame() + if self.clear then + self.roll_frames = self.roll_frames + 1 + if self.roll_frames < 0 then + return + elseif self.roll_frames >= 1337 then + self.completed = true + end + elseif self.ready_frames == 0 then + self.frames = self.frames + 1 + end +end + +function DemonModeGame:onPieceEnter() + if (self.level % 100 ~= 99) and self.frames ~= 0 then + self.level = self.level + 1 + end +end + +function DemonModeGame:onLineClear(cleared_row_count) + if cleared_row_count == 4 then + self.section_tetris_count = self.section_tetris_count + 1 + end + local advanced_levels = cleared_row_levels[cleared_row_count] + if not self.clear then + self:updateSectionTimes(self.level, self.level + advanced_levels) + end +end + +function DemonModeGame:updateSectionTimes(old_level, new_level) + local section = math.floor(old_level / 100) + 1 + if math.floor(old_level / 100) < math.floor(new_level / 100) then + -- If at least one Tetris in this section hasn't been made, + -- deny section passage. + if old_level > 500 then + if self.section_tetris_count == 0 then + self.level = 100 * math.floor(old_level / 100) + self.section_tries = self.section_tries + 1 + else + self.level = math.min(new_level, 2500) + -- if this is first try (no denials, add a grade) + if self.section_tries == 0 then + self.grade = self.grade + 1 + end + self.section_tries = 0 + self.section_tetris_count = 0 + -- record new section + section_time = self.frames - self.section_start_time + table.insert(self.section_times, section_time) + self.section_start_time = self.frames + -- maybe clear + if self.level == 2500 and not self.clear then + self.clear = true + self.grid:clear() + self.roll_frames = -150 + end + end + elseif old_level < 100 then + -- If section time is under cutoff, skip to level 500. + if self.frames < sp(1,00) then + self.level = 500 + self.grade = 5 + self.section_tries = 0 + self.section_tetris_count = 0 + else + self.level = math.min(new_level, 2500) + end + -- record new section + section_time = self.frames - self.section_start_time + table.insert(self.section_times, section_time) + self.section_start_time = self.frames + end + else + self.level = math.min(new_level, 2500) + end +end + +function DemonModeGame:updateScore(level, drop_bonus, cleared_lines) + if cleared_lines > 0 then + self.score = self.score + ( + (math.ceil((level + cleared_lines) / 4) + drop_bonus) * + cleared_lines * (cleared_lines * 2 - 1) * (self.combo * 2 - 1) + ) + self.lines = self.lines + cleared_lines + self.combo = self.combo + cleared_lines - 1 + else + self.drop_bonus = 0 + self.combo = 1 + end +end + +local letter_grades = { + [0] = "", "D", "C", "B", "A", + "S", "S-A", "S-B", "S-C", "S-D", + "X", "X-A", "X-B", "X-C", "X-D", + "W", "W-A", "W-B", "W-C", "W-D", + "Master", "MasterS", "MasterX", "MasterW", "Grand Master", + "Demon Master" +} + +function DemonModeGame:getLetterGrade() + return letter_grades[self.grade] +end + +function DemonModeGame:drawGrid() + if self.clear and not (self.completed or self.game_over) then + self.grid:drawInvisible(self.rollOpacityFunction) + else + self.grid:draw() + end +end + +DemonModeGame.rollOpacityFunction = function(age) + if age > 4 then return 0 + else return 1 - age / 4 end +end + +function DemonModeGame:drawScoringInfo() + DemonModeGame.super.drawScoringInfo(self) + + love.graphics.setColor(1, 1, 1, 1) + + love.graphics.setFont(font_3x5_2) + love.graphics.print( + self.das.direction .. " " .. + self.das.frames .. " " .. + st(self.prev_inputs) + ) + love.graphics.printf("NEXT", 64, 40, 40, "left") + love.graphics.printf("GRADE", 240, 120, 40, "left") + love.graphics.printf("SCORE", 240, 200, 40, "left") + love.graphics.printf("LEVEL", 240, 320, 40, "left") + + -- draw section time data + local current_section = getSectionForLevel(self.level) + self:drawSectionTimesWithSecondary(current_section) + + love.graphics.setFont(font_3x5_3) + love.graphics.printf(self.score, 240, 220, 90, "left") + love.graphics.printf(self:getLetterGrade(), 240, 140, 90, "left") + love.graphics.printf(string.format("%.2f", self.level / 100), 240, 340, 70, "right") +end + +function DemonModeGame:getHighscoreData() + return { + grade = self.grade, + level = self.level, + frames = self.frames, + } +end + +function DemonModeGame:getBackground() + return math.floor(self.level / 100) +end + +return DemonModeGame diff --git a/tetris/modes/gamemode.lua b/tetris/modes/gamemode.lua new file mode 100644 index 0000000..5f9b8bd --- /dev/null +++ b/tetris/modes/gamemode.lua @@ -0,0 +1,442 @@ +local Object = require 'libs.classic' +require 'funcs' + +local Grid = require 'tetris.components.grid' +local Randomizer = require 'tetris.randomizers.randomizer' + +local GameMode = Object:extend() + +GameMode.rollOpacityFunction = function(age) return 0 end + +function GameMode:new() + self.grid = Grid() + self.randomizer = Randomizer() + self.piece = nil + self.ready_frames = 100 + self.frames = 0 + self.game_over_frames = 0 + self.score = 0 + self.level = 0 + self.lines = 0 + self.drop_bonus = 0 + self.are = 0 + self.lcd = 0 + self.das = { direction = "none", frames = -1 } + self.move = "none" + self.prev_inputs = {} + self.next_queue = {} + self.game_over = false + self.clear = false + self.completed = false + -- configurable parameters + self.lock_drop = false + self.lock_hard_drop = false + self.instant_hard_drop = false + self.instant_soft_drop = true + self.enable_hold = false + self.enable_hard_drop = true + self.next_queue_length = 1 + self.draw_section_times = false + self.draw_secondary_section_times = false + -- variables related to configurable parameters + self.drop_locked = false + self.hard_drop_locked = false + self.hold_queue = nil + self.held = false + self.section_start_time = 0 + self.section_times = { [0] = 0 } + self.secondary_section_times = { [0] = 0 } +end + +function GameMode:initialize() + -- after all the variables are initialized, run initialization procedures + for i = 1, 30 do + table.insert(self.next_queue, self:getNextPiece(ruleset)) + end +end + +function GameMode:getARR() return 1 end +function GameMode:getDropSpeed() return 1 end +function GameMode:getARE() return 25 end +function GameMode:getLineARE() return 25 end +function GameMode:getLockDelay() return 30 end +function GameMode:getLineClearDelay() return 40 end +function GameMode:getDasLimit() return 15 end + +function GameMode:getNextPiece(ruleset) + return { + skin = "2tie", + shape = self.randomizer:nextPiece(), + orientation = ruleset:getDefaultOrientation(), + } +end + +function GameMode:initialize(ruleset) + -- generate next queue + for i = 1, self.next_queue_length do + table.insert(self.next_queue, self:getNextPiece(ruleset)) + end +end + +function GameMode:update(inputs, ruleset) + if self.game_over then + self.game_over_frames = self.game_over_frames + 1 + if self.game_over_frames >= 60 then + self.completed = true + end + return + end + if self.completed then return end + + -- advance one frame + if self:advanceOneFrame(inputs) == false then return end + + self:chargeDAS(inputs, self:getDasLimit(), self.getARR()) + + if self.piece == nil then + self:processDelays(inputs, ruleset) + else + -- perform active frame actions such as fading out the next queue + self:whilePieceActive() + local gravity = self:getGravity() + + if self.enable_hold and inputs["hold"] == true and self.held == false then + self:hold(inputs, ruleset) + self.prev_inputs = inputs + return + end + + if self.lock_drop and inputs["down"] ~= true then + self.drop_locked = false + end + + if self.lock_hard_drop and inputs["up"] ~= true then + self.hard_drop_locked = false + end + + ruleset:processPiece( + inputs, self.piece, self.grid, self:getGravity(), self.prev_inputs, + self.move, self:getLockDelay(), self:getDropSpeed(), + self.drop_locked, self.hard_drop_locked, self.enable_hard_drop + ) + + if inputs["up"] == true and + self.piece:isDropBlocked(self.grid) and + not self.hard_drop_locked and + self.instant_hard_drop + then + self.piece.locked = true + end + + if inputs["down"] == true and + self.piece:isDropBlocked(self.grid) and + not self.drop_locked and + self.instant_soft_drop + then + self.piece.locked = true + end + + if self.piece.locked == true then + self.grid:applyPiece(self.piece) + self:onPieceLock(self.piece) + self.piece = nil + if self.enable_hold then + self.held = false + end + + self.grid:markClearedRows() + + local cleared_row_count = self.grid:getClearedRowCount() + + self:updateScore(self.level, self.drop_bonus, cleared_row_count) + + if cleared_row_count > 0 then + self.lcd = self:getLineClearDelay() + self.are = self:getLineARE() + if self.lcd == 0 then + self.grid:clearClearedRows() + if self.are == 0 then + self:initializeOrHold(inputs, ruleset) + end + end + self:onLineClear(cleared_row_count) + else + if self:getARE() == 0 then + self:initializeOrHold(inputs, ruleset) + else + self.are = self:getARE() + end + end + end + end + self.prev_inputs = inputs +end + +function GameMode:updateScore() end + +function GameMode:advanceOneFrame() + if self.clear then + self.completed = true + elseif self.ready_frames == 0 then + self.frames = self.frames + 1 + end +end + +-- event functions +function GameMode:whilePieceActive() end +function GameMode:onPieceLock(piece) end +function GameMode:onLineClear(cleared_row_count) end +function GameMode:onPieceEnter() end +function GameMode:onHold() end + +function GameMode:onGameOver() + switchBGM(nil) +end + +function GameMode:chargeDAS(inputs) + if inputs[self.das.direction] == true then + local das_frames = self.das.frames + 1 + if das_frames >= self:getDasLimit() then + if self.das.direction == "left" then + self.move = (self:getARR() == 0 and "speed" or "") .. "left" + self.das.frames = self:getDasLimit() - self:getARR() + elseif self.das.direction == "right" then + self.move = (self:getARR() == 0 and "speed" or "") .. "right" + self.das.frames = self:getDasLimit() - self:getARR() + end + else + self.move = "none" + self.das.frames = das_frames + end + elseif inputs["right"] == true then + self.move = "right" + self.das = { direction = "right", frames = 0 } + elseif inputs["left"] == true then + self.move = "left" + self.das = { direction = "left", frames = 0 } + else + self.move = "none" + self.das = { direction = "none", frames = -1 } + end +end + +function GameMode:processDelays(inputs, ruleset, drop_speed) + if self.ready_frames > 0 then + self.ready_frames = self.ready_frames - 1 + if self.ready_frames == 0 then + self:initializeOrHold(inputs, ruleset) + end + elseif self.lcd > 0 then + self.lcd = self.lcd - 1 + if self.lcd == 0 then + self.grid:clearClearedRows() + if self.are == 0 then + self:initializeOrHold(inputs, ruleset) + end + end + elseif self.are > 0 then + self.are = self.are - 1 + if self.are == 0 then + self:initializeOrHold(inputs, ruleset) + end + end +end + +function GameMode:initializeOrHold(inputs, ruleset) + if self.enable_hold and inputs["hold"] == true then + self:hold(inputs, ruleset) + else + self:initializeNextPiece(inputs, ruleset, self.next_queue[1]) + end + self:onPieceEnter() + if not self.grid:canPlacePiece(self.piece) then + self:onGameOver() + self.game_over = true + end +end + +function GameMode:hold(inputs, ruleset) + local data = copy(self.hold_queue) + if self.piece == nil then + self.hold_queue = self.next_queue[1] + table.remove(self.next_queue, 1) + table.insert(self.next_queue, self:getNextPiece(ruleset)) + else + self.hold_queue = { + skin = self.piece.skin, + shape = self.piece.shape, + orientation = ruleset:getDefaultOrientation(), + } + end + if data == nil then + self:initializeNextPiece(inputs, ruleset, self.next_queue[1]) + else + self:initializeNextPiece(inputs, ruleset, data, false) + end + self.held = true + self:onHold() +end + +function GameMode:initializeNextPiece(inputs, ruleset, piece_data, generate_next_piece) + local gravity = self:getGravity() + self.piece = ruleset:initializePiece( + inputs, piece_data, self.grid, gravity, + self.prev_inputs, self.move, + self:getLockDelay(), self:getDropSpeed(), + self.lock_drop, self.lock_hard_drop + ) + if self.lock_drop then + self.drop_locked = true + end + if self.lock_hard_drop then + self.hard_drop_locked = true + end + if generate_next_piece == nil then + table.remove(self.next_queue, 1) + table.insert(self.next_queue, self:getNextPiece(ruleset)) + end + self:playNextSound() +end + +function GameMode:playNextSound() + playSE("blocks", self.next_queue[1].shape) +end + +function GameMode:getHighScoreData() + return { + score = self.score + } +end + +function GameMode:drawPiece() + if self.piece ~= nil then + self.piece:draw( + 1, + self:getLockDelay() == 0 and 1 or + (0.25 + 0.75 * math.max(1 - self.piece.gravity, 1 - (self.piece.lock_delay / self:getLockDelay()))), + self.grid + ) + end +end + +function GameMode:drawGhostPiece(ruleset) + if self.piece == nil then return end + local ghost_piece = self.piece:withOffset({x=0, y=0}) + ghost_piece.ghost = true + ghost_piece:dropToBottom(self.grid) + ghost_piece:draw(0.5) +end + +function GameMode:drawNextQueue(ruleset) + function drawPiece(piece, skin, offsets, pos_x, pos_y) + for index, offset in pairs(offsets) do + local x = ruleset.spawn_positions[piece].x + offset.x + local y = ruleset.spawn_positions[piece].y + offset.y + love.graphics.draw(blocks[skin][piece], pos_x+x*16, pos_y+y*16) + end + end + for i = 1, self.next_queue_length do + self:setNextOpacity(i) + local next_piece = self.next_queue[i].shape + local skin = self.next_queue[i].skin + local rotation = self.next_queue[i].orientation + if config.side_next then -- next at side + drawPiece(next_piece, skin, ruleset.block_offsets[next_piece][rotation], 192, -16+i*48) + else -- next at top + drawPiece(next_piece, skin, ruleset.block_offsets[next_piece][rotation], -16+i*80, -32) + end + end + if self.hold_queue ~= nil then + self:setHoldOpacity() + drawPiece( + self.hold_queue.shape, + self.hold_queue.skin, + ruleset.block_offsets[self.hold_queue.shape][self.hold_queue.orientation], + -16, -32 + ) + end + return false +end + +function GameMode:setNextOpacity(i) love.graphics.setColor(1, 1, 1, 1) end +function GameMode:setHoldOpacity() love.graphics.setColor(1, 1, 1, 1) end + +function GameMode:drawScoringInfo() + love.graphics.setColor(1, 1, 1, 1) + love.graphics.setFont(font_3x5_2) + + if config["side_next"] then + love.graphics.printf("NEXT", 240, 72, 40, "left") + else + love.graphics.printf("NEXT", 64, 40, 40, "left") + end + + love.graphics.print( + self.das.direction .. " " .. + self.das.frames .. " " .. + st(self.prev_inputs) + ) + + love.graphics.setFont(font_8x11) + love.graphics.printf(formatTime(self.frames), 64, 420, 160, "center") +end + +function GameMode:drawSectionTimes(current_section) + local section_x = 530 + + for section, time in pairs(self.section_times) do + if section > 0 then + love.graphics.printf(formatTime(time), section_x, 40 + 20 * section, 90, "left") + end + end + + love.graphics.printf(formatTime(self.frames - self.section_start_time), section_x, 40 + 20 * current_section, 90, "left") +end + +function GameMode:drawSectionTimesWithSecondary(current_section) + local section_x = 530 + local section_secondary_x = 440 + + for section, time in pairs(self.section_times) do + if section > 0 then + love.graphics.printf(formatTime(time), section_x, 40 + 20 * section, 90, "left") + end + end + + for section, time in pairs(self.secondary_section_times) do + if section > 0 then + love.graphics.printf(formatTime(time), section_secondary_x, 40 + 20 * section, 90, "left") + end + end + + local current_x + if table.getn(self.section_times) < table.getn(self.secondary_section_times) then + current_x = section_x + else + current_x = section_secondary_x + end + + love.graphics.printf(formatTime(self.frames - self.section_start_time), current_x, 40 + 20 * current_section, 90, "left") +end + +function GameMode:drawSectionTimesWithSplits(current_section) + local section_x = 440 + local split_x = 530 + + local split_time = 0 + + for section, time in pairs(self.section_times) do + if section > 0 then + love.graphics.printf(formatTime(time), section_x, 40 + 20 * section, 90, "left") + split_time = split_time + time + love.graphics.printf(formatTime(split_time), split_x, 40 + 20 * section, 90, "left") + end + end + + love.graphics.printf(formatTime(self.frames - self.section_start_time), section_x, 40 + 20 * current_section, 90, "left") + love.graphics.printf(formatTime(self.frames), split_x, 40 + 20 * current_section, 90, "left") +end + +function GameMode:drawCustom() end + +return GameMode diff --git a/tetris/modes/interval_training.lua b/tetris/modes/interval_training.lua new file mode 100644 index 0000000..b519144 --- /dev/null +++ b/tetris/modes/interval_training.lua @@ -0,0 +1,155 @@ +require 'funcs' + +local GameMode = require 'tetris.modes.gamemode' +local Piece = require 'tetris.components.piece' + +local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls' + +local IntervalTrainingGame = GameMode:extend() + +IntervalTrainingGame.name = "Interval Training" +IntervalTrainingGame.hash = "IntervalTraining" +IntervalTrainingGame.tagline = "Can you clear the time hurdles when the game goes this fast?" + +IntervalTrainingGame.arr = 1 +IntervalTrainingGame.drop_speed = 1 + +function IntervalTrainingGame:new() + IntervalTrainingGame.super:new() + self.roll_frames = 0 + self.combo = 1 + self.randomizer = History6RollsRandomizer() + self.section_time_limit = 1800 + self.section_start_time = 0 + self.section_times = { [0] = 0 } + self.lock_drop = true + self.enable_hold = true + self.next_queue_length = 3 +end + +function IntervalTrainingGame:getARE() + return 4 +end + +function IntervalTrainingGame:getLineARE() + return 4 +end + +function IntervalTrainingGame:getDasLimit() + return 6 +end + +function IntervalTrainingGame:getLineClearDelay() + return 6 +end + +function IntervalTrainingGame:getLockDelay() + return 15 +end + +function IntervalTrainingGame:getGravity() + return 20 +end + +function IntervalTrainingGame:getSection() + return math.floor(level / 100) + 1 +end + +function IntervalTrainingGame:advanceOneFrame() + if self.clear then + self.roll_frames = self.roll_frames + 1 + if self.roll_frames > 2968 then + self.completed = true + end + return false + elseif self.ready_frames == 0 then + self.frames = self.frames + 1 + if self:getSectionTime() >= self.section_time_limit then + self.game_over = true + end + end + return true +end + +function IntervalTrainingGame:onPieceEnter() + if (self.level % 100 ~= 99 or self.level == 998) and not self.clear and self.frames ~= 0 then + self.level = self.level + 1 + end +end + +function IntervalTrainingGame:onLineClear(cleared_row_count) + if not self.clear then + local new_level = self.level + cleared_row_count + self:updateSectionTimes(self.level, new_level) + self.level = math.min(new_level, 999) + if self.level == 999 then + self.clear = true + end + end +end + +function IntervalTrainingGame:getSectionTime() + return self.frames - self.section_start_time +end + +function IntervalTrainingGame:updateSectionTimes(old_level, new_level) + if math.floor(old_level / 100) < math.floor(new_level / 100) then + -- record new section + table.insert(self.section_times, self:getSectionTime()) + self.section_start_time = self.frames + else + self.level = math.min(new_level, 999) + end +end + +function IntervalTrainingGame:drawGrid(ruleset) + self.grid:draw() +end + +function IntervalTrainingGame:getHighscoreData() + return { + level = self.level, + frames = self.frames, + } +end + +function IntervalTrainingGame:drawScoringInfo() + love.graphics.setColor(1, 1, 1, 1) + + love.graphics.setFont(font_3x5_2) + love.graphics.print( + self.das.direction .. " " .. + self.das.frames .. " " .. + st(self.prev_inputs) + ) + love.graphics.printf("NEXT", 64, 40, 40, "left") + love.graphics.printf("TIME LEFT", 240, 250, 80, "left") + love.graphics.printf("LEVEL", 240, 320, 40, "left") + + local current_section = math.floor(self.level / 100) + 1 + self:drawSectionTimesWithSplits(current_section) + + love.graphics.setFont(font_3x5_3) + love.graphics.printf(self.level, 240, 340, 40, "right") + + -- draw time left, flash red if necessary + local time_left = self.section_time_limit - math.max(self:getSectionTime(), 0) + if not self.game_over and not self.clear and time_left < sp(0,10) and time_left % 4 < 2 then + love.graphics.setColor(1, 0.3, 0.3, 1) + end + love.graphics.printf(formatTime(time_left), 240, 270, 160, "left") + + love.graphics.setColor(1, 1, 1, 1) + love.graphics.printf(self:getSectionEndLevel(), 240, 370, 40, "right") +end + +function IntervalTrainingGame:getSectionEndLevel() + if self.level > 900 then return 999 + else return math.floor(self.level / 100 + 1) * 100 end +end + +function IntervalTrainingGame:getBackground() + return math.floor(self.level / 100) +end + +return IntervalTrainingGame diff --git a/tetris/modes/ligne.lua b/tetris/modes/ligne.lua new file mode 100644 index 0000000..a82a6d5 --- /dev/null +++ b/tetris/modes/ligne.lua @@ -0,0 +1,131 @@ +require 'funcs' + +local GameMode = require 'tetris.modes.gamemode' +local Piece = require 'tetris.components.piece' + +local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls' + +local LigneGame = GameMode:extend() + +LigneGame.name = "Ligne" +LigneGame.hash = "Ligne" +LigneGame.tagline = "How fast can you clear 40 lines?" + +LigneGame.arr = 1 +LigneGame.drop_speed = 1 + +function LigneGame:new() + LigneGame.super:new() + + self.lines = 0 + self.line_goal = 40 + self.pieces = 0 + self.randomizer = History6RollsRandomizer() + + self.roll_frames = 0 + + self.lock_drop = true + self.lock_hard_drop = true + self.instant_hard_drop = true + self.instant_soft_drop = false + self.enable_hold = true + self.next_queue_length = 3 +end + +function LigneGame:getDropSpeed() + return 20 +end + +function LigneGame:getARR() + return 0 +end + +function LigneGame:getARE() + return 0 +end + +function LigneGame:getLineARE() + return self:getARE() +end + +function LigneGame:getDasLimit() + return 6 +end + +function LigneGame:getLineClearDelay() + return 0 +end + +function LigneGame:getLockDelay() + return 15 +end + +function LigneGame:getGravity() + return 1/64 +end + +function LigneGame:advanceOneFrame() + if self.clear then + self.roll_frames = self.roll_frames + 1 + if self.roll_frames > 150 then + self.completed = true + end + return false + elseif self.ready_frames == 0 then + self.frames = self.frames + 1 + end + return true +end + +function LigneGame:onPieceLock() + self.pieces = self.pieces + 1 +end + +function LigneGame:onLineClear(cleared_row_count) + if not self.clear then + self.lines = self.lines + cleared_row_count + if self.lines >= self.line_goal then + self.clear = true + end + end +end + +function LigneGame:drawGrid(ruleset) + self.grid:draw() + if self.piece ~= nil then + self:drawGhostPiece(ruleset) + end +end + +function LigneGame:getHighscoreData() + return { + level = self.level, + frames = self.frames, + } +end + +function LigneGame:drawScoringInfo() + LigneGame.super.drawScoringInfo(self) + love.graphics.setColor(1, 1, 1, 1) + + local text_x = config["side_next"] and 320 or 240 + + love.graphics.setFont(font_3x5_2) + love.graphics.printf("NEXT", 64, 40, 40, "left") + love.graphics.printf("LINES", text_x, 320, 40, "left") + love.graphics.printf("line/min", text_x, 160, 80, "left") + love.graphics.printf("piece/sec", text_x, 220, 80, "left") + + love.graphics.setFont(font_3x5_3) + love.graphics.printf(string.format("%.02f", self.lines / math.max(1, self.frames) * 3600), text_x, 180, 80, "left") + love.graphics.printf(string.format("%.04f", self.pieces / math.max(1, self.frames) * 60), text_x, 240, 80, "left") + + love.graphics.setFont(font_3x5_4) + love.graphics.printf(math.max(0, self.line_goal - self.lines), text_x, 340, 40, "left") +end + +function LigneGame:getBackground() + return 2 +end + +return LigneGame diff --git a/tetris/modes/marathon_2020.lua b/tetris/modes/marathon_2020.lua new file mode 100644 index 0000000..c8a0770 --- /dev/null +++ b/tetris/modes/marathon_2020.lua @@ -0,0 +1,446 @@ +require 'funcs' + +local GameMode = require 'tetris.modes.gamemode' +local Piece = require 'tetris.components.piece' + +local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls' + +local Marathon2020Game = GameMode:extend() + +Marathon2020Game.name = "Marathon 2020" +Marathon2020Game.hash = "Marathon2020" +Marathon2020Game.tagline = "2020 levels of pure pain! Can you achieve the World Master rank?" + +function Marathon2020Game:new() + Marathon2020Game.super:new() + + self.lock_drop = true + self.lock_hard_drop = true + self.enable_hold = true + self.next_queue_length = 3 + + self.delay_level = 0 + self.roll_frames = 0 + self.no_roll_frames = 0 + self.combo = 1 + self.randomizer = History6RollsRandomizer() + self.section_cool_count = 0 + self.section_status = { [0] = "none" } + self.torikan_passed = { + [500] = false, [900] = false, + [1000] = false, [1500] = false, [1900] = false + } + self.torikan_hit = false + + self.grade = 0 + self.grade_points = 0 + self.grade_point_decay_counter = 0 + self.max_grade_points = 0 +end + +function Marathon2020Game:getARE() + if self.delay_level < 1 then return 27 + elseif self.delay_level < 2 then return 24 + elseif self.delay_level < 3 then return 21 + elseif self.delay_level < 4 then return 18 + elseif self.delay_level < 5 then return 16 + elseif self.delay_level < 6 then return 14 + elseif self.delay_level < 7 then return 12 + elseif self.delay_level < 8 then return 10 + elseif self.delay_level < 9 then return 8 + elseif self.delay_level < 13 then return 6 + elseif self.delay_level < 15 then return 5 + else return 4 end +end + +function Marathon2020Game:getLineARE() + return self:getARE() +end + +function Marathon2020Game:getDasLimit() + if self.delay_level < 1 then return 15 + elseif self.delay_level < 3 then return 12 + elseif self.delay_level < 5 then return 9 + elseif self.delay_level < 8 then return 8 + elseif self.delay_level < 10 then return 7 + elseif self.delay_level < 13 then return 6 + elseif self.delay_level < 15 then return 5 + elseif self.delay_level < 20 then return 4 + else return 3 end +end + +function Marathon2020Game:getLineClearDelay() + if self.delay_level < 1 then return 40 + elseif self.delay_level < 3 then return 25 + elseif self.delay_level < 4 then return 20 + elseif self.delay_level < 5 then return 15 + elseif self.delay_level < 7 then return 12 + elseif self.delay_level < 9 then return 8 + elseif self.delay_level < 11 then return 6 + elseif self.delay_level < 14 then return 4 + else return 2 end +end + +function Marathon2020Game:getLockDelay() + if self.delay_level < 6 then return 30 + elseif self.delay_level < 7 then return 26 + elseif self.delay_level < 8 then return 22 + elseif self.delay_level < 9 then return 19 + elseif self.delay_level < 10 then return 17 + elseif self.delay_level < 16 then return 15 + elseif self.delay_level < 17 then return 13 + elseif self.delay_level < 18 then return 11 + elseif self.delay_level < 19 then return 10 + elseif self.delay_level < 20 then return 9 + else return 8 end +end + +function Marathon2020Game:getGravity() + if self.level < 30 then return 4/256 + elseif self.level < 35 then return 6/256 + elseif self.level < 40 then return 8/256 + elseif self.level < 50 then return 10/256 + elseif self.level < 60 then return 12/256 + elseif self.level < 70 then return 16/256 + elseif self.level < 80 then return 32/256 + elseif self.level < 90 then return 48/256 + elseif self.level < 100 then return 64/256 + elseif self.level < 120 then return 80/256 + elseif self.level < 140 then return 96/256 + elseif self.level < 160 then return 112/256 + elseif self.level < 170 then return 128/256 + elseif self.level < 200 then return 144/256 + elseif self.level < 220 then return 4/256 + elseif self.level < 230 then return 32/256 + elseif self.level < 233 then return 64/256 + elseif self.level < 236 then return 96/256 + elseif self.level < 239 then return 128/256 + elseif self.level < 243 then return 160/256 + elseif self.level < 247 then return 192/256 + elseif self.level < 251 then return 224/256 + elseif self.level < 300 then return 1 + elseif self.level < 330 then return 2 + elseif self.level < 360 then return 3 + elseif self.level < 400 then return 4 + elseif self.level < 420 then return 5 + elseif self.level < 450 then return 4 + elseif self.level < 500 then return 3 + else return 20 end +end + +local cleared_row_levels = {1, 2, 4, 6} + +function Marathon2020Game:advanceOneFrame() + if self.torikan_hit then + self.no_roll_frames = self.no_roll_frames + 1 + if self.no_roll_frames > 120 then + self.completed = true + end + return false + elseif self.clear then + self.roll_frames = self.roll_frames + 1 + if self.roll_frames < 0 then + return false + elseif self.roll_frames > 4000 then + self.completed = true + end + elseif self.ready_frames == 0 then + self.frames = self.frames + 1 + end + return true +end + +local cool_cutoffs = { + sp(0,45,00), sp(0,41,50), sp(0,38,50), sp(0,35,00), sp(0,32,50), + sp(0,29,20), sp(0,27,20), sp(0,24,80), sp(0,22,80), sp(0,20,60), + sp(0,19,60), sp(0,19,40), sp(0,19,40), sp(0,18,40), sp(0,18,20), + sp(0,16,20), sp(0,16,20), sp(0,16,20), sp(0,16,20), sp(0,16,20), + sp(0,15,20) +} + +local levels_for_cleared_rows = { 1, 2, 4, 6 } + +function Marathon2020Game:onPieceEnter() + self:updateLevel(1, false) +end + +function Marathon2020Game:whilePieceActive() + self.grade_point_decay_counter = self.grade_point_decay_counter + self.grade + 2 + if self.grade_point_decay_counter > 240 then + self.grade_point_decay_counter = 0 + self.grade_points = math.max(0, self.grade_points - 1) + end +end + +function Marathon2020Game:onLineClear(cleared_row_count) + self:updateLevel(levels_for_cleared_rows[cleared_row_count], true) + self:updateGrade(cleared_row_count) +end + +function Marathon2020Game:updateLevel(increment, line_clear) + local new_level + if self.torikan_passed[900] == false then + if line_clear == false and ( + math.floor((self.level + increment) / 100) > math.floor(self.level / 100) or + self.level == 998 + ) then + new_level = math.min(998, self.level + (99 - self.level % 100)) + else + new_level = math.min(999, self.level + increment) + end + elseif self.torikan_passed[1900] == false then + if line_clear == false and ( + math.floor((self.level + increment) / 100) > math.floor(self.level / 100) or + self.level == 1999 + ) then + new_level = math.min(1999, self.level + (99 - self.level % 100)) + else + new_level = math.min(2000, self.level + increment) + end + else + if line_clear == false and ( + self.level < 1900 and + math.floor((self.level + increment) / 100) > math.floor(self.level / 100) + ) then + new_level = self.level + (99 - self.level % 100) + elseif line_clear == false and self.level + increment > 2019 then + new_level = 2019 + else + new_level = math.min(2020, self.level + increment) + end + end + if not self.clear then + self:updateSectionTimes(self.level, new_level) + if not self.clear then + self.level = new_level + end + end +end + +local low_cleared_line_points = {10, 20, 30, 40} +local mid_cleared_line_points = {2, 6, 12, 24} +local high_cleared_line_points = {1, 4, 9, 20} + +local function getGradeForGradePoints(points) + return math.floor(math.sqrt((points / 50) * 8 + 1) / 2 - 0.5) + -- Don't be afraid of the above function. All it does is make it so that + -- you need 50 points to get to grade 1, 100 points to grade 2, etc. +end + +function Marathon2020Game:updateGrade(cleared_lines) + -- update grade points and max grade points + local point_level = math.floor(self.level / 100) + self.delay_level + local plus_points = math.max( + low_cleared_line_points[cleared_lines], + mid_cleared_line_points[cleared_lines] * (1 + point_level / 2), + high_cleared_line_points[cleared_lines] * (point_level - 2), + (self.level >= 1000 and cleared_lines == 4) and self.grade * 30 or 0 + ) + self.grade_points = self.grade_points + plus_points + if self.grade_points > self.max_grade_points then + self.max_grade_points = self.grade_points + end + self.grade = getGradeForGradePoints(self.max_grade_points) +end + +function Marathon2020Game:getTotalGrade() + return self.grade + self.section_cool_count +end + +local function getSectionForLevel(level) + if level < 2001 then + return math.floor(level / 100) + 1 + else + return 20 + end +end + +function Marathon2020Game:getEndOfSectionForSection(section) + if self.torikan_passed[900] == false and section == 10 then return 999 + elseif self.torikan_passed[1900] == false and section == 20 then return 2000 + elseif section == 20 then return 2020 + else return section * 100 end +end + +function Marathon2020Game:sectionPassed(old_level, new_level) + if self.torikan_passed[900] == false then + return ( + (math.floor(old_level / 100) < math.floor(new_level / 100)) or + (new_level >= 999) + ) + elseif self.torikan_passed[1900] == false then + return ( + (math.floor(old_level / 100) < math.floor(new_level / 100)) or + (new_level >= 2000) + ) + else + return ( + (new_level < 2001 and math.floor(old_level / 100) < math.floor(new_level / 100)) or + (new_level >= 2020) + ) + end +end + +function Marathon2020Game:checkTorikan(section) + if section == 5 and self.frames < sp(6,00,00) then self.torikan_passed[500] = true end + if section == 9 and self.frames < sp(8,30,00) then self.torikan_passed[900] = true end + if section == 10 and self.frames < sp(8,45,00) then self.torikan_passed[1000] = true end + if section == 15 and self.frames < sp(11,30,00) then self.torikan_passed[1500] = true end + if section == 19 and self.frames < sp(13,15,00) then self.torikan_passed[1900] = true end +end + +function Marathon2020Game:checkClear(level) + if ( + self.torikan_passed[500] == false and level >= 500 or + self.torikan_passed[900] == false and level >= 999 or + self.torikan_passed[1000] == false and level >= 1000 or + self.torikan_passed[1500] == false and level >= 1500 or + self.torikan_passed[1900] == false and level >= 2000 or + level >= 2020 + ) then + + if self.torikan_passed[500] == false then self.level = 500 + elseif self.torikan_passed[900] == false then self.level = 999 + elseif self.torikan_passed[1000] == false then self.level = 1000 + elseif self.torikan_passed[1500] == false then self.level = 1500 + elseif self.torikan_passed[1900] == false then self.level = 2000 + else self.level = 2020 end + + self.clear = true + self.grid:clear() + if ( + self.torikan_passed[900] == false and level >= 999 or + level >= 2020 + ) then + self.roll_frames = -150 + else + self.torikan_hit = true + self.no_roll_frames = -150 + end + end +end + +function Marathon2020Game:updateSectionTimes(old_level, new_level) + function sectionCool() + self.section_cool_count = self.section_cool_count + 1 + self.delay_level = math.min(20, self.delay_level + 1) + table.insert(self.section_status, "cool") + end + + local section = getSectionForLevel(old_level) + + if section <= 19 and old_level % 100 < 70 and new_level >= math.floor(old_level / 100) * 100 + 70 then + -- record section 70 time + section_70_time = self.frames - self.section_start_time + table.insert(self.secondary_section_times, section_70_time) + end + + if self:sectionPassed(old_level, new_level) then + -- record new section + section_time = self.frames - self.section_start_time + table.insert(self.section_times, section_time) + self.section_start_time = self.frames + + if section > 4 then self.delay_level = math.min(20, self.delay_level + 1) end + self:checkTorikan(section) + self:checkClear(new_level) + + if ( + section <= 19 and self.section_status[section - 1] == "cool" and + self.secondary_section_times[section] < self.secondary_section_times[section - 1] + 120 and + self.secondary_section_times[section] < cool_cutoffs[section] + ) then + sectionCool() + elseif self.section_status[section - 1] == "cool" then + table.insert(self.section_status, "none") + elseif section <= 19 and self.secondary_section_times[section] < cool_cutoffs[section] then + sectionCool() + else + table.insert(self.section_status, "none") + end + end +end + +function Marathon2020Game:updateScore(level, drop_bonus, cleared_lines) + if cleared_lines > 0 then + self.score = self.score + ( + (math.ceil((level + cleared_lines) / 4) + drop_bonus) * + cleared_lines * (cleared_lines * 2 - 1) * (self.combo * 2 - 1) + ) + self.lines = self.lines + cleared_lines + self.combo = self.combo + cleared_lines - 1 + else + self.drop_bonus = 0 + self.combo = 1 + end +end + +Marathon2020Game.mRollOpacityFunction = function(age) + if age > 300 then return 0 + elseif age < 240 then return 1 + else return (300 - age) / 60 end +end + +Marathon2020Game.rollOpacityFunction = function(age) + if age > 4 then return 0 + else return 1 - age / 4 end +end + +function Marathon2020Game:qualifiesForMRoll() + return false -- until I actually have grading working +end + +function Marathon2020Game:drawGrid() + if self.clear and not (self.completed or self.game_over) then + if self:qualifiesForMRoll() then + self.grid:drawInvisible(self.mRollOpacityFunction) + else + self.grid:drawInvisible(self.rollOpacityFunction) + end + else + self.grid:draw() + if self.piece ~= nil and self.level < 100 then + self:drawGhostPiece(ruleset) + end + end +end + +function Marathon2020Game:drawScoringInfo() + Marathon2020Game.super.drawScoringInfo(self) + + local current_section = getSectionForLevel(self.level) + local text_x = config["side_next"] and 320 or 240 + + love.graphics.setFont(font_3x5_2) + love.graphics.printf("GRADE", text_x, 100, 40, "left") + love.graphics.printf("GRADE PTS.", text_x, 200, 90, "left") + love.graphics.printf("LEVEL", text_x, 320, 40, "left") + + self:drawSectionTimesWithSecondary(current_section) + + love.graphics.setFont(font_3x5_3) + love.graphics.printf(self:getTotalGrade(), text_x, 120, 90, "left") + love.graphics.printf(self.grade_points, text_x, 220, 90, "left") + love.graphics.printf(self.level, text_x, 340, 50, "right") + + if self.clear then + love.graphics.printf(self.level, text_x, 370, 50, "right") + else + love.graphics.printf(self:getEndOfSectionForSection(current_section), text_x, 370, 50, "right") + end + +end + +function Marathon2020Game:getHighscoreData() + return { + grade = self.grade, + level = self.level, + frames = self.frames, + } +end + +function Marathon2020Game:getBackground() + return math.min(19, math.floor(self.level / 100)) +end + +return Marathon2020Game diff --git a/tetris/modes/marathon_a1.lua b/tetris/modes/marathon_a1.lua new file mode 100644 index 0000000..2356fad --- /dev/null +++ b/tetris/modes/marathon_a1.lua @@ -0,0 +1,226 @@ +require 'funcs' + +local GameMode = require 'tetris.modes.gamemode' +local Piece = require 'tetris.components.piece' + +local History4RollsRandomizer = require 'tetris.randomizers.history_4rolls' + +local MarathonA1Game = GameMode:extend() + +MarathonA1Game.name = "Marathon A1" +MarathonA1Game.hash = "MarathonA1" +MarathonA1Game.tagline = "Can you score enough points to reach the title of Grand Master?" + +MarathonA1Game.arr = 1 +MarathonA1Game.drop_speed = 1 + +function MarathonA1Game:new() + MarathonA1Game.super:new() + + self.roll_frames = 0 + self.combo = 1 + self.gm_conditions = { + level300 = false, + level500 = false, + level999 = false + } + + self.randomizer = History4RollsRandomizer() + + self.lock_drop = false + self.enable_hard_drop = false + self.enable_hold = false + self.next_queue_length = 1 +end + +function MarathonA1Game:getARE() + return 25 +end + +function MarathonA1Game:getLineARE() + return 25 +end + +function MarathonA1Game:getDasLimit() + return 15 +end + +function MarathonA1Game:getLineClearDelay() + return 40 +end + +function MarathonA1Game:getLockDelay() + return 30 +end + +local function getRankForScore(score) + if score < 400 then return {rank = "9", next = 400} + elseif score < 800 then return {rank = "8", next = 800} + elseif score < 1400 then return {rank = "7", next = 1400} + elseif score < 2000 then return {rank = "6", next = 2000} + elseif score < 3500 then return {rank = "5", next = 3500} + elseif score < 5500 then return {rank = "4", next = 5500} + elseif score < 8000 then return {rank = "3", next = 8000} + elseif score < 12000 then return {rank = "2", next = 12000} + elseif score < 16000 then return {rank = "1", next = 16000} + elseif score < 22000 then return {rank = "S1", next = 22000} + elseif score < 30000 then return {rank = "S2", next = 30000} + elseif score < 40000 then return {rank = "S3", next = 40000} + elseif score < 52000 then return {rank = "S4", next = 52000} + elseif score < 66000 then return {rank = "S5", next = 66000} + elseif score < 82000 then return {rank = "S6", next = 82000} + elseif score < 100000 then return {rank = "S7", next = 100000} + elseif score < 120000 then return {rank = "S8", next = 120000} + else return {rank = "S9", next = "???"} + end +end + +function MarathonA1Game:getGravity() + local level = self.level + if (level < 30) then return 4/256 + elseif (level < 35) then return 6/256 + elseif (level < 40) then return 8/256 + elseif (level < 50) then return 10/256 + elseif (level < 60) then return 12/256 + elseif (level < 70) then return 16/256 + elseif (level < 80) then return 32/256 + elseif (level < 90) then return 48/256 + elseif (level < 100) then return 64/256 + elseif (level < 120) then return 80/256 + elseif (level < 140) then return 96/256 + elseif (level < 160) then return 112/256 + elseif (level < 170) then return 128/256 + elseif (level < 200) then return 144/256 + elseif (level < 220) then return 4/256 + elseif (level < 230) then return 32/256 + elseif (level < 233) then return 64/256 + elseif (level < 236) then return 96/256 + elseif (level < 239) then return 128/256 + elseif (level < 243) then return 160/256 + elseif (level < 247) then return 192/256 + elseif (level < 251) then return 224/256 + elseif (level < 300) then return 1 + elseif (level < 330) then return 2 + elseif (level < 360) then return 3 + elseif (level < 400) then return 4 + elseif (level < 420) then return 5 + elseif (level < 450) then return 4 + elseif (level < 500) then return 3 + else return 20 + end +end + +function MarathonA1Game:advanceOneFrame() + if self.clear then + self.roll_frames = self.roll_frames + 1 + if self.roll_frames > 2968 then + self.completed = true + end + elseif self.ready_frames == 0 then + self.frames = self.frames + 1 + end + return true +end + +function MarathonA1Game:onPieceEnter() + if (self.level % 100 ~= 99 and self.level ~= 998) and not self.clear and self.frames ~= 0 then + self.level = self.level + 1 + end +end + +function MarathonA1Game:onLineClear(cleared_row_count) + self:checkGMRequirements(self.level, self.level + cleared_row_count) + if not self.clear then + local new_level = math.min(self.level + cleared_row_count, 999) + if self.level == 999 then + self.clear = true + else + self.level = new_level + end + end +end + +function MarathonA1Game:updateScore(level, drop_bonus, cleared_lines) + if cleared_lines > 0 then + self.score = self.score + ( + (math.ceil((level + cleared_lines) / 4) + drop_bonus) * + cleared_lines * (cleared_lines * 2 - 1) * self.combo + ) + self.lines = self.lines + cleared_lines + self.combo = self.combo + (cleared_lines - 1) * 2 + else + self.drop_bonus = 0 + self.combo = 1 + end +end + +function MarathonA1Game:checkGMRequirements(old_level, new_level) + if old_level < 300 and new_level >= 300 then + if self.score > 12000 and self.frames <= sp(4,15) then + self.gm_conditions["level300"] = true + end + elseif old_level < 500 and new_level >= 500 then + if self.score > 40000 and self.frames <= sp(7,30) then + self.gm_conditions["level500"] = true + end + elseif old_level < 999 and new_level >= 999 then + if self.score > 126000 and self.frames <= sp(13,30) then + self.gm_conditions["level900"] = true + end + end +end + +function MarathonA1Game:drawGrid() + self.grid:draw() +end + +function MarathonA1Game:drawScoringInfo() + MarathonA1Game.super.drawScoringInfo(self) + love.graphics.setColor(1, 1, 1, 1) + + love.graphics.setFont(font_3x5_2) + love.graphics.print( + self.das.direction .. " " .. + self.das.frames .. " " .. + st(self.prev_inputs) + ) + love.graphics.printf("NEXT", 64, 40, 40, "left") + love.graphics.printf("GRADE", 240, 120, 40, "left") + love.graphics.printf("SCORE", 240, 200, 40, "left") + love.graphics.printf("NEXT RANK", 240, 260, 90, "left") + love.graphics.printf("LEVEL", 240, 320, 40, "left") + + love.graphics.setFont(font_3x5_3) + love.graphics.printf(self.score, 240, 220, 90, "left") + if self.gm_conditions["level300"] and self.gm_conditions["level500"] and self.gm_conditions["level900"] then + love.graphics.printf("GM", 240, 140, 90, "left") + else + love.graphics.printf(getRankForScore(self.score).rank, 240, 140, 90, "left") + end + love.graphics.printf(getRankForScore(self.score).next, 240, 280, 90, "left") + love.graphics.printf(self.level, 240, 340, 40, "right") + love.graphics.printf(self:getSectionEndLevel(), 240, 370, 40, "right") + + love.graphics.setFont(font_8x11) + love.graphics.printf(formatTime(self.frames), 64, 420, 160, "center") +end + +function MarathonA1Game:getSectionEndLevel() + if self.level > 900 then return 999 + else return math.floor(self.level / 100 + 1) * 100 end +end + +function MarathonA1Game:getBackground() + return math.floor(self.level / 100) +end + +function MarathonA1Game:getHighscoreData() + return { + grade = self.grade, + score = self.score, + level = self.level, + frames = self.frames, + } +end + +return MarathonA1Game diff --git a/tetris/modes/marathon_a2.lua b/tetris/modes/marathon_a2.lua new file mode 100644 index 0000000..e8f0c34 --- /dev/null +++ b/tetris/modes/marathon_a2.lua @@ -0,0 +1,363 @@ +require 'funcs' + +local GameMode = require 'tetris.modes.gamemode' +local Piece = require 'tetris.components.piece' + +local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls' + +local MarathonA2Game = GameMode:extend() + +MarathonA2Game.name = "Marathon A2" +MarathonA2Game.hash = "MarathonA2" +MarathonA2Game.tagline = "The points don't matter! Can you reach the invisible roll?" + +MarathonA2Game.arr = 1 +MarathonA2Game.drop_speed = 1 + +function MarathonA2Game:new() + MarathonA2Game.super:new() + + self.roll_frames = 0 + self.combo = 1 + self.randomizer = History6RollsRandomizer() + self.grade = 0 + self.grade_points = 0 + self.grade_point_decay_counter = 0 + self.section_start_time = 0 + self.section_times = { [0] = 0 } + self.section_tetrises = { [0] = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } + + self.randomizer = History6RollsRandomizer() + + self.lock_drop = false + self.enable_hold = false + self.next_queue_length = 1 +end + +function MarathonA2Game:getARE() + if self.level < 700 then return 25 + elseif self.level < 800 then return 16 + else return 12 end +end + +function MarathonA2Game:getLineARE() + if self.level < 600 then return 25 + elseif self.level < 700 then return 16 + elseif self.level < 800 then return 12 + else return 6 end +end + +function MarathonA2Game:getDasLimit() + if self.level < 500 then return 15 + elseif self.level < 900 then return 9 + else return 7 end +end + +function MarathonA2Game:getLineClearDelay() + if self.level < 500 then return 40 + elseif self.level < 600 then return 25 + elseif self.level < 700 then return 16 + elseif self.level < 800 then return 12 + else return 6 end +end + +function MarathonA2Game:getLockDelay() + if self.level < 900 then return 30 + else return 17 end +end + +function MarathonA2Game:getGravity() + if (self.level < 30) then return 4/256 + elseif (self.level < 35) then return 6/256 + elseif (self.level < 40) then return 8/256 + elseif (self.level < 50) then return 10/256 + elseif (self.level < 60) then return 12/256 + elseif (self.level < 70) then return 16/256 + elseif (self.level < 80) then return 32/256 + elseif (self.level < 90) then return 48/256 + elseif (self.level < 100) then return 64/256 + elseif (self.level < 120) then return 80/256 + elseif (self.level < 140) then return 96/256 + elseif (self.level < 160) then return 112/256 + elseif (self.level < 170) then return 128/256 + elseif (self.level < 200) then return 144/256 + elseif (self.level < 220) then return 4/256 + elseif (self.level < 230) then return 32/256 + elseif (self.level < 233) then return 64/256 + elseif (self.level < 236) then return 96/256 + elseif (self.level < 239) then return 128/256 + elseif (self.level < 243) then return 160/256 + elseif (self.level < 247) then return 192/256 + elseif (self.level < 251) then return 224/256 + elseif (self.level < 300) then return 1 + elseif (self.level < 330) then return 2 + elseif (self.level < 360) then return 3 + elseif (self.level < 400) then return 4 + elseif (self.level < 420) then return 5 + elseif (self.level < 450) then return 4 + elseif (self.level < 500) then return 3 + else return 20 + end +end + +function MarathonA2Game:advanceOneFrame() + if self.clear then + self.roll_frames = self.roll_frames + 1 + if self.roll_frames > 3694 then + self.completed = true + if self.grade == 32 then + self.grade = 33 + end + end + elseif self.ready_frames == 0 then + self.frames = self.frames + 1 + end + return true +end + +function MarathonA2Game:onPieceEnter() + if (self.level % 100 ~= 99 and self.level ~= 998) and not self.clear and self.frames ~= 0 then + self.level = self.level + 1 + end +end + +function MarathonA2Game:onLineClear(cleared_row_count) + self:updateSectionTimes(self.level, self.level + cleared_row_count) + self.level = math.min(self.level + cleared_row_count, 999) + if self.level == 999 and not self.clear then + self.clear = true + if self:qualifiesForMRoll() then + self.grade = 32 + end + self.grid:clear() + self.roll_frames = -150 + end +end + +function MarathonA2Game:updateScore(level, drop_bonus, cleared_lines) + self:updateGrade(cleared_lines) + if cleared_lines > 0 then + self.score = self.score + ( + (math.ceil((level + cleared_lines) / 4) + drop_bonus) * + cleared_lines * (cleared_lines * 2 - 1) * (self.combo * 2 - 1) + ) + self.lines = self.lines + cleared_lines + self.combo = self.combo + cleared_lines - 1 + else + self.drop_bonus = 0 + self.combo = 1 + end +end + +function MarathonA2Game:updateSectionTimes(old_level, new_level) + if self.clear then return end + if math.floor(old_level / 100) < math.floor(new_level / 100) or + new_level >= 999 then + -- record new section + section_time = self.frames - self.section_start_time + self.section_times[math.floor(old_level / 100)] = section_time + self.section_start_time = self.frames + end +end + +local grade_point_bonuses = { + {10, 20, 40, 50}, + {10, 20, 30, 40}, + {10, 20, 30, 40}, + {10, 15, 30, 40}, + {10, 15, 20, 40}, + {5, 15, 20, 30}, + {5, 10, 20, 30}, + {5, 10, 15, 30}, + {5, 10, 15, 30}, + {5, 10, 15, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, +} + +local grade_point_decays = { + 125, 80, 80, 50, 45, 45, 45, + 40, 40, 40, 40, 40, 30, 30, 30, + 20, 20, 20, 20, 20, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 10, 10 +} + +local combo_multipliers = { + {1.0, 1.0, 1.0, 1.0}, + {1.2, 1.4, 1.5, 1.0}, + {1.2, 1.5, 1.8, 1.0}, + {1.4, 1.6, 2.0, 1.0}, + {1.4, 1.7, 2.2, 1.0}, + {1.4, 1.8, 2.3, 1.0}, + {1.4, 1.9, 2.4, 1.0}, + {1.5, 2.0, 2.5, 1.0}, + {1.5, 2.1, 2.6, 1.0}, + {2.0, 2.5, 3.0, 1.0}, +} + +local grade_conversion = { + [0] = 0, + 1, 2, 3, 4, 5, 5, 6, 6, 7, 7, + 7, 8, 8, 8, 9, 9, 9, 10, 11, 12, + 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, + 17, 18, 19 +} + +function MarathonA2Game:updateGrade(cleared_lines) + if self.clear then return end + if cleared_lines == 0 then + self.grade_point_decay_counter = self.grade_point_decay_counter + 1 + if self.grade_point_decay_counter >= grade_point_decays[self.grade + 1] then + self.grade_point_decay_counter = 0 + self.grade_points = math.max(0, self.grade_points - 1) + end + else + self.grade_points = self.grade_points + ( + math.ceil( + grade_point_bonuses[self.grade + 1][cleared_lines] * + combo_multipliers[math.min(self.combo, 10)][cleared_lines] + ) * (1 + math.floor(self.level / 250)) + ) + if self.grade_points >= 100 and self.grade < 31 then + self.grade_points = 0 + self.grade = self.grade + 1 + end + end +end + +local tetris_requirements = { [0] = 2, 2, 2, 2, 2, 1, 1, 1, 1, 1 } + +function MarathonA2Game:qualifiesForMRoll() + if not self.clear then return false end + -- tetris requirements + for section = 0, 9 do + if self.section_tetrises[section] < tetris_requirements[section] then + return false + end + end + -- section time requirements + local section_average = 0 + for section = 0, 4 do + section_average = section_average + self.section_times[section] + if self.section_times[section] > sp(1,05) then + return false + end + end + -- section time average requirements + if self.section_times[5] > section_average / 5 then + return false + end + for section = 6, 9 do + if self.section_times[section] > self.section_times[section - 1] + 120 then + return false + end + end + if self.grade < 17 or self.frames > sp(8,45) then + return false + end + return true +end + +function MarathonA2Game:getLetterGrade() + local grade = grade_conversion[self.grade] + if grade < 9 then + return tostring(9 - grade) + elseif grade < 18 then + return "S" .. tostring(grade - 8) + elseif grade == 18 then + return "M" + else + return "GM" + end +end + +MarathonA2Game.rollOpacityFunction = function(age) + if age < 240 then return 1 + elseif age > 300 then return 0 + else return 1 - (age - 240) / 60 end +end + +MarathonA2Game.mRollOpacityFunction = function(age) + if age > 4 then return 0 + else return 1 - age / 4 end +end + +function MarathonA2Game:drawGrid(ruleset) + if self.clear and not (self.completed or self.game_over) then + if self:qualifiesForMRoll() then + self.grid:drawInvisible(self.mRollOpacityFunction) + else + self.grid:drawInvisible(self.rollOpacityFunction) + end + else + self.grid:draw() + if self.piece ~= nil and self.level < 100 then + self:drawGhostPiece(ruleset) + end + end +end + +function MarathonA2Game:drawScoringInfo() + love.graphics.setColor(1, 1, 1, 1) + + love.graphics.setFont(font_3x5_2) + love.graphics.print( + self.das.direction .. " " .. + self.das.frames .. " " .. + st(self.prev_inputs) + ) + love.graphics.printf("NEXT", 64, 40, 40, "left") + love.graphics.printf("GRADE", 240, 120, 40, "left") + love.graphics.printf("SCORE", 240, 200, 40, "left") + love.graphics.printf("LEVEL", 240, 320, 40, "left") + + love.graphics.setFont(font_3x5_3) + love.graphics.printf(self:getLetterGrade(), 240, 140, 90, "left") + love.graphics.printf(self.score, 240, 220, 90, "left") + love.graphics.printf(self.level, 240, 340, 40, "right") + love.graphics.printf(self:getSectionEndLevel(), 240, 370, 40, "right") + + love.graphics.setFont(font_8x11) + love.graphics.printf(formatTime(self.frames), 64, 420, 160, "center") +end + +function MarathonA2Game:getHighscoreData() + return { + grade = grade_conversion[self.grade], + score = self.score, + level = self.level, + frames = self.frames, + } +end + +function MarathonA2Game:getSectionEndLevel() + if self.level > 900 then return 999 + else return math.floor(self.level / 100 + 1) * 100 end +end + +function MarathonA2Game:getBackground() + return math.floor(self.level / 100) +end + +return MarathonA2Game diff --git a/tetris/modes/marathon_a3.lua b/tetris/modes/marathon_a3.lua new file mode 100644 index 0000000..3919447 --- /dev/null +++ b/tetris/modes/marathon_a3.lua @@ -0,0 +1,431 @@ +require 'funcs' + +local GameMode = require 'tetris.modes.gamemode' +local Piece = require 'tetris.components.piece' + +local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls_35bag' + +local MarathonA3Game = GameMode:extend() + +MarathonA3Game.name = "Marathon A3" +MarathonA3Game.hash = "MarathonA3" +MarathonA3Game.tagline = "The game gets faster way more quickly! Can you get all the Section COOLs?" + +MarathonA3Game.arr = 1 +MarathonA3Game.drop_speed = 1 + +function MarathonA3Game:new() + MarathonA3Game.super:new() + + self.speed_level = 0 + self.roll_frames = 0 + self.combo = 1 + self.grade = 0 + self.grade_points = 0 + self.roll_points = 0 + self.grade_point_decay_counter = 0 + self.section_cool_grade = 0 + self.section_status = { [0] = "none" } + self.section_start_time = 0 + self.section_70_times = { [0] = 0 } + self.section_times = { [0] = 0 } + + self.randomizer = History6RollsRandomizer() + + self.lock_drop = true + self.lock_hard_drop = true + self.enable_hold = true + self.next_queue_length = 3 +end + +function MarathonA3Game:getARE() + if self.speed_level < 700 then return 27 + elseif self.speed_level < 800 then return 18 + elseif self.speed_level < 1000 then return 14 + elseif self.speed_level < 1100 then return 8 + elseif self.speed_level < 1200 then return 7 + else return 6 end +end + +function MarathonA3Game:getLineARE() + if self.speed_level < 600 then return 27 + elseif self.speed_level < 700 then return 18 + elseif self.speed_level < 800 then return 14 + elseif self.speed_level < 1100 then return 8 + elseif self.speed_level < 1200 then return 7 + else return 6 end +end + +function MarathonA3Game:getDasLimit() + if self.speed_level < 500 then return 15 + elseif self.speed_level < 900 then return 9 + else return 7 end +end + +function MarathonA3Game:getLineClearDelay() + if self.speed_level < 500 then return 40 + elseif self.speed_level < 600 then return 25 + elseif self.speed_level < 700 then return 16 + elseif self.speed_level < 800 then return 12 + else return 6 end +end + +function MarathonA3Game:getLockDelay() + if self.speed_level < 900 then return 30 + elseif self.speed_level < 1100 then return 17 + else return 15 end +end + +function MarathonA3Game:getGravity() + if (self.speed_level < 30) then return 4/256 + elseif (self.speed_level < 35) then return 6/256 + elseif (self.speed_level < 40) then return 8/256 + elseif (self.speed_level < 50) then return 10/256 + elseif (self.speed_level < 60) then return 12/256 + elseif (self.speed_level < 70) then return 16/256 + elseif (self.speed_level < 80) then return 32/256 + elseif (self.speed_level < 90) then return 48/256 + elseif (self.speed_level < 100) then return 64/256 + elseif (self.speed_level < 120) then return 80/256 + elseif (self.speed_level < 140) then return 96/256 + elseif (self.speed_level < 160) then return 112/256 + elseif (self.speed_level < 170) then return 128/256 + elseif (self.speed_level < 200) then return 144/256 + elseif (self.speed_level < 220) then return 4/256 + elseif (self.speed_level < 230) then return 32/256 + elseif (self.speed_level < 233) then return 64/256 + elseif (self.speed_level < 236) then return 96/256 + elseif (self.speed_level < 239) then return 128/256 + elseif (self.speed_level < 243) then return 160/256 + elseif (self.speed_level < 247) then return 192/256 + elseif (self.speed_level < 251) then return 224/256 + elseif (self.speed_level < 300) then return 1 + elseif (self.speed_level < 330) then return 2 + elseif (self.speed_level < 360) then return 3 + elseif (self.speed_level < 400) then return 4 + elseif (self.speed_level < 420) then return 5 + elseif (self.speed_level < 450) then return 4 + elseif (self.speed_level < 500) then return 3 + else return 20 + end +end + +function MarathonA3Game:advanceOneFrame() + if self.clear then + self.roll_frames = self.roll_frames + 1 + if self.roll_frames < 0 then + if self.roll_frames + 1 == 0 then + switchBGM("credit_roll", "gm3") + end + return + elseif self.roll_frames > 3238 then + if self:qualifiesForMRoll() then + self.roll_points = self.roll_points + 160 + else + self.roll_points = self.roll_points + 50 + end + switchBGM(nil) + self.completed = true + end + elseif self.ready_frames == 0 then + self.frames = self.frames + 1 + end + return true +end + +function MarathonA3Game:onPieceEnter() + if (self.level % 100 ~= 99) and self.level ~= 998 and self.frames ~= 0 then + self:updateSectionTimes(self.level, self.level + 1) + self.level = self.level + 1 + self.speed_level = self.speed_level + 1 + end +end + +local cleared_row_levels = {1, 2, 4, 6} + +function MarathonA3Game:onLineClear(cleared_row_count) + local advanced_levels = cleared_row_levels[cleared_row_count] + self:updateSectionTimes(self.level, self.level + advanced_levels) + if not self.clear then + self.level = math.min(self.level + advanced_levels, 999) + end + self.speed_level = self.speed_level + advanced_levels + if self.level == 999 and not self.clear then + self.clear = true + self.grid:clear() + self.roll_frames = -150 + end +end + +local cool_cutoffs = { + sp(0,52), sp(0,52), sp(0,49), sp(0,45), sp(0,45), + sp(0,42), sp(0,42), sp(0,38), sp(0,38), +} + +local regret_cutoffs = { + sp(0,90), sp(0,75), sp(0,75), sp(0,68), sp(0,60), + sp(0,60), sp(0,50), sp(0,50), sp(0,50), sp(0,50), +} + +function MarathonA3Game:updateSectionTimes(old_level, new_level) + if self.clear then return end + local section = math.floor(old_level / 100) + 1 + if math.floor(old_level / 100) < math.floor(new_level / 100) or + new_level >= 999 then + -- record new section + section_time = self.frames - self.section_start_time + table.insert(self.section_times, section_time) + self.section_start_time = self.frames + + if section_time > regret_cutoffs[section] then + self.section_cool_grade = self.section_cool_grade - 1 + table.insert(self.section_status, "regret") + elseif section <= 9 and self.section_status[section - 1] == "cool" and + self.section_70_times[section] < self.section_70_times[section - 1] + 1000 then + self.section_cool_grade = self.section_cool_grade + 1 + self.speed_level = self.speed_level + 100 + table.insert(self.section_status, "cool") + elseif self.section_status[section - 1] == "cool" then + table.insert(self.section_status, "none") + elseif section <= 9 and self.section_70_times[section] < cool_cutoffs[section] then + self.section_cool_grade = self.section_cool_grade + 1 + self.speed_level = self.speed_level + 100 + table.insert(self.section_status, "cool") + else + table.insert(self.section_status, "none") + end + elseif section <= 9 and old_level % 100 < 70 and new_level % 100 >= 70 then + -- record section 70 time + section_70_time = self.frames - self.section_start_time + table.insert(self.section_70_times, section_70_time) + end +end + +function MarathonA3Game:updateScore(level, drop_bonus, cleared_lines) + self:updateGrade(cleared_lines) + if cleared_lines > 0 then + self.score = self.score + ( + (math.ceil((level + cleared_lines) / 4) + drop_bonus) * + cleared_lines * (cleared_lines * 2 - 1) * (self.combo * 2 - 1) + ) + self.lines = self.lines + cleared_lines + self.combo = self.combo + cleared_lines - 1 + else + self.drop_bonus = 0 + self.combo = 1 + end +end + +local grade_point_bonuses = { + {10, 20, 40, 50}, + {10, 20, 30, 40}, + {10, 20, 30, 40}, + {10, 15, 30, 40}, + {10, 15, 20, 40}, + {5, 15, 20, 30}, + {5, 10, 20, 30}, + {5, 10, 15, 30}, + {5, 10, 15, 30}, + {5, 10, 15, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, + {2, 12, 13, 30}, +} + +local grade_point_decays = { + 125, 80, 80, 50, 45, 45, 45, + 40, 40, 40, 40, 40, 30, 30, 30, + 20, 20, 20, 20, 20, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 10, 10 +} + +local combo_multipliers = { + {1.0, 1.0, 1.0, 1.0}, + {1.0, 1.2, 1.4, 1.5}, + {1.0, 1.2, 1.5, 1.8}, + {1.0, 1.4, 1.6, 2.0}, + {1.0, 1.4, 1.7, 2.2}, + {1.0, 1.4, 1.8, 2.3}, + {1.0, 1.4, 1.9, 2.4}, + {1.0, 1.5, 2.0, 2.5}, + {1.0, 1.5, 2.1, 2.6}, + {1.0, 2.0, 2.5, 3.0}, +} + +local roll_points = {4, 8, 12, 26} +local mroll_points = {10, 20, 30, 100} + +local grade_conversion = { + [0] = 0, + 1, 2, 3, 4, 5, 5, 6, 6, 7, 7, + 7, 8, 8, 8, 9, 9, 9, 10, 11, 12, 12, + 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, + 17 +} + +function MarathonA3Game:updateGrade(cleared_lines) + if cleared_lines == 0 then + self.grade_point_decay_counter = self.grade_point_decay_counter + 1 + if self.grade_point_decay_counter >= grade_point_decays[self.grade + 1] then + self.grade_point_decay_counter = 0 + self.grade_points = math.max(0, self.grade_points - 1) + end + else + if self.clear then + if self:qualifiesForMRoll() then + self.roll_points = self.roll_points + mroll_points[cleared_lines] + else + self.roll_points = self.roll_points + roll_points[cleared_lines] + end + else + self.grade_points = self.grade_points + ( + math.ceil( + grade_point_bonuses[self.grade + 1][cleared_lines] * + combo_multipliers[math.min(self.combo, 10)][cleared_lines] + ) * (1 + math.floor(self.level / 250)) + ) + if self.grade_points >= 100 and self.grade < 31 then + self.grade_points = 0 + self.grade = self.grade + 1 + end + end + end +end + +function MarathonA3Game:qualifiesForMRoll() + return self.grade >= 27 and self.section_cool_grade >= 9 +end + +function MarathonA3Game:getAggregateGrade() + return self.section_cool_grade + math.floor(self.roll_points / 100) + grade_conversion[self.grade] +end + +local master_grades = { "M", "MK", "MV", "MO", "MM" } + +function MarathonA3Game:getLetterGrade() + local grade = self:getAggregateGrade() + if grade < 9 then + return tostring(9 - grade) + elseif grade < 18 then + return "S" .. tostring(grade - 8) + elseif grade < 27 then + return "M" .. tostring(grade - 17) + elseif grade < 32 then + return master_grades[grade - 26] + else + return "GM" + end +end + +function MarathonA3Game:drawGrid() + if self.clear and not (self.completed or self.game_over) then + if self:qualifiesForMRoll() then + self.grid:drawInvisible(self.mRollOpacityFunction) + else + self.grid:drawInvisible(self.rollOpacityFunction) + end + else + self.grid:draw() + end +end + +MarathonA3Game.rollOpacityFunction = function(age) + if age < 240 then return 1 + elseif age > 300 then return 0 + else return 1 - (age - 240) / 60 end +end + +MarathonA3Game.mRollOpacityFunction = function(age) + if age > 4 then return 0 + else return 1 - age / 4 end +end + +function MarathonA3Game:drawScoringInfo() + love.graphics.setColor(1, 1, 1, 1) + + love.graphics.setFont(font_3x5_2) + love.graphics.print( + self.das.direction .. " " .. + self.das.frames .. " " .. + st(self.prev_inputs) + ) + love.graphics.printf("NEXT", 64, 40, 40, "left") + love.graphics.printf("GRADE", 240, 120, 40, "left") + love.graphics.printf("SCORE", 240, 200, 40, "left") + love.graphics.printf("LEVEL", 240, 320, 40, "left") + + -- draw section time data + current_section = math.floor(self.level / 100) + 1 + + section_x = 530 + section_70_x = 440 + + for section, time in pairs(self.section_times) do + if section > 0 then + love.graphics.printf(formatTime(time), section_x, 40 + 20 * section, 90, "left") + end + end + + for section, time in pairs(self.section_70_times) do + if section > 0 then + love.graphics.printf(formatTime(time), section_70_x, 40 + 20 * section, 90, "left") + end + end + + local current_x + if table.getn(self.section_times) < table.getn(self.section_70_times) then + current_x = section_x + else + current_x = section_70_x + end + + love.graphics.printf(formatTime(self.frames - self.section_start_time), current_x, 40 + 20 * current_section, 90, "left") + + love.graphics.setFont(font_3x5_3) + love.graphics.printf(self.score, 240, 220, 90, "left") + love.graphics.printf(self:getLetterGrade(), 240, 140, 90, "left") + love.graphics.printf(self.level, 240, 340, 40, "right") + love.graphics.printf(self:getSectionEndLevel(), 240, 370, 40, "right") + + love.graphics.setFont(font_8x11) + love.graphics.printf(formatTime(self.frames), 64, 420, 160, "center") +end + +function MarathonA3Game:getHighscoreData() + return { + grade = self:getAggregateGrade(), + level = self.level, + frames = self.frames, + } +end + +function MarathonA3Game:getSectionEndLevel() + if self.level > 900 then return 999 + else return math.floor(self.level / 100 + 1) * 100 end +end + +function MarathonA3Game:getBackground() + return math.floor(self.level / 100) +end + +return MarathonA3Game diff --git a/tetris/modes/marathon_l1.lua b/tetris/modes/marathon_l1.lua new file mode 100644 index 0000000..42fa954 --- /dev/null +++ b/tetris/modes/marathon_l1.lua @@ -0,0 +1,176 @@ +require 'funcs' + +local GameMode = require 'tetris.modes.gamemode' +local Piece = require 'tetris.components.piece' + +local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls' + +local MarathonL1Game = GameMode:extend() + +MarathonL1Game.name = "Line Attack" +MarathonL1Game.hash = "MarathonL1" +MarathonL1Game.tagline = "Can you clear the time hurdles when the game goes this fast?" + +MarathonL1Game.arr = 1 +MarathonL1Game.drop_speed = 1 + +function MarathonL1Game:new() + MarathonL1Game.super:new() + + self.roll_frames = 0 + self.randomizer = History6RollsRandomizer() + + self.section_time_limit = 3600 + self.section_start_time = 0 + self.section_times = { [0] = 0 } + self.section_clear = false + + self.lock_drop = true + self.enable_hold = true + self.next_queue_length = 3 +end + +function MarathonL1Game:getARE() + if self.lines < 10 then return 18 + elseif self.lines < 40 then return 14 + elseif self.lines < 60 then return 12 + elseif self.lines < 70 then return 10 + elseif self.lines < 80 then return 8 + elseif self.lines < 90 then return 7 + else return 6 end +end + +function MarathonL1Game:getLineARE() + return self:getARE() +end + +function MarathonL1Game:getDasLimit() + if self.lines < 20 then return 10 + elseif self.lines < 50 then return 9 + elseif self.lines < 70 then return 8 + else return 7 end +end + +function MarathonL1Game:getLineClearDelay() + if self.lines < 10 then return 14 + elseif self.lines < 30 then return 9 + else return 5 end +end + +function MarathonL1Game:getLockDelay() + if self.lines < 10 then return 28 + elseif self.lines < 20 then return 24 + elseif self.lines < 30 then return 22 + elseif self.lines < 40 then return 20 + elseif self.lines < 50 then return 18 + elseif self.lines < 70 then return 14 + else return 13 end +end + +function MarathonL1Game:getGravity() + return 20 +end + +function MarathonL1Game:getSection() + return math.floor(level / 100) + 1 +end + +function MarathonL1Game:advanceOneFrame() + if self.clear then + self.roll_frames = self.roll_frames + 1 + if self.roll_frames < 0 then + return false + elseif self.roll_frames > 2968 then + self.completed = true + end + elseif self.ready_frames == 0 then + if not self.section_clear then + self.frames = self.frames + 1 + end + if self:getSectionTime() >= self.section_time_limit then + self.game_over = true + end + end + return true +end + +function MarathonL1Game:onLineClear(cleared_row_count) + if not self.clear then + local new_lines = self.lines + cleared_row_count + self:updateSectionTimes(self.lines, new_lines) + self.lines = math.min(new_lines, 150) + if self.lines == 150 then + self.clear = true + self.roll_frames = -150 + end + end +end + +function MarathonL1Game:getSectionTime() + return self.frames - self.section_start_time +end + +function MarathonL1Game:updateSectionTimes(old_lines, new_lines) + if math.floor(old_lines / 10) < math.floor(new_lines / 10) then + -- record new section + table.insert(self.section_times, self:getSectionTime()) + self.section_start_time = self.frames + self.section_clear = true + end +end + +function MarathonL1Game:onPieceEnter() + self.section_clear = false +end + +function MarathonL1Game:drawGrid(ruleset) + self.grid:draw() +end + +function MarathonL1Game:getHighscoreData() + return { + lines = self.lines, + frames = self.frames, + } +end + +function MarathonL1Game:drawScoringInfo() + MarathonL1Game.super.drawScoringInfo(self) + + love.graphics.setColor(1, 1, 1, 1) + + love.graphics.setFont(font_3x5_2) + love.graphics.print( + self.das.direction .. " " .. + self.das.frames .. " " .. + st(self.prev_inputs) + ) + love.graphics.printf("NEXT", 64, 40, 40, "left") + love.graphics.printf("TIME LEFT", 240, 250, 80, "left") + love.graphics.printf("LINES", 240, 320, 40, "left") + + local current_section = math.floor(self.lines / 10) + 1 + self:drawSectionTimesWithSplits(current_section) + + love.graphics.setFont(font_3x5_3) + love.graphics.printf(self.lines, 240, 340, 40, "right") + love.graphics.printf(self.clear and self.lines or self:getSectionEndLines(), 240, 370, 40, "right") + + -- draw time left, flash red if necessary + local time_left = self.section_time_limit - math.max(self:getSectionTime(), 0) + if not self.game_over and not self.clear and time_left < sp(0,10) and time_left % 4 < 2 then + love.graphics.setColor(1, 0.3, 0.3, 1) + end + love.graphics.printf(formatTime(time_left), 240, 270, 160, "left") + love.graphics.setColor(1, 1, 1, 1) +end + +function MarathonL1Game:getSectionEndLines() + return math.floor(self.lines / 10 + 1) * 10 +end + +function MarathonL1Game:getBackground() + return math.floor(self.lines / 10) +end + +return MarathonL1Game diff --git a/tetris/modes/pacer_test.lua b/tetris/modes/pacer_test.lua new file mode 100644 index 0000000..60be6b7 --- /dev/null +++ b/tetris/modes/pacer_test.lua @@ -0,0 +1,170 @@ +require 'funcs' + +local GameMode = require 'tetris.modes.gamemode' +local Piece = require 'tetris.components.piece' + +local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls' + +local PacerTest = GameMode:extend() + +PacerTest.name = "TetrisGram™ Pacer Test" +PacerTest.hash = "PacerTest" +PacerTest.tagline = "" + +PacerTest.arr = 1 +PacerTest.drop_speed = 1 + +local function getLevelFrames(level) + if level == 1 then return 72 * 60 / 8.0 + else return 72 * 60 / (8 + level * 0.5) + end +end + +local level_end_sections = { + 7, 15, 23, 32, 41, 51, 61, 72, 83, 94, + 106, 118, 131, 144, 157, 171, 185, 200, + 215, 231, 247 +} + +function PacerTest:new() + PacerTest.super:new() + + self.ready_frames = 2430 + self.clear_frames = 0 + self.randomizer = History6RollsRandomizer() + + self.level = 1 + self.section = 0 + self.level_frames = 0 + + self.section_lines = 0 + self.section_clear = false + self.strikes = 0 + + self.lock_drop = true + self.lock_hard_drop = true + self.enable_hold = true + self.instant_hard_drop = true + self.instant_soft_drop = false + self.next_queue_length = 3 +end + +function PacerTest:initialize(ruleset) + for i = 1, 30 do + table.insert(self.next_queue, self:getNextPiece(ruleset)) + end + self.level_frames = getLevelFrames(1) + switchBGM("pacer_test") +end + +function PacerTest:getARE() + return 0 +end + +function PacerTest:getLineARE() + return 0 +end + +function PacerTest:getDasLimit() + return 8 +end + +function PacerTest:getLineClearDelay() + return 6 +end + +function PacerTest:getLockDelay() + return 30 +end + +function PacerTest:getGravity() + return 1/64 +end + +function PacerTest:getSection() + return math.floor(level / 100) + 1 +end + +function PacerTest:advanceOneFrame() + if self.clear then + self.clear_frames = self.clear_frames + 1 + if self.clear_frames > 600 then + self.completed = true + end + return false + elseif self.ready_frames == 0 then + self.frames = self.frames + 1 + self.level_frames = self.level_frames - 1 + if self.level_frames <= 0 then + self:checkSectionStatus() + self.section = self.section + 1 + if self.section >= level_end_sections[self.level] then + self.level = self.level + 1 + end + self.level_frames = self.level_frames + getLevelFrames(self.level) + end + end + return true +end + +function PacerTest:checkSectionStatus() + if self.section_clear then + self.strikes = 0 + self.section_clear = false + else + self.strikes = self.strikes + 1 + if self.strikes >= 200 then + self.game_over = true + fadeoutBGM(2.5) + end + end + self.section_lines = 0 +end + +function PacerTest:onLineClear(cleared_row_count) + self.section_lines = self.section_lines + cleared_row_count + if self.section_lines >= 3 then + self.section_clear = true + end +end + +function PacerTest:drawGrid(ruleset) + self.grid:draw() + if self.piece ~= nil then + self:drawGhostPiece(ruleset) + end +end + +function PacerTest:getHighscoreData() + return { + level = self.level, + frames = self.frames, + } +end + +function PacerTest:drawScoringInfo() + PacerTest.super.drawScoringInfo(self) + love.graphics.setColor(1, 1, 1, 1) + + local text_x = config["side_next"] and 320 or 240 + + love.graphics.setFont(font_3x5_2) + love.graphics.printf("NEXT", 64, 40, 40, "left") + love.graphics.printf("LINES", text_x, 224, 70, "left") + love.graphics.printf("LEVEL", text_x, 320, 40, "left") + + for i = 1, math.min(self.strikes, 3) do + love.graphics.draw(misc_graphics["strike"], text_x + (i - 1) * 30, 280) + end + + love.graphics.setFont(font_3x5_3) + love.graphics.printf(self.section_lines .. "/3", text_x, 244, 40, "left") + love.graphics.printf(self.level, text_x, 340, 40, "right") + love.graphics.printf(self.section, text_x, 370, 40, "right") +end + +function PacerTest:getBackground() + return self.level - 1 +end + +return PacerTest diff --git a/tetris/modes/phantom_mania.lua b/tetris/modes/phantom_mania.lua new file mode 100644 index 0000000..7d067eb --- /dev/null +++ b/tetris/modes/phantom_mania.lua @@ -0,0 +1,199 @@ +require 'funcs' + +local GameMode = require 'tetris.modes.gamemode' +local Piece = require 'tetris.components.piece' + +local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls' + +local PhantomManiaGame = GameMode:extend() + +PhantomManiaGame.name = "Phantom Mania" +PhantomManiaGame.hash = "PhantomMania" +PhantomManiaGame.tagline = "The blocks disappear as soon as they're locked! Can you remember where everything is?" + +PhantomManiaGame.arr = 1 +PhantomManiaGame.drop_speed = 1 + +function PhantomManiaGame:new() + PhantomManiaGame.super:new() + + self.lock_drop = true + self.next_queue_length = 1 + + self.roll_frames = 0 + self.combo = 1 + self.randomizer = History6RollsRandomizer() +end + +function PhantomManiaGame:getARE() + if self.level < 100 then return 18 + elseif self.level < 200 then return 14 + elseif self.level < 400 then return 8 + elseif self.level < 500 then return 7 + else return 6 end +end + +function PhantomManiaGame:getLineARE() + if self.level < 100 then return 18 + elseif self.level < 400 then return 8 + elseif self.level < 500 then return 7 + else return 6 end +end + +function PhantomManiaGame:getDasLimit() + if self.level < 200 then return 11 + elseif self.level < 300 then return 10 + elseif self.level < 400 then return 9 + else return 7 end +end + +function PhantomManiaGame:getLineClearDelay() + return self:getLineARE() +end + +function PhantomManiaGame:getLockDelay() + if self.level < 100 then return 30 + elseif self.level < 200 then return 26 + elseif self.level < 300 then return 22 + elseif self.level < 400 then return 18 + else return 15 end +end + +function PhantomManiaGame:getGravity() + return 20 +end + +function PhantomManiaGame:hitTorikan(old_level, new_level) + if old_level < 300 and new_level >= 300 and self.frames > sp(2,28) then + self.level = 300 + return true + end + if old_level < 500 and new_level >= 500 and self.frames > sp(3,38) then + self.level = 500 + return true + end + if old_level < 800 and new_level >= 800 and self.frames > sp(5,23) then + self.level = 800 + return true + end + return false +end + +function PhantomManiaGame:advanceOneFrame() + if self.clear then + self.roll_frames = self.roll_frames + 1 + if self.roll_frames < 0 then + return false + elseif self.roll_frames > 1982 then + self.completed = true + end + elseif self.ready_frames == 0 then + self.frames = self.frames + 1 + end + return true +end + +function PhantomManiaGame:onPieceEnter() + if (self.level % 100 ~= 99 and self.level ~= 998) and not self.clear and self.frames ~= 0 then + self.level = self.level + 1 + end +end + +function PhantomManiaGame:onLineClear(cleared_row_count) + if not self.clear then + local new_level = self.level + cleared_row_count + if self:hitTorikan(self.level, new_level) then + if new_level >= 999 then + self.level = 999 + end + self.clear = true + else + self.level = new_level + end + end +end + +function PhantomManiaGame:updateScore(level, drop_bonus, cleared_lines) + if cleared_lines > 0 then + self.score = self.score + ( + (math.ceil((level + cleared_lines) / 4) + drop_bonus) * + cleared_lines * (cleared_lines * 2 - 1) * (self.combo * 2 - 1) + ) + self.lines = self.lines + cleared_lines + self.combo = self.combo + cleared_lines - 1 + else + self.drop_bonus = 0 + self.combo = 1 + end +end + +PhantomManiaGame.rollOpacityFunction = function(age) + if age > 4 then return 0 + else return 1 - age / 4 end +end + +function PhantomManiaGame:drawGrid() + if not (self.game_over or self.clear) then + self.grid:drawInvisible(self.rollOpacityFunction) + else + self.grid:draw() + end +end + +local function getLetterGrade(level, clear) + if level < 300 or level == 300 and clear then + return "" + elseif level < 500 or level == 500 and clear then + return "M" + elseif level < 700 then + return "MK" + elseif level < 800 or level == 800 and clear then + return "MV" + elseif level < 900 then + return "MO" + elseif level < 999 then + return "MM" + elseif level == 999 then + return "GM" + end +end + +function PhantomManiaGame:drawScoringInfo() + PhantomManiaGame.super.drawScoringInfo(self) + + local text_x = config["side_next"] and 320 or 240 + + love.graphics.setFont(font_3x5_2) + love.graphics.printf("GRADE", text_x, 120, 40, "left") + love.graphics.printf("SCORE", text_x, 200, 40, "left") + love.graphics.printf("LEVEL", text_x, 320, 40, "left") + + love.graphics.setFont(font_3x5_3) + love.graphics.printf(getLetterGrade(self.level, self.clear), text_x, 140, 90, "left") + love.graphics.printf(self.score, text_x, 220, 90, "left") + love.graphics.printf(self.level, text_x, 340, 40, "right") + if self.clear then + love.graphics.printf(self.level, text_x, 370, 40, "right") + else + love.graphics.printf(self:getSectionEndLevel(), text_x, 370, 40, "right") + end + +end + +function PhantomManiaGame:getSectionEndLevel() + if self.level > 900 then return 999 + else return math.floor(self.level / 100 + 1) * 100 end +end + +function PhantomManiaGame:getBackground() + return math.floor(self.level / 100) +end + +function PhantomManiaGame:getHighscoreData() + return { + level = self.level, + frames = self.frames, + } +end + +return PhantomManiaGame diff --git a/tetris/modes/phantom_mania2.lua b/tetris/modes/phantom_mania2.lua new file mode 100644 index 0000000..c48a2fd --- /dev/null +++ b/tetris/modes/phantom_mania2.lua @@ -0,0 +1,303 @@ +require 'funcs' + +local GameMode = require 'tetris.modes.gamemode' +local Piece = require 'tetris.components.piece' + +local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls' + +local PhantomMania2Game = GameMode:extend() + +PhantomMania2Game.name = "Phantom Mania 2" +PhantomMania2Game.hash = "PhantomMania2" +PhantomMania2Game.tagline = "The blocks disappear even faster now! Can you make it to level 1300?" + +PhantomMania2Game.arr = 1 +PhantomMania2Game.drop_speed = 1 + +function PhantomMania2Game:new() + PhantomMania2Game.super:new() + self.level = 0 + self.grade = 0 + self.garbage = 0 + self.clear = false + self.completed = false + self.roll_frames = 0 + self.combo = 1 + self.hold_age = 0 + self.queue_age = 0 + self.randomizer = History6RollsRandomizer() + + self.lock_drop = true + self.enable_hold = true + self.next_queue_length = 3 +end + +function PhantomMania2Game:getARE() + if self.level < 300 then return 12 + else return 6 end +end + +function PhantomMania2Game:getLineARE() + if self.level < 100 then return 8 + elseif self.level < 200 then return 7 + elseif self.level < 500 then return 6 + elseif self.level < 1300 then return 5 + else return 6 end +end + +function PhantomMania2Game:getDasLimit() + if self.level < 200 then return 9 + elseif self.level < 500 then return 7 + else return 5 end +end + +function PhantomMania2Game:getLineClearDelay() + return self:getLineARE() - 2 +end + +function PhantomMania2Game:getLockDelay() + if self.level < 200 then return 18 + elseif self.level < 300 then return 17 + elseif self.level < 500 then return 15 + elseif self.level < 600 then return 13 + else return 12 end +end + +function PhantomMania2Game:getGravity() + return 20 +end + +function PhantomMania2Game:getGarbageLimit() + if self.level < 600 then return 20 + elseif self.level < 700 then return 18 + elseif self.level < 800 then return 10 + elseif self.level < 900 then return 9 + else return 8 end +end + +function PhantomMania2Game:getNextPiece(ruleset) + return { + skin = self.level >= 1000 and "bone" or "2tie", + shape = self.randomizer:nextPiece(), + orientation = ruleset:getDefaultOrientation(), + } +end + +function PhantomMania2Game:hitTorikan(old_level, new_level) + if old_level < 300 and new_level >= 300 and self.frames > sp(2,02) then + self.level = 300 + return true + end + if old_level < 500 and new_level >= 500 and self.frames > sp(3,03) then + self.level = 500 + return true + end + if old_level < 800 and new_level >= 800 and self.frames > sp(4,40) then + self.level = 800 + return true + end + if old_level < 1000 and new_level >= 1000 and self.frames > sp(5,38) then + self.level = 1000 + return true + end + return false +end + +function PhantomMania2Game:advanceOneFrame() + if self.clear then + self.roll_frames = self.roll_frames + 1 + if self.roll_frames < 0 then + if self.roll_frames + 1 == 0 then + switchBGM("credit_roll", "gm3") + return true + end + return false + elseif self.roll_frames > 3238 then + switchBGM(nil) + self.completed = true + end + elseif self.ready_frames == 0 then + self.frames = self.frames + 1 + self.hold_age = self.hold_age + 1 + end + return true +end + +function PhantomMania2Game:whilePieceActive() + self.queue_age = self.queue_age + 1 +end + +function PhantomMania2Game:onPieceEnter() + self.queue_age = 0 + if (self.level % 100 ~= 99) and not self.clear and self.frames ~= 0 then + self.level = self.level + 1 + end +end + +local cleared_row_levels = {1, 2, 4, 6} +local cleared_row_points = {0.02, 0.05, 0.15, 0.6} + +function PhantomMania2Game:onLineClear(cleared_row_count) + if not self.clear then + local new_level = self.level + cleared_row_levels[cleared_row_count] + self:updateSectionTimes(self.level, new_level) + if new_level >= 1300 or self:hitTorikan(self.level, new_level) then + if new_level >= 1300 then + self.level = 1300 + end + self.clear = true + self.grid:clear() + self.roll_frames = -150 + else + self.level = math.min(new_level, 1300) + end + self:advanceBottomRow(-cleared_row_count) + end +end + +function PhantomMania2Game:onPieceLock() + self:advanceBottomRow(1) +end + +function PhantomMania2Game:onHold() + self.hold_age = 0 +end + +function PhantomMania2Game:updateScore(level, drop_bonus, cleared_lines) + if cleared_lines > 0 then + self.score = self.score + ( + (math.ceil((level + cleared_lines) / 4) + drop_bonus) * + cleared_lines * (cleared_lines * 2 - 1) * (self.combo * 2 - 1) + ) + self.lines = self.lines + cleared_lines + self.combo = self.combo + cleared_lines - 1 + else + self.drop_bonus = 0 + self.combo = 1 + end +end + + +local cool_cutoffs = { + sp(0,36), sp(0,36), sp(0,36), sp(0,36), sp(0,36), + sp(0,30), sp(0,30), sp(0,30), sp(0,30), sp(0,30), + sp(0,27), sp(0,27), sp(0,27), +} + +local regret_cutoffs = { + sp(0,50), sp(0,50), sp(0,50), sp(0,50), sp(0,50), + sp(0,40), sp(0,40), sp(0,40), sp(0,40), sp(0,40), + sp(0,35), sp(0,35), sp(0,35), +} + +function PhantomMania2Game:updateSectionTimes(old_level, new_level) + if math.floor(old_level / 100) < math.floor(new_level / 100) then + local section = math.floor(old_level / 100) + 1 + section_time = self.frames - self.section_start_time + table.insert(self.section_times, section_time) + self.section_start_time = self.frames + if section_time <= cool_cutoffs[section] then + self.grade = self.grade + 2 + elseif section_time <= regret_cutoffs[section] then + self.grade = self.grade + 1 + end + end +end + +function PhantomMania2Game:advanceBottomRow(dx) + if self.level >= 500 and self.level < 1000 then + self.garbage = math.max(self.garbage + dx, 0) + if self.garbage >= self:getGarbageLimit() then + self.grid:copyBottomRow() + self.garbage = 0 + end + end +end + +PhantomMania2Game.rollOpacityFunction = function(age) + if age > 4 then return 0 + else return 1 - age / 4 end +end + +PhantomMania2Game.garbageOpacityFunction = function(age) + if age > 30 then return 0 + else return 1 - age / 30 end +end + +function PhantomMania2Game:drawGrid() + if not (self.game_over or self.clear) then + self.grid:drawInvisible(self.rollOpacityFunction, self.garbageOpacityFunction) + else + self.grid:draw() + end +end + +local function getLetterGrade(grade) + if grade == 0 then + return "1" + elseif grade <= 9 then + return "S" .. tostring(grade) + else + return "M" .. tostring(grade - 9) + end +end + +function PhantomMania2Game:setNextOpacity(i) + if self.level > 1000 then + local hidden_next_pieces = math.floor(self.level / 100) - 10 + if i < hidden_next_pieces then + love.graphics.setColor(1, 1, 1, 0) + elseif i == hidden_next_pieces then + love.graphics.setColor(1, 1, 1, 1 - math.min(1, self.queue_age / 4)) + else + love.graphics.setColor(1, 1, 1, 1) + end + else + love.graphics.setColor(1, 1, 1, 1) + end +end + +function PhantomMania2Game:setHoldOpacity() + if self.level > 1000 then + love.graphics.setColor(1, 1, 1, 1 - math.min(1, self.hold_age / 15)) + else + love.graphics.setColor(1, 1, 1, 1) + end +end + +function PhantomMania2Game:drawScoringInfo() + PhantomMania2Game.super.drawScoringInfo(self) + + love.graphics.setColor(1, 1, 1, 1) + + local text_x = config["side_next"] and 320 or 240 + + love.graphics.setFont(font_3x5_2) + love.graphics.printf("GRADE", text_x, 120, 40, "left") + love.graphics.printf("SCORE", text_x, 200, 40, "left") + love.graphics.printf("LEVEL", text_x, 320, 40, "left") + + love.graphics.setFont(font_3x5_3) + love.graphics.printf(getLetterGrade(math.floor(self.grade)), text_x, 140, 90, "left") + love.graphics.printf(self.score, text_x, 220, 90, "left") + love.graphics.printf(self.level, text_x, 340, 50, "right") + if self.clear then + love.graphics.printf(self.level, text_x, 370, 50, "right") + else + love.graphics.printf(math.floor(self.level / 100 + 1) * 100, text_x, 370, 50, "right") + end +end + +function PhantomMania2Game:getBackground() + return math.floor(self.level / 100) +end + +function PhantomMania2Game:getHighscoreData() + return { + level = self.level, + frames = self.frames, + grade = self.grade, + } +end + +return PhantomMania2Game diff --git a/tetris/modes/phantom_mania_n.lua b/tetris/modes/phantom_mania_n.lua new file mode 100644 index 0000000..6dd9cc7 --- /dev/null +++ b/tetris/modes/phantom_mania_n.lua @@ -0,0 +1,200 @@ +require 'funcs' + +local GameMode = require 'tetris.modes.gamemode' +local Piece = require 'tetris.components.piece' + +local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls' + +local PhantomManiaGame = GameMode:extend() + +PhantomManiaGame.name = "Phantom Mania N" +PhantomManiaGame.hash = "PhantomManiaN" +PhantomManiaGame.tagline = "The old mode from Nullpomino." + +PhantomManiaGame.arr = 1 +PhantomManiaGame.drop_speed = 1 + +function PhantomManiaGame:new() + PhantomManiaGame.super:new() + + self.lock_drop = true + self.next_queue_length = 3 + self.enable_hold = true + + self.roll_frames = 0 + self.combo = 1 + self.randomizer = History6RollsRandomizer() +end + +function PhantomManiaGame:getARE() + if self.level < 100 then return 18 + elseif self.level < 200 then return 14 + elseif self.level < 400 then return 8 + elseif self.level < 500 then return 7 + else return 6 end +end + +function PhantomManiaGame:getLineARE() + if self.level < 100 then return 18 + elseif self.level < 400 then return 8 + elseif self.level < 500 then return 7 + else return 6 end +end + +function PhantomManiaGame:getDasLimit() + if self.level < 200 then return 11 + elseif self.level < 300 then return 10 + elseif self.level < 400 then return 9 + else return 7 end +end + +function PhantomManiaGame:getLineClearDelay() + return self:getLineARE() +end + +function PhantomManiaGame:getLockDelay() + if self.level < 100 then return 30 + elseif self.level < 200 then return 26 + elseif self.level < 300 then return 22 + elseif self.level < 400 then return 18 + else return 15 end +end + +function PhantomManiaGame:getGravity() + return 20 +end + +function PhantomManiaGame:hitTorikan(old_level, new_level) + if old_level < 300 and new_level >= 300 and self.frames > sp(2,28) then + self.level = 300 + return true + end + if old_level < 500 and new_level >= 500 and self.frames > sp(3,38) then + self.level = 500 + return true + end + if old_level < 800 and new_level >= 800 and self.frames > sp(5,23) then + self.level = 800 + return true + end + return false +end + +function PhantomManiaGame:advanceOneFrame() + if self.clear then + self.roll_frames = self.roll_frames + 1 + if self.roll_frames < 0 then + return false + elseif self.roll_frames > 1982 then + self.completed = true + end + elseif self.ready_frames == 0 then + self.frames = self.frames + 1 + end + return true +end + +function PhantomManiaGame:onPieceEnter() + if (self.level % 100 ~= 99 and self.level ~= 998) and not self.clear and self.frames ~= 0 then + self.level = self.level + 1 + end +end + +function PhantomManiaGame:onLineClear(cleared_row_count) + if not self.clear then + local new_level = self.level + cleared_row_count + if self:hitTorikan(self.level, new_level) then + if new_level >= 999 then + self.level = 999 + end + self.clear = true + else + self.level = new_level + end + end +end + +function PhantomManiaGame:updateScore(level, drop_bonus, cleared_lines) + if cleared_lines > 0 then + self.score = self.score + ( + (math.ceil((level + cleared_lines) / 4) + drop_bonus) * + cleared_lines * (cleared_lines * 2 - 1) * (self.combo * 2 - 1) + ) + self.lines = self.lines + cleared_lines + self.combo = self.combo + cleared_lines - 1 + else + self.drop_bonus = 0 + self.combo = 1 + end +end + +PhantomManiaGame.rollOpacityFunction = function(age) + if age > 4 then return 0 + else return 1 - age / 4 end +end + +function PhantomManiaGame:drawGrid() + if not (self.game_over or self.clear) then + self.grid:drawInvisible(self.rollOpacityFunction) + else + self.grid:draw() + end +end + +local function getLetterGrade(level, clear) + if level < 300 or level == 300 and clear then + return "" + elseif level < 500 or level == 500 and clear then + return "M" + elseif level < 700 then + return "MK" + elseif level < 800 or level == 800 and clear then + return "MV" + elseif level < 900 then + return "MO" + elseif level < 999 then + return "MM" + elseif level == 999 then + return "GM" + end +end + +function PhantomManiaGame:drawScoringInfo() + PhantomManiaGame.super.drawScoringInfo(self) + + local text_x = config["side_next"] and 320 or 240 + + love.graphics.setFont(font_3x5_2) + love.graphics.printf("GRADE", text_x, 120, 40, "left") + love.graphics.printf("SCORE", text_x, 200, 40, "left") + love.graphics.printf("LEVEL", text_x, 320, 40, "left") + + love.graphics.setFont(font_3x5_3) + love.graphics.printf(getLetterGrade(self.level, self.clear), text_x, 140, 90, "left") + love.graphics.printf(self.score, text_x, 220, 90, "left") + love.graphics.printf(self.level, text_x, 340, 40, "right") + if self.clear then + love.graphics.printf(self.level, text_x, 370, 40, "right") + else + love.graphics.printf(self:getSectionEndLevel(), text_x, 370, 40, "right") + end + +end + +function PhantomManiaGame:getSectionEndLevel() + if self.level > 900 then return 999 + else return math.floor(self.level / 100 + 1) * 100 end +end + +function PhantomManiaGame:getBackground() + return math.floor(self.level / 100) +end + +function PhantomManiaGame:getHighscoreData() + return { + level = self.level, + frames = self.frames, + } +end + +return PhantomManiaGame diff --git a/tetris/modes/strategy.lua b/tetris/modes/strategy.lua new file mode 100644 index 0000000..1d87cca --- /dev/null +++ b/tetris/modes/strategy.lua @@ -0,0 +1,157 @@ +require 'funcs' + +local GameMode = require 'tetris.modes.gamemode' +local Piece = require 'tetris.components.piece' + +local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls' + +local StrategyGame = GameMode:extend() + +StrategyGame.name = "Strategy" +StrategyGame.hash = "Strategy" +StrategyGame.tagline = "You have lots of time to think! Can you use it to place a piece fast?" + +StrategyGame.arr = 1 +StrategyGame.drop_speed = 1 + +function StrategyGame:new() + StrategyGame.super:new() + self.level = 0 + self.clear = false + self.completed = false + self.roll_frames = 0 + self.combo = 1 + self.randomizer = History6RollsRandomizer() + + self.enable_hold = false + self.next_queue_length = 1 +end + +function StrategyGame:getARE() + if self.level < 100 then return 60 + elseif self.level < 200 then return 54 + elseif self.level < 300 then return 48 + elseif self.level < 400 then return 42 + elseif self.level < 500 then return 36 + elseif self.level < 600 then return 30 + elseif self.level < 700 then return 24 + elseif self.level < 800 then return 21 + elseif self.level < 900 then return 18 + else return 15 end +end + +function StrategyGame:getLineARE() + return self:getARE() +end + +function StrategyGame:getDasLimit() + return 6 +end + +function StrategyGame:getLineClearDelay() + return self:getARE() +end + +function StrategyGame:getLockDelay() + if self.level < 500 then return 8 + elseif self.level < 700 then return 6 + else return 4 end +end + +function StrategyGame:getGravity() + return 20 +end + +function StrategyGame:getNextPiece(ruleset) + return { + skin = "2tie", + shape = self.randomizer:nextPiece(), + orientation = ruleset:getDefaultOrientation(), + } +end + +function StrategyGame:advanceOneFrame() + if self.clear then + self.roll_frames = self.roll_frames + 1 + if self.roll_frames > 1936 then + switchBGM(nil) + self.completed = true + end + elseif self.ready_frames == 0 then + self.frames = self.frames + 1 + end + return true +end + +function StrategyGame:onPieceEnter() + if (self.level % 100 ~= 99) and not self.clear and self.frames ~= 0 then + self.level = self.level + 1 + end +end + +function StrategyGame:onLineClear(cleared_row_count) + if not self.clear then + self.level = math.min(999, self.level + cleared_row_count) + if self.level == 999 then + self.clear = true + end + end +end + +function StrategyGame: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 + +function StrategyGame:setNextOpacity(i) + if self.are == 0 then love.graphics.setColor(1, 1, 1, 1) + else love.graphics.setColor(1, 1, 1, self.are / self:getARE()) + end +end + +function StrategyGame:drawGrid() + self.grid:draw() +end + +function StrategyGame:drawScoringInfo() + StrategyGame.super.drawScoringInfo(self) + + love.graphics.setColor(1, 1, 1, 1) + + local text_x = config["side_next"] and 320 or 240 + + love.graphics.setFont(font_3x5_2) + love.graphics.printf("SCORE", text_x, 200, 40, "left") + love.graphics.printf("LEVEL", text_x, 320, 40, "left") + + love.graphics.setFont(font_3x5_3) + love.graphics.printf(self.score, text_x, 220, 90, "left") + love.graphics.printf(self.level, text_x, 340, 50, "right") + if self.clear then + love.graphics.printf(self.level, text_x, 370, 50, "right") + else + love.graphics.printf(math.floor(self.level / 100 + 1) * 100, text_x, 370, 50, "right") + end +end + +function StrategyGame:getBackground() + return math.floor(self.level / 100) +end + +function StrategyGame:getHighscoreData() + return { + level = self.level, + frames = self.frames, + } +end + +return StrategyGame diff --git a/tetris/modes/survival_2020.lua b/tetris/modes/survival_2020.lua new file mode 100644 index 0000000..2573f30 --- /dev/null +++ b/tetris/modes/survival_2020.lua @@ -0,0 +1,265 @@ +require 'funcs' + +local GameMode = require 'tetris.modes.gamemode' +local Piece = require 'tetris.components.piece' + +local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls_35bag' + +local Survival2020Game = GameMode:extend() + +Survival2020Game.name = "Survival 2020" +Survival2020Game.hash = "Survival2020" +Survival2020Game.tagline = "A new time limit on the blocks! Can you handle being forced to perform under the total delay?" + +Survival2020Game.arr = 1 +Survival2020Game.drop_speed = 1 + +function Survival2020Game:new() + Survival2020Game.super:new() + self.level = 0 + self.grade = 0 + self.garbage = 0 + self.clear = false + self.completed = false + self.roll_frames = 0 + self.combo = 1 + self.total_delay = 0 + self.randomizer = History6RollsRandomizer() + + self.lock_drop = true + self.enable_hold = true + self.next_queue_length = 3 +end + +function Survival2020Game:getARE() + if self.level < 200 then return 12 + elseif self.level < 300 then return 10 + elseif self.level < 500 then return 6 + elseif self.level < 1000 then return 4 + elseif self.level < 1500 then return 3 + else return 2 end +end + +function Survival2020Game:getLineARE() + return self:getARE() +end + +function Survival2020Game:getDasLimit() + if self.level < 200 then return 9 + elseif self.level < 500 then return 7 + elseif self.level < 1000 then return 5 + elseif self.level < 1500 then return 4 + else return 3 end +end + +function Survival2020Game:getLineClearDelay() + if self.level < 300 then return 6 + elseif self.level < 500 then return 4 + else return 2 end +end + +function Survival2020Game:getLockDelay() + if self.level < 100 then return 20 + elseif self.level < 200 then return 18 + elseif self.level < 300 then return 17 + elseif self.level < 400 then return 15 + elseif self.level < 500 then return 14 + elseif self.level < 1000 then return 13 + elseif self.level < 1500 then return 10 + else return 8 end +end + +function Survival2020Game:getTotalDelay() + if self.level < 500 then return 60 + elseif self.level < 600 then return 45 -- lock delay: 15 + elseif self.level < 700 then return 36 + elseif self.level < 800 then return 27 + elseif self.level < 900 then return 21 + elseif self.level < 1000 then return 15 + elseif self.level < 1100 then return 36 -- lock delay: 11 + elseif self.level < 1200 then return 27 + elseif self.level < 1300 then return 21 + elseif self.level < 1400 then return 15 + elseif self.level < 1500 then return 12 + elseif self.level < 1600 then return 30 -- lock delay: 8 + elseif self.level < 1700 then return 21 + elseif self.level < 1800 then return 15 + elseif self.level < 1900 then return 12 + elseif self.level < 2020 then return 10 + else return 30 end +end + +function Survival2020Game:getGravity() + return 20 +end + +function Survival2020Game:getNextPiece(ruleset) + return { + skin = self.level >= 1000 and "bone" or "2tie", + shape = self.randomizer:nextPiece(), + orientation = ruleset:getDefaultOrientation(), + } +end + +function Survival2020Game:hitTorikan(old_level, new_level) + if old_level < 500 and new_level >= 500 and self.frames > sp(3,00) then + self.level = 500 + return true + end + if old_level < 1000 and new_level >= 1000 and self.frames > sp(5,00) then + self.level = 1000 + return true + end + if old_level < 1500 and new_level >= 1500 and self.frames > sp(7,00) then + self.level = 1500 + return true + end + return false +end + +function Survival2020Game:advanceOneFrame() + if self.clear then + self.roll_frames = self.roll_frames + 1 + if self.roll_frames < 0 then + if self.roll_frames + 1 == 0 then + switchBGM("credit_roll", "gm3") + return true + end + return false + elseif self.roll_frames > 3238 then + switchBGM(nil) + self.completed = true + end + elseif self.ready_frames == 0 then + self.frames = self.frames + 1 + if self.piece ~= nil then + self.total_delay = self.total_delay + 1 + if self.total_delay >= self:getTotalDelay() then + self.piece:dropToBottom(self.grid) + self.piece.locked = true + end + end + end + return true +end + +function Survival2020Game:onPieceEnter() + if not self.clear and ( + (self.level < 1900 and self.level % 100 ~= 99) or + self.level == 2019 + ) then + self.level = self.level + 1 + end + self.total_delay = 0 +end + +local cleared_row_levels = {1, 2, 4, 6} +local cleared_row_points = {0.02, 0.05, 0.15, 0.6} + +function Survival2020Game:onLineClear(cleared_row_count) + if not self.clear then + local new_level = self.level + cleared_row_levels[cleared_row_count] + self:updateSectionTimes(self.level, new_level) + if new_level >= 2020 or self:hitTorikan(self.level, new_level) then + if new_level >= 2020 then + self.level = 2020 + end + self.clear = true + self.grid:clear() + self.roll_frames = -150 + else + self.level = math.min(new_level, 2020) + end + end +end + +function Survival2020Game: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 + +function Survival2020Game:updateSectionTimes(old_level, new_level) + if math.floor(old_level / 100) < math.floor(new_level / 100) then + local section = math.floor(old_level / 100) + 1 + section_time = self.frames - self.section_start_time + table.insert(self.section_times, section_time) + self.section_start_time = self.frames + if section_time <= sp(0,30) then + self.grade = self.grade + 2 + else + self.grade = self.grade + 1 + end + end +end + +Survival2020Game.opacityFunction = function(age) + if age > 300 then return 0 + else return 1 - Math.max(age - 240, 0) / 60 end +end + +function Survival2020Game:drawGrid() + if self.level < 1500 then + self.grid:draw() + else + self.grid:drawInvisible(self.opacityFunction) + end +end + +local function getLetterGrade(grade) + if grade == 0 then + return "1" + elseif grade <= 9 then + return "S" .. tostring(grade) + else + return "M" .. tostring(grade - 9) + end +end + +function Survival2020Game:drawScoringInfo() + Survival2020Game.super.drawScoringInfo(self) + + love.graphics.setColor(1, 1, 1, 1) + + local text_x = config["side_next"] and 320 or 240 + + love.graphics.setFont(font_3x5_2) + love.graphics.printf("GRADE", text_x, 120, 40, "left") + love.graphics.printf("SCORE", text_x, 200, 40, "left") + love.graphics.printf("LEVEL", text_x, 320, 40, "left") + + local current_section = math.floor(self.level / 100) + 1 + self:drawSectionTimesWithSplits(current_section) + + love.graphics.setFont(font_3x5_3) + love.graphics.printf(getLetterGrade(math.floor(self.grade)), text_x, 140, 90, "left") + love.graphics.printf(self.score, text_x, 220, 90, "left") + love.graphics.printf(self.level, text_x, 340, 50, "right") + if self.clear then + love.graphics.printf(self.level, text_x, 370, 50, "right") + else + love.graphics.printf(math.floor(self.level / 100 + 1) * 100, text_x, 370, 50, "right") + end +end + +function Survival2020Game:getBackground() + return math.floor(self.level / 100) +end + +function Survival2020Game:getHighscoreData() + return { + level = self.level, + frames = self.frames, + grade = self.grade, + } +end + +return Survival2020Game diff --git a/tetris/modes/survival_a1.lua b/tetris/modes/survival_a1.lua new file mode 100644 index 0000000..93fde67 --- /dev/null +++ b/tetris/modes/survival_a1.lua @@ -0,0 +1,198 @@ +require 'funcs' + +local GameMode = require 'tetris.modes.gamemode' +local Piece = require 'tetris.components.piece' + +local History4RollsRandomizer = require 'tetris.randomizers.history_4rolls' + +local SurvivalA1Game = GameMode:extend() + +SurvivalA1Game.name = "Survival A1" +SurvivalA1Game.hash = "SurvivalA1" +SurvivalA1Game.tagline = "The game starts fast and only gets faster!" + +SurvivalA1Game.arr = 1 +SurvivalA1Game.drop_speed = 1 + +function SurvivalA1Game:new() + SurvivalA1Game.super:new() + + self.roll_frames = 0 + self.combo = 1 + self.gm_conditions = { + level300 = false, + level500 = false, + level999 = false + } + + self.randomizer = History4RollsRandomizer() + + self.lock_drop = false + self.enable_hard_drop = false + self.enable_hold = false + self.next_queue_length = 1 +end + +function SurvivalA1Game:getARE() + return 25 +end + +function SurvivalA1Game:getLineARE() + return 25 +end + +function SurvivalA1Game:getDasLimit() + return 15 +end + +function SurvivalA1Game:getLineClearDelay() + return 40 +end + +function SurvivalA1Game:getLockDelay() + return 30 +end + +function SurvivalA1Game:getGravity() + return 20 +end + +local function getRankForScore(score) + if score < 400 then return {rank = "9", next = 400} + elseif score < 800 then return {rank = "8", next = 800} + elseif score < 1400 then return {rank = "7", next = 1400} + elseif score < 2000 then return {rank = "6", next = 2000} + elseif score < 3500 then return {rank = "5", next = 3500} + elseif score < 5500 then return {rank = "4", next = 5500} + elseif score < 8000 then return {rank = "3", next = 8000} + elseif score < 12000 then return {rank = "2", next = 12000} + elseif score < 16000 then return {rank = "1", next = 16000} + elseif score < 22000 then return {rank = "S1", next = 22000} + elseif score < 30000 then return {rank = "S2", next = 30000} + elseif score < 40000 then return {rank = "S3", next = 40000} + elseif score < 52000 then return {rank = "S4", next = 52000} + elseif score < 66000 then return {rank = "S5", next = 66000} + elseif score < 82000 then return {rank = "S6", next = 82000} + elseif score < 100000 then return {rank = "S7", next = 100000} + elseif score < 120000 then return {rank = "S8", next = 120000} + else return {rank = "S9", next = "???"} + end +end + +function SurvivalA1Game:advanceOneFrame() + if self.clear then + self.roll_frames = self.roll_frames + 1 + if self.roll_frames > 2968 then + self.completed = true + end + elseif self.ready_frames == 0 then + self.frames = self.frames + 1 + end + return true +end + +function SurvivalA1Game: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 SurvivalA1Game:onLineClear(cleared_row_count) + self:checkGMRequirements(self.level, self.level + cleared_row_count) + if not self.clear then + local new_level = math.min(self.level + cleared_row_count, 999) + if self.level == 999 then + self.clear = true + else + self.level = new_level + end + end +end + +function SurvivalA1Game:updateScore(level, drop_bonus, cleared_lines) + if cleared_lines > 0 then + self.score = self.score + ( + (math.ceil((level + cleared_lines) / 4) + drop_bonus) * + cleared_lines * (cleared_lines * 2 - 1) * self.combo + ) + self.lines = self.lines + cleared_lines + self.combo = self.combo + (cleared_lines - 1) * 2 + else + self.drop_bonus = 0 + self.combo = 1 + end +end + +function 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 + self.gm_conditions["level300"] = true + end + elseif old_level < 500 and new_level >= 500 then + if self.score > 40000 and self.frames <= sp(7,30) then + self.gm_conditions["level500"] = true + end + elseif old_level < 999 and new_level >= 999 then + if self.score > 126000 and self.frames <= sp(13,30) then + self.gm_conditions["level900"] = true + end + end +end + +function SurvivalA1Game:drawGrid() + self.grid:draw() + if self.piece ~= nil and self.level < 100 then + self:drawGhostPiece(ruleset) + end +end + +function SurvivalA1Game:drawScoringInfo() + SurvivalA1Game.super.drawScoringInfo(self) + love.graphics.setColor(1, 1, 1, 1) + + love.graphics.setFont(font_3x5_2) + love.graphics.print( + self.das.direction .. " " .. + self.das.frames .. " " .. + st(self.prev_inputs) + ) + love.graphics.printf("NEXT", 64, 40, 40, "left") + love.graphics.printf("GRADE", 240, 120, 40, "left") + love.graphics.printf("SCORE", 240, 200, 40, "left") + love.graphics.printf("NEXT RANK", 240, 260, 90, "left") + love.graphics.printf("LEVEL", 240, 320, 40, "left") + + love.graphics.setFont(font_3x5_3) + love.graphics.printf(self.score, 240, 220, 90, "left") + if self.gm_conditions["level300"] and self.gm_conditions["level500"] and self.gm_conditions["level900"] then + love.graphics.printf("GM", 240, 140, 90, "left") + else + love.graphics.printf(getRankForScore(self.score).rank, 240, 140, 90, "left") + end + love.graphics.printf(getRankForScore(self.score).next, 240, 280, 90, "left") + love.graphics.printf(self.level, 240, 340, 40, "right") + love.graphics.printf(self:getSectionEndLevel(), 240, 370, 40, "right") + + love.graphics.setFont(font_8x11) + love.graphics.printf(formatTime(self.frames), 64, 420, 160, "center") +end + +function SurvivalA1Game:getSectionEndLevel() + if self.level > 900 then return 999 + else return math.floor(self.level / 100 + 1) * 100 end +end + +function SurvivalA1Game:getBackground() + return math.floor(self.level / 100) +end + +function SurvivalA1Game:getHighscoreData() + return { + grade = self.grade, + score = self.score, + level = self.level, + frames = self.frames, + } +end + +return SurvivalA1Game diff --git a/tetris/modes/survival_a2.lua b/tetris/modes/survival_a2.lua new file mode 100644 index 0000000..172af29 --- /dev/null +++ b/tetris/modes/survival_a2.lua @@ -0,0 +1,166 @@ +require 'funcs' + +local GameMode = require 'tetris.modes.gamemode' +local Piece = require 'tetris.components.piece' + +local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls' + +local SurvivalA2Game = GameMode:extend() + +SurvivalA2Game.name = "Survival A2" +SurvivalA2Game.hash = "SurvivalA2" +SurvivalA2Game.tagline = "The game starts fast and only gets faster!" + +SurvivalA2Game.arr = 1 +SurvivalA2Game.drop_speed = 1 + +function SurvivalA2Game:new() + SurvivalA2Game.super:new() + self.roll_frames = 0 + self.combo = 1 + self.randomizer = History6RollsRandomizer() + + self.lock_drop = true +end + +function SurvivalA2Game:getARE() + if self.level < 100 then return 18 + elseif self.level < 300 then return 14 + elseif self.level < 400 then return 8 + elseif self.level < 500 then return 7 + else return 6 end +end + +function SurvivalA2Game:getLineARE() + if self.level < 100 then return 14 + elseif self.level < 400 then return 8 + elseif self.level < 500 then return 7 + else return 6 end +end + +function SurvivalA2Game: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 SurvivalA2Game:getLineClearDelay() + return self:getLineARE() +end + +function SurvivalA2Game: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 SurvivalA2Game:getGravity() + return 20 +end + +function SurvivalA2Game:hitTorikan(old_level, new_level) + if old_level < 500 and new_level >= 500 and self.frames > sp(3,25) then + self.level = 500 + return true + end + return false +end + +function SurvivalA2Game:advanceOneFrame() + if self.clear then + self.roll_frames = self.roll_frames + 1 + if self.roll_frames > 2968 then + self.completed = true + end + elseif self.ready_frames == 0 then + self.frames = self.frames + 1 + end + return true +end + +function SurvivalA2Game:onPieceEnter() + if (self.level % 100 ~= 99 or self.level == 998) and not self.clear and self.frames ~= 0 then + self.level = self.level + 1 + end +end + +function SurvivalA2Game:onLineClear(cleared_row_count) + if not self.clear then + local new_level = math.min(self.level + cleared_row_count, 999) + if self.level == 999 or self:hitTorikan(self.level, new_level) then + self.clear = true + else + self.level = new_level + end + end +end + +function SurvivalA2Game:updateScore(level, drop_bonus, cleared_lines) + if cleared_lines > 0 then + self.score = self.score + ( + (math.ceil((level + cleared_lines) / 4) + drop_bonus) * + cleared_lines * (cleared_lines * 2 - 1) * self.combo + ) + self.lines = self.lines + cleared_lines + self.combo = self.combo + (cleared_lines - 1) * 2 + else + self.drop_bonus = 0 + self.combo = 1 + end +end + +function SurvivalA2Game:getLetterGrade() + if self.level >= 999 then return "GM" + elseif self.level >= 500 then return "M" + else return "" end +end + +function SurvivalA2Game:drawGrid() + self.grid:draw() +end + +function SurvivalA2Game:drawScoringInfo() + SurvivalA2Game.super.drawScoringInfo(self) + love.graphics.setColor(1, 1, 1, 1) + + local text_x = config["side_next"] and 320 or 240 + + love.graphics.setFont(font_3x5_2) + love.graphics.print( + self.das.direction .. " " .. + self.das.frames .. " " .. + st(self.prev_inputs) + ) + love.graphics.printf("NEXT", 64, 40, 40, "left") + 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(self.score, text_x, 220, 90, "left") + love.graphics.printf(self:getLetterGrade(), text_x, 140, 90, "left") + love.graphics.printf(self.level, text_x, 340, 40, "right") + love.graphics.printf(self:getSectionEndLevel(), text_x, 370, 40, "right") +end + +function SurvivalA2Game:getSectionEndLevel() + if self.clear then return self.level + elseif self.level > 900 then return 999 + else return math.floor(self.level / 100 + 1) * 100 end +end + +function SurvivalA2Game:getBackground() + return math.floor(self.level / 100) +end + +function SurvivalA2Game:getHighscoreData() + return { + level = self.level, + frames = self.frames, + } +end + +return SurvivalA2Game diff --git a/tetris/modes/survival_a3.lua b/tetris/modes/survival_a3.lua new file mode 100644 index 0000000..7d9db12 --- /dev/null +++ b/tetris/modes/survival_a3.lua @@ -0,0 +1,236 @@ +require 'funcs' + +local GameMode = require 'tetris.modes.gamemode' +local Piece = require 'tetris.components.piece' + +local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls_35bag' + +local SurvivalA3Game = GameMode:extend() + +SurvivalA3Game.name = "Survival A3" +SurvivalA3Game.hash = "SurvivalA3" +SurvivalA3Game.tagline = "The blocks turn black and white! Can you make it to level 1300?" + +SurvivalA3Game.arr = 1 +SurvivalA3Game.drop_speed = 1 + +function SurvivalA3Game:new() + SurvivalA3Game.super:new() + self.level = 0 + self.grade = 0 + self.garbage = 0 + self.clear = false + self.completed = false + self.roll_frames = 0 + self.combo = 1 + self.randomizer = History6RollsRandomizer() + + self.lock_drop = true + self.enable_hold = true + self.next_queue_length = 3 +end + +function SurvivalA3Game:getARE() + if self.level < 300 then return 12 + else return 6 end +end + +function SurvivalA3Game:getLineARE() + if self.level < 100 then return 8 + elseif self.level < 200 then return 7 + elseif self.level < 500 then return 6 + elseif self.level < 1300 then return 5 + else return 6 end +end + +function SurvivalA3Game:getDasLimit() + if self.level < 200 then return 9 + elseif self.level < 500 then return 7 + else return 5 end +end + +function SurvivalA3Game:getLineClearDelay() + return self:getLineARE() - 2 +end + +function SurvivalA3Game:getLockDelay() + if self.level < 200 then return 18 + elseif self.level < 300 then return 17 + elseif self.level < 500 then return 15 + elseif self.level < 600 then return 13 + elseif self.level < 1100 then return 12 + elseif self.level < 1200 then return 10 + else return 8 end +end + +function SurvivalA3Game:getGravity() + return 20 +end + +function SurvivalA3Game:getGarbageLimit() + if self.level < 600 then return 20 + elseif self.level < 700 then return 18 + elseif self.level < 800 then return 10 + elseif self.level < 900 then return 9 + else return 8 end +end + +function SurvivalA3Game:getNextPiece(ruleset) + return { + skin = self.level >= 1000 and "bone" or "2tie", + shape = self.randomizer:nextPiece(), + orientation = ruleset:getDefaultOrientation(), + } +end + +function SurvivalA3Game:hitTorikan(old_level, new_level) + if old_level < 500 and new_level >= 500 and self.frames > sp(2,28) then + self.level = 500 + return true + end + if old_level < 1000 and new_level >= 1000 and self.frames > sp(4,56) then + self.level = 1000 + return true + end + return false +end + +function SurvivalA3Game:advanceOneFrame() + if self.clear then + self.roll_frames = self.roll_frames + 1 + if self.roll_frames < 0 then + if self.roll_frames + 1 == 0 then + switchBGM("credit_roll", "gm3") + return true + end + return false + elseif self.roll_frames > 3238 then + switchBGM(nil) + self.completed = true + end + elseif self.ready_frames == 0 then + self.frames = self.frames + 1 + end + return true +end + +function SurvivalA3Game:onPieceEnter() + if (self.level % 100 ~= 99) and not self.clear and self.frames ~= 0 then + self.level = self.level + 1 + end +end + +local cleared_row_levels = {1, 2, 4, 6} +local cleared_row_points = {0.02, 0.05, 0.15, 0.6} + +function SurvivalA3Game:onLineClear(cleared_row_count) + if not self.clear then + local new_level = self.level + cleared_row_levels[cleared_row_count] + self:updateSectionTimes(self.level, new_level) + if new_level >= 1300 or self:hitTorikan(self.level, new_level) then + if new_level >= 1300 then + self.level = 1300 + end + self.clear = true + self.grid:clear() + self.roll_frames = -150 + else + self.level = math.min(new_level, 1300) + end + self:advanceBottomRow(-cleared_row_count) + end +end + +function SurvivalA3Game:onPieceLock() + self:advanceBottomRow(1) +end + +function SurvivalA3Game: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 + +function SurvivalA3Game:updateSectionTimes(old_level, new_level) + if math.floor(old_level / 100) < math.floor(new_level / 100) then + local section = math.floor(old_level / 100) + 1 + section_time = self.frames - self.section_start_time + table.insert(self.section_times, section_time) + self.section_start_time = self.frames + if section_time <= sp(1,00) then + self.grade = self.grade + 1 + end + end +end + +function SurvivalA3Game:advanceBottomRow(dx) + if self.level >= 500 and self.level < 1000 then + self.garbage = math.max(self.garbage + dx, 0) + if self.garbage >= self:getGarbageLimit() then + self.grid:copyBottomRow() + self.garbage = 0 + end + end +end + +function SurvivalA3Game:drawGrid() + self.grid:draw() +end + +local function getLetterGrade(grade) + if grade == 0 then + return "1" + elseif grade <= 9 then + return "S" .. tostring(grade) + else + return "M" .. tostring(grade - 9) + end +end + +function SurvivalA3Game:drawScoringInfo() + SurvivalA3Game.super.drawScoringInfo(self) + + love.graphics.setColor(1, 1, 1, 1) + + local text_x = config["side_next"] and 320 or 240 + + love.graphics.setFont(font_3x5_2) + love.graphics.printf("GRADE", text_x, 120, 40, "left") + love.graphics.printf("SCORE", text_x, 200, 40, "left") + love.graphics.printf("LEVEL", text_x, 320, 40, "left") + + local current_section = math.floor(self.level / 100) + 1 + self:drawSectionTimesWithSplits(current_section) + + love.graphics.setFont(font_3x5_3) + love.graphics.printf(getLetterGrade(math.floor(self.grade)), text_x, 140, 90, "left") + love.graphics.printf(self.score, text_x, 220, 90, "left") + love.graphics.printf(self.level, text_x, 340, 50, "right") + if self.clear then + love.graphics.printf(self.level, text_x, 370, 50, "right") + else + love.graphics.printf(math.floor(self.level / 100 + 1) * 100, text_x, 370, 50, "right") + end +end + +function SurvivalA3Game:getBackground() + return math.floor(self.level / 100) +end + +function SurvivalA3Game:getHighscoreData() + return { + level = self.level, + frames = self.frames, + grade = self.grade, + } +end + +return SurvivalA3Game diff --git a/tetris/randomizers/always_o.lua b/tetris/randomizers/always_o.lua new file mode 100644 index 0000000..442a1d6 --- /dev/null +++ b/tetris/randomizers/always_o.lua @@ -0,0 +1,9 @@ +local Randomizer = require 'tetris.randomizers.randomizer' + +local AlwaysORandomizer = Randomizer:extend() + +function AlwaysORandomizer:generatePiece() + return "O" +end + +return AlwaysORandomizer diff --git a/tetris/randomizers/bag7.lua b/tetris/randomizers/bag7.lua new file mode 100644 index 0000000..e758a63 --- /dev/null +++ b/tetris/randomizers/bag7.lua @@ -0,0 +1,17 @@ +local Randomizer = require 'tetris.randomizers.randomizer' + +local Bag7Randomizer = Randomizer:extend() + +function Bag7Randomizer:initialize() + self.bag = {"I", "J", "L", "O", "S", "T", "Z"} +end + +function Bag7Randomizer: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)) + return table.remove(self.bag, x) +end + +return Bag7Randomizer diff --git a/tetris/randomizers/history_4rolls.lua b/tetris/randomizers/history_4rolls.lua new file mode 100644 index 0000000..fc57ee0 --- /dev/null +++ b/tetris/randomizers/history_4rolls.lua @@ -0,0 +1,34 @@ +local Randomizer = require 'tetris.randomizers.randomizer' + +local History4RollsRandomizer = Randomizer:extend() + +function History4RollsRandomizer:initialize() + self.history = {"Z", "S", "Z", "S"} +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 +end + +function History4RollsRandomizer:updateHistory(shape) + table.remove(self.history, 1) + table.insert(self.history, shape) + return shape +end + +function inHistory(piece, history) + for idx, entry in pairs(history) do + if entry == piece then + return true + end + end + return false +end + +return History4RollsRandomizer diff --git a/tetris/randomizers/history_6rolls.lua b/tetris/randomizers/history_6rolls.lua new file mode 100644 index 0000000..95b2c6e --- /dev/null +++ b/tetris/randomizers/history_6rolls.lua @@ -0,0 +1,34 @@ +local Randomizer = require 'tetris.randomizers.randomizer' + +local History6RollsRandomizer = Randomizer:extend() + +function History6RollsRandomizer:initialize() + self.history = {"Z", "S", "Z", "S"} +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 +end + +function History6RollsRandomizer:updateHistory(shape) + table.remove(self.history, 1) + table.insert(self.history, shape) + return shape +end + +function inHistory(piece, history) + for idx, entry in pairs(history) do + if entry == piece then + return true + end + end + return false +end + +return History6RollsRandomizer diff --git a/tetris/randomizers/history_6rolls_35bag.lua b/tetris/randomizers/history_6rolls_35bag.lua new file mode 100644 index 0000000..6385902 --- /dev/null +++ b/tetris/randomizers/history_6rolls_35bag.lua @@ -0,0 +1,47 @@ +local Randomizer = require 'tetris.randomizers.randomizer' + +local History6RollsRandomizer = Randomizer:extend() + +function History6RollsRandomizer:initialize() + self.history = {"Z", "S", "Z", "S"} + self.bag_counts = { + I = 5, J = 5, L = 5, O = 5, S = 3, T = 5, Z = 3 + } +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 +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) + table.insert(self.history, shape) + self.bag_counts[replaced_piece] = self.bag_counts[replaced_piece] + 1 + return shape +end + +function inHistory(piece, history) + for idx, entry in pairs(history) do + if entry == piece then + return true + end + end + return false +end + +return History6RollsRandomizer diff --git a/tetris/randomizers/randomizer.lua b/tetris/randomizers/randomizer.lua new file mode 100644 index 0000000..7fc40f9 --- /dev/null +++ b/tetris/randomizers/randomizer.lua @@ -0,0 +1,28 @@ +local Object = require 'libs.classic' + +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) +end + +function Randomizer:initialize() + -- do nothing +end + +local shapes = {"I", "J", "L", "O", "S", "T", "Z"} + +function Randomizer:generatePiece() + return shapes[math.random(7)] +end + +return Randomizer diff --git a/tetris/rulesets/arika.lua b/tetris/rulesets/arika.lua new file mode 100644 index 0000000..e9f2f79 --- /dev/null +++ b/tetris/rulesets/arika.lua @@ -0,0 +1,98 @@ +local Piece = require 'tetris.components.piece' +local Ruleset = require 'tetris.rulesets.ruleset' + +local ARS = Ruleset:extend() + +ARS.name = "Classic ARS" +ARS.hash = "Arika" + +ARS.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 }, +} + +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} }, + } +} + +function ARS:attemptWallkicks(piece, new_piece, rot_dir, grid) + + -- I and O don't kick + if (piece.shape == "I" or 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 + ) 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 + + -- kick right, kick 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=-1, y=0}))) then + piece:setRelativeRotation(rot_dir):setOffset({x=-1, y=0}) + self:onPieceRotate(piece, grid) + end + +end + +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:getDefaultOrientation() return 3 end -- downward facing pieces by default + +return ARS diff --git a/tetris/rulesets/arika_ti.lua b/tetris/rulesets/arika_ti.lua new file mode 100644 index 0000000..663a608 --- /dev/null +++ b/tetris/rulesets/arika_ti.lua @@ -0,0 +1,132 @@ +local Piece = require 'tetris.components.piece' +local Ruleset = require 'tetris.rulesets.ruleset' + +local ARS = Ruleset:extend() + +ARS.name = "Ti-ARS" +ARS.hash = "ArikaTI" + +ARS.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 }, +} + +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 + ) 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 + + 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 + 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 + end + +end + +function ARS:onPieceCreate(piece, grid) + piece.floorkick = 0 +end + +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:getDefaultOrientation() return 3 end -- downward facing pieces by default + +return ARS diff --git a/tetris/rulesets/bonkers.lua b/tetris/rulesets/bonkers.lua new file mode 100644 index 0000000..b29b915 --- /dev/null +++ b/tetris/rulesets/bonkers.lua @@ -0,0 +1,102 @@ +Piece = require("tetris.components.piece") + +local BONKERS = {} + +BONKERS.name = "B.O.N.K.E.R.S." +BONKERS.hash = "Bonkers" + +BONKERS.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 }, +} + +BONKERS.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} }, + } +} + +-- Component functions. + +function BONKERS:attemptWallkicks(piece, new_piece, rot_dir, grid) + + if piece.shape == "O" then + break + elseif piece.shape == "I" then + horizontal_kicks = {0, 1, -1, 2, -2} + else + horizontal_kicks = {0, 1, -1} + end + + for y_offset = 20, new_piece.position.y - 24, -1 do + for idx, x_offset in pairs(horizontal_kicks) do + local offset = {x=x_offset, y=y_offset} + kicked_piece = new_piece:withOffset(offset) + if grid:canPlacePiece(kicked_piece) then + piece:setRelativeRotation(rot_dir) + piece:setOffset(offset) + piece.lock_delay = 0 -- rotate reset + return + end + end + end + +end + +function BONKERS:onPieceDrop(piece, grid) + piece.lock_delay = 0 -- step reset +end + +function BONKERS:onPieceMove(piece, grid) + piece.lock_delay = 0 -- move reset +end + +function BONKERS:onPieceRotate(piece, grid) + piece.lock_delay = 0 -- rotate reset +end + +return BONKERS diff --git a/tetris/rulesets/cambridge.lua b/tetris/rulesets/cambridge.lua new file mode 100644 index 0000000..d03992e --- /dev/null +++ b/tetris/rulesets/cambridge.lua @@ -0,0 +1,410 @@ +local Piece = require 'tetris.components.piece' +local Ruleset = require 'tetris.rulesets.ruleset' + +local CRS = Ruleset:extend() + +CRS.name = "Cambridge" +CRS.hash = "Cambridge" + +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 }, + T = { x=4, y=5 }, + Z = { x=4, y=4 }, +} + +CRS.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=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} }, + }, + 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} }, + } +} + +CRS.wallkicks = { + I={ + [true] = { + [0]={ + [1]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=-1}, {x=-1, y=-1}, {x=0, y=-2}, {x=-1, y=-2}}, + [2]={{x=0, y=0}}, + [3]={{x=0, y=0}, {x=1, y=0}, {x=0, y=-1}, {x=1, y=-1}, {x=0, y=-2}, {x=1, y=-2}}, + }, + [1]={ + [2]={{x=0, y=0}, {x=1, y=0}, {x=0, y=1}, {x=1, y=1}, {x=-1, y=0}, {x=2, y=0}, {x=-1, y=1}, {x=2, y=1}}, + [3]={{x=0, y=0}}, + [0]={{x=0, y=0}, {x=1, y=0}, {x=0, y=1}, {x=1, y=1}, {x=-1, y=0}, {x=2, y=0}, {x=-1, y=1}, {x=2, y=1}}, + }, + [2]={ + [3]={{x=0, y=0}, {x=1, y=0}, {x=0, y=-1}, {x=1, y=-1}, {x=0, y=-2}, {x=1, y=-2}}, + [0]={{x=0, y=0}}, + [1]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=-1}, {x=-1, y=-1}, {x=0, y=-2}, {x=-1, y=-2}}, + }, + [3]={ + [0]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=1}, {x=-1, y=1}, {x=1, y=0}, {x=-2, y=0}, {x=1, y=1}, {x=-2, y=1}}, + [1]={{x=0, y=0}}, + [2]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=1}, {x=-1, y=1}, {x=1, y=0}, {x=-2, y=0}, {x=1, y=1}, {x=-2, y=1}}, + }, + }, + [false] = { + [0]={ + [1]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=-1}, {x=-1, y=-1}, {x=0, y=-2}, {x=-1, y=-2}}, + [2]={{x=0, y=0}}, + [3]={{x=0, y=0}, {x=1, y=0}, {x=0, y=-1}, {x=1, y=-1}, {x=0, y=-2}, {x=1, y=-2}}, + }, + [1]={ + [2]={{x=0, y=0}, {x=1, y=0}, {x=0, y=1}, {x=1, y=1}, {x=-1, y=0}, {x=2, y=0}, {x=-1, y=1}, {x=2, y=1}}, + [3]={{x=0, y=0}}, + [0]={{x=0, y=0}, {x=1, y=0}, {x=0, y=1}, {x=1, y=1}, {x=-1, y=0}, {x=2, y=0}, {x=-1, y=1}, {x=2, y=1}}, + }, + [2]={ + [3]={{x=0, y=0}, {x=1, y=0}, {x=0, y=-1}, {x=1, y=-1}, {x=0, y=-2}, {x=1, y=-2}}, + [0]={{x=0, y=0}}, + [1]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=-1}, {x=-1, y=-1}, {x=0, y=-2}, {x=-1, y=-2}}, + }, + [3]={ + [0]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=1}, {x=-1, y=1}, {x=1, y=0}, {x=-2, y=0}, {x=1, y=1}, {x=-2, y=1}}, + [1]={{x=0, y=0}}, + [2]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=1}, {x=-1, y=1}, {x=1, y=0}, {x=-2, y=0}, {x=1, y=1}, {x=-2, y=1}}, + }, + }, + }, + J={ + [true] = { + [0]={ + [1]={{x=0, y=-1}, {x=-1, y=-1}, {x=0, y=0}, {x=-1, y=0}}, -- allows for the "J situp" + [2]={{x=0, y=0}, {x=0, y=-1}}, + [3]={{x=0, y=-1}, {x=1, y=-1}, {x=0, y=0}, {x=1, y=0}}, + }, + [1]={ + [2]={{x=0, y=0}, {x=1, y=0}, {x=-1, y=0}}, + [3]={{x=0, y=0}, {x=0, y=1}, {x=0, y=-1}}, + [0]={{x=0, y=1}, {x=1, y=1}, {x=0, y=0}, {x=-1, y=1}, {x=1, y=0}}, + }, + [2]={ + [3]={{x=0, y=0}, {x=1, y=0}, {x=0, y=1}, {x=1, y=1}}, + [0]={{x=0, y=0}, {x=0, y=1}}, + [1]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=1}, {x=-1, y=1}}, + }, + [3]={ + [0]={{x=0, y=1}, {x=-1, y=1}, {x=0, y=0}, {x=1, y=1}, {x=-1, y=0}}, + [1]={{x=0, y=0}, {x=0, y=1}, {x=0, y=-1}}, + [2]={{x=0, y=0}, {x=-1, y=0}, {x=1, y=0}}, + }, + }, + [false] = { + [0]={ + [1]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=1}, {x=-1, y=1}}, + [2]={{x=0, y=0}, {x=0, y=-1}}, + [3]={{x=0, y=0}, {x=1, y=0}, {x=0, y=1}, {x=1, y=1}}, + }, + [1]={ + [2]={{x=0, y=0}, {x=1, y=0}, {x=-1, y=0}}, + [3]={{x=0, y=0}, {x=0, y=1}, {x=0, y=-1}}, + [0]={{x=0, y=0}, {x=1, y=0}, {x=0, y=-1}, {x=-1, y=0}, {x=1, y=-1}}, + }, + [2]={ + [3]={{x=0, y=0}, {x=1, y=0}, {x=0, y=1}, {x=1, y=1}}, + [0]={{x=0, y=0}, {x=0, y=1}}, + [1]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=1}, {x=-1, y=1}}, + }, + [3]={ + [0]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=-1}, {x=1, y=0}, {x=-1, y=-1}}, + [1]={{x=0, y=0}, {x=0, y=1}, {x=0, y=-1}}, + [2]={{x=0, y=0}, {x=-1, y=0}, {x=1, y=0}}, + }, + }, + }, + L={ + [true] = { + [0]={ + [1]={{x=0, y=-1}, {x=-1, y=-1}, {x=0, y=0}, {x=-1, y=0}}, + [2]={{x=0, y=0}, {x=0, y=-1}}, + [3]={{x=0, y=-1}, {x=1, y=-1}, {x=0, y=0}, {x=1, y=0}}, -- allows for the "L situp" + }, + [1]={ + [2]={{x=0, y=0}, {x=1, y=0}, {x=-1, y=0}}, + [3]={{x=0, y=0}, {x=0, y=1}, {x=0, y=-1}}, + [0]={{x=0, y=0}, {x=1, y=0}, {x=0, y=-1}, {x=-1, y=0}, {x=1, y=-1}}, + }, + [2]={ + [3]={{x=0, y=0}, {x=1, y=0}, {x=0, y=1}, {x=1, y=1}}, + [0]={{x=0, y=0}, {x=0, y=1}}, + [1]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=1}, {x=-1, y=1}}, + }, + [3]={ + [0]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=-1}, {x=1, y=0}, {x=-1, y=-1}}, + [1]={{x=0, y=0}, {x=0, y=1}, {x=0, y=-1}}, + [2]={{x=0, y=0}, {x=-1, y=0}, {x=1, y=0}}, + }, + }, + [false] = { + [0]={ + [1]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=1}, {x=-1, y=1}}, + [2]={{x=0, y=0}, {x=0, y=-1}}, + [3]={{x=0, y=0}, {x=1, y=0}, {x=0, y=1}, {x=1, y=1}}, + }, + [1]={ + [2]={{x=0, y=0}, {x=1, y=0}, {x=-1, y=0}}, + [3]={{x=0, y=0}, {x=0, y=1}, {x=0, y=-1}}, + [0]={{x=0, y=0}, {x=1, y=0}, {x=0, y=-1}, {x=-1, y=0}, {x=1, y=-1}}, + }, + [2]={ + [3]={{x=0, y=0}, {x=1, y=0}, {x=0, y=1}, {x=1, y=1}}, + [0]={{x=0, y=0}, {x=0, y=1}}, + [1]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=1}, {x=-1, y=1}}, + }, + [3]={ + [0]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=-1}, {x=1, y=0}, {x=-1, y=-1}}, + [1]={{x=0, y=0}, {x=0, y=1}, {x=0, y=-1}}, + [2]={{x=0, y=0}, {x=-1, y=0}, {x=1, y=0}}, + }, + }, + }, + S={ + [true] = { + [0]={ + [1]={{x=0, y=1}, {x=-1, y=1}, {x=0, y=0}, {x=-1, y=0}}, + [2]={{x=0, y=0}, {x=-1, y=1}}, + [3]={{x=0, y=1}, {x=-1, y=1}, {x=0, y=0}, {x=-1, y=0}}, + }, + [1]={ + [0]={{x=0, y=0}, {x=1, y=0}, {x=0, y=-1}, {x=1, y=-1}}, + [2]={{x=0, y=0}, {x=1, y=0}, {x=0, y=-1}, {x=1, y=-1}}, + [3]={{x=0, y=0}, {x=1, y=1}}, + }, + [2]={ + [0]={{x=0, y=0}, {x=-1, y=1}}, + [1]={{x=0, y=1}, {x=-1, y=1}, {x=0, y=0}, {x=-1, y=0}}, + [3]={{x=0, y=1}, {x=-1, y=1}, {x=0, y=0}, {x=-1, y=0}}, + }, + [3]={ + [0]={{x=0, y=0}, {x=1, y=0}, {x=0, y=-1}, {x=1, y=-1}}, + [1]={{x=0, y=0}, {x=1, y=1}}, + [2]={{x=0, y=0}, {x=1, y=0}, {x=0, y=-1}, {x=1, y=-1}}, + }, + }, + [false] = { + [0]={ + [1]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=1}, {x=-1, y=1}}, + [2]={{x=0, y=0}, {x=-1, y=1}}, + [3]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=1}, {x=-1, y=1}}, + }, + [1]={ + [0]={{x=0, y=0}, {x=1, y=0}, {x=0, y=-1}, {x=1, y=-1}}, + [2]={{x=0, y=0}, {x=1, y=0}, {x=0, y=-1}, {x=1, y=-1}}, + [3]={{x=0, y=0}, {x=1, y=1}}, + }, + [2]={ + [0]={{x=0, y=0}, {x=-1, y=1}}, + [1]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=1}, {x=-1, y=1}}, + [3]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=1}, {x=-1, y=1}}, + }, + [3]={ + [0]={{x=0, y=0}, {x=1, y=0}, {x=0, y=-1}, {x=1, y=-1}}, + [1]={{x=0, y=0}, {x=1, y=1}}, + [2]={{x=0, y=0}, {x=1, y=0}, {x=0, y=-1}, {x=1, y=-1}}, + }, + }, + }, + T={ + [true] = { + [0]={ + [1]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=-1}, {x=-1, y=-1}}, + [2]={{x=0, y=0}, {x=0, y=-1}}, + [3]={{x=0, y=0}, {x=1, y=0}, {x=0, y=-1}, {x=1, y=-1}}, + }, + [1]={ + [0]={{x=0, y=1}, {x=1, y=1}, {x=0, y=0}, {x=1, y=0}}, -- prioritizes the nub T spin over the regular T spin + [2]={{x=0, y=0}, {x=1, y=0}}, + [3]={{x=0, y=0}, {x=0, y=1}, {x=0, y=-1}}, + }, + [2]={ + [0]={{x=0, y=0}, {x=0, y=1}}, + [1]={{x=0, y=0}, {x=-1, y=0}}, + [3]={{x=0, y=0}, {x=1, y=0}}, + }, + [3]={ + [0]={{x=0, y=1}, {x=-1, y=1}, {x=0, y=0}, {x=-1, y=0}}, -- prioritizes the nub T spin over the regular T spin + [1]={{x=0, y=0}, {x=0, y=1}, {x=0, y=-1}}, + [2]={{x=0, y=0}, {x=-1, y=0}}, + }, + }, + [false] = { + [0]={ + [1]={{x=0, y=0}, {x=1, y=0}, {x=0, y=1}, {x=1, y=1}}, + [2]={{x=0, y=0}, {x=0, y=-1}}, + [3]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=1}, {x=-1, y=1}}, + }, + [1]={ + [0]={{x=0, y=0}, {x=1, y=0}, {x=0, y=-1}, {x=1, y=-1}}, + [2]={{x=0, y=0}, {x=1, y=0}}, + [3]={{x=0, y=0}, {x=0, y=1}, {x=0, y=-1}}, + }, + [2]={ + [0]={{x=0, y=0}, {x=0, y=1}}, + [1]={{x=0, y=0}, {x=-1, y=0}}, + [3]={{x=0, y=0}, {x=1, y=0}}, + }, + [3]={ + [0]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=-1}, {x=-1, y=-1}}, + [1]={{x=0, y=0}, {x=0, y=1}, {x=0, y=-1}}, + [2]={{x=0, y=0}, {x=-1, y=0}}, + }, + }, + }, + Z={ + [true] = { + [0]={ + [1]={{x=0, y=1}, {x=1, y=1}, {x=0, y=0}, {x=1, y=0}}, + [2]={{x=0, y=0}, {x=1, y=1}}, + [3]={{x=0, y=1}, {x=1, y=1}, {x=0, y=0}, {x=1, y=0}}, + }, + [1]={ + [0]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=-1}, {x=-1, y=-1}}, + [2]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=-1}, {x=-1, y=-1}}, + [3]={{x=0, y=0}, {x=-1, y=1}}, + }, + [2]={ + [0]={{x=0, y=0}, {x=1, y=1}}, + [1]={{x=0, y=1}, {x=1, y=1}, {x=0, y=0}, {x=1, y=0}}, + [3]={{x=0, y=1}, {x=1, y=1}, {x=0, y=0}, {x=1, y=0}}, + }, + [3]={ + [0]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=-1}, {x=-1, y=-1}}, + [1]={{x=0, y=0}, {x=-1, y=1}}, + [2]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=-1}, {x=-1, y=-1}}, + }, + }, + [false] = { + [0]={ + [1]={{x=0, y=0}, {x=1, y=0}, {x=0, y=1}, {x=1, y=1}}, + [2]={{x=0, y=0}, {x=1, y=1}}, + [3]={{x=0, y=0}, {x=1, y=0}, {x=0, y=1}, {x=1, y=1}}, + }, + [1]={ + [0]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=-1}, {x=-1, y=-1}}, + [2]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=-1}, {x=-1, y=-1}}, + [3]={{x=0, y=0}, {x=-1, y=1}}, + }, + [2]={ + [0]={{x=0, y=0}, {x=1, y=1}}, + [1]={{x=0, y=0}, {x=1, y=0}, {x=0, y=1}, {x=1, y=1}}, + [3]={{x=0, y=0}, {x=1, y=0}, {x=0, y=1}, {x=1, y=1}}, + }, + [3]={ + [0]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=-1}, {x=-1, y=-1}}, + [1]={{x=0, y=0}, {x=-1, y=1}}, + [2]={{x=0, y=0}, {x=-1, y=0}, {x=0, y=-1}, {x=-1, y=-1}}, + }, + }, + }, +} + +function CRS:attemptRotate(new_inputs, piece, grid, initial) + local rot_dir = 0 + + if (new_inputs["rotate_left"] or new_inputs["rotate_left2"]) then + rot_dir = 3 + elseif (new_inputs["rotate_right"] or new_inputs["rotate_right2"]) then + rot_dir = 1 + elseif (new_inputs["rotate_180"]) then + rot_dir = self:get180RotationValue() + end + + if rot_dir == 0 then return end + + local new_piece = piece:withRelativeRotation(rot_dir) + self:attemptWallkicks(piece, new_piece, rot_dir, grid) +end + + +function CRS:attemptWallkicks(piece, new_piece, rot_dir, grid) + + if piece.shape == "O" then return end + + local kicks = CRS.wallkicks[piece.shape][piece:isDropBlocked(grid)][piece.rotation][new_piece.rotation] + + 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 CRS:onPieceCreate(piece, grid) + piece.rotate_counter = 0 + piece.move_counter = 0 +end + +function CRS:onPieceDrop(piece, grid) + piece.lock_delay = 0 -- step reset +end + +function CRS:onPieceMove(piece, grid) + if piece:isDropBlocked(grid) then + piece.move_counter = piece.move_counter + 1 + if piece.move_counter >= 24 then + piece.locked = true + end + end +end + +function CRS:onPieceRotate(piece, grid) + if piece:isDropBlocked(grid) then + piece.rotate_counter = piece.rotate_counter + 1 + if piece.rotate_counter >= 12 then + piece.locked = true + end + end +end + +function CRS:getDefaultOrientation() return 1 end -- downward facing pieces by default + +return CRS diff --git a/tetris/rulesets/ruleset.lua b/tetris/rulesets/ruleset.lua new file mode 100644 index 0000000..3e20cc1 --- /dev/null +++ b/tetris/rulesets/ruleset.lua @@ -0,0 +1,135 @@ +local Object = require 'libs.classic' +local Piece = require 'tetris.components.piece' + +local Ruleset = Object:extend() + +Ruleset.name = "" +Ruleset.hash = "" + +Ruleset.enable_IRS_wallkicks = false + +-- Component functions. + +function Ruleset:rotatePiece(inputs, piece, grid, prev_inputs, initial) + local new_inputs = {} + + for input, value in pairs(inputs) do + if value and not prev_inputs[input] then + new_inputs[input] = true + end + end + + self:attemptRotate(new_inputs, piece, grid, initial) + + -- prev_inputs becomes the previous inputs + for input, value in pairs(inputs) do + prev_inputs[input] = inputs[input] + end +end + +function Ruleset:attemptRotate(new_inputs, piece, grid, initial) + local rot_dir = 0 + + if (new_inputs["rotate_left"] or new_inputs["rotate_left2"]) then + rot_dir = 3 + elseif (new_inputs["rotate_right"] or new_inputs["rotate_right2"]) then + rot_dir = 1 + elseif (new_inputs["rotate_180"]) then + rot_dir = self:get180RotationValue() + end + + if rot_dir == 0 then return end + + local new_piece = piece:withRelativeRotation(rot_dir) + + if (grid:canPlacePiece(new_piece)) then + piece:setRelativeRotation(rot_dir) + self:onPieceRotate(piece, grid) + else + if not(initial and self.enable_IRS_wallkicks == false) then + self:attemptWallkicks(piece, new_piece, rot_dir, grid) + end + end +end + +function Ruleset:attemptWallkicks(piece, new_piece, rot_dir, grid) + -- do nothing in default ruleset +end + +function Ruleset:movePiece(piece, grid, move) + local x = piece.position.x + if move == "left" then + piece:moveInGrid({x=-1, y=0}, 1, grid) + elseif move == "speedleft" then + piece:moveInGrid({x=-1, y=0}, 10, grid) + elseif move == "right" then + piece:moveInGrid({x=1, y=0}, 1, grid) + elseif move == "speedright" then + piece:moveInGrid({x=1, y=0}, 10, grid) + end + if piece.position.x ~= x then + self:onPieceMove(piece, grid) + end +end + +function Ruleset:dropPiece(inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked, hard_drop_enabled) + local y = piece.position.y + if inputs["down"] == true and drop_locked == false then + piece:addGravity(gravity + drop_speed, grid) + elseif inputs["up"] == true and hard_drop_enabled == true then + if hard_drop_locked == true or piece:isDropBlocked(grid) then + piece:addGravity(gravity, grid) + else + piece:dropToBottom(grid) + end + else + piece:addGravity(gravity, grid) + end + if piece.position.y ~= y then + self:onPieceDrop(piece, grid) + end +end + +function Ruleset:lockPiece(piece, grid, lock_delay) + if piece:isDropBlocked(grid) and piece.gravity >= 1 and piece.lock_delay >= lock_delay then + piece.locked = true + end +end + +function Ruleset:get180RotationValue() return 2 end +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 +) + 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) + self:onPieceCreate(piece) + self:rotatePiece(inputs, piece, grid, {}, true) + self:dropPiece(inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked) + return piece +end + +-- stuff like move count, rotate count, floorkick count go here +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 +) + 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:lockPiece(piece, grid, lock_delay) +end + +function Ruleset:onPieceMove(piece) end +function Ruleset:onPieceRotate(piece) end +function Ruleset:onPieceDrop(piece) end + +return Ruleset diff --git a/tetris/rulesets/standard_exp.lua b/tetris/rulesets/standard_exp.lua new file mode 100644 index 0000000..1b8d2df --- /dev/null +++ b/tetris/rulesets/standard_exp.lua @@ -0,0 +1,168 @@ +local Piece = require 'tetris.components.piece' +local Ruleset = require 'tetris.rulesets.ruleset' + +local SRS = Ruleset:extend() + +SRS.name = "SRS" +SRS.hash = "Standard" + +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.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=-2, y=1}, {x=1, y=-2}}, + [2]={}, + [3]={{x=-1, y=0}, {x=2, 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]={{x=0, y=1}, {x=0, y=-1}, {x=0, y=2}, {x=0, y=-2}}, + }, + [2]={ + [0]={}, + [1]={{x=1, y=0}, {x=-2, y=0}, {x=1, y=2}, {x=-2, y=-1}}, + [3]={{x=2, y=0}, {x=-1, y=0}, {x=2, y=-1}, {x=-1, y=2}}, + }, + [3]={ + [0]={{x=1, y=0}, {x=-2, y=0}, {x=1, y=2}, {x=-2, y=-1}}, + [1]={{x=0, y=1}, {x=0, y=-1}, {x=0, y=2}, {x=0, y=-2}}, + [2]={{x=-2, y=0}, {x=1, y=0}, {x=-2, y=1}, {x=1, y=-2}}, + }, +}; + +-- 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.rotate_counter = 0 + piece.move_counter = 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.move_counter = piece.move_counter + 1 + if piece.move_counter >= 24 then + piece.locked = true + end + end +end + +function SRS:onPieceRotate(piece, grid) + piece.lock_delay = 0 -- rotate reset + if piece:isDropBlocked(grid) then + piece.rotate_counter = piece.rotate_counter + 1 + if piece.rotate_counter >= 12 then + piece.locked = true + end + end +end + +return SRS diff --git a/tetris/rulesets/tengen.lua b/tetris/rulesets/tengen.lua new file mode 100644 index 0000000..9b6ccfd --- /dev/null +++ b/tetris/rulesets/tengen.lua @@ -0,0 +1,133 @@ +local Piece = require 'tetris.components.piece' +local Ruleset = require 'tetris.rulesets.ruleset' + +local Tengen = Ruleset:extend() + +Tengen.name = "Tengen" +Tengen.hash = "Tengen" + +Tengen.spawn_positions = { + I = { x=3, y=4 }, + J = { x=4, y=4 }, + L = { x=4, y=4 }, + O = { x=5, y=4 }, + S = { x=4, y=4 }, + T = { x=4, y=4 }, + Z = { x=4, y=4 }, +} + +Tengen.block_offsets = { + I={ + { {x=0, y=0}, {x=1, y=0}, {x=2, y=0}, {x=3, y=0} }, + { {x=0, y=0}, {x=0, y=1}, {x=0, y=2}, {x=0, y=3} }, + { {x=0, y=0}, {x=1, y=0}, {x=2, y=0}, {x=3, y=0} }, + { {x=0, y=0}, {x=0, y=1}, {x=0, y=2}, {x=0, y=3} }, + }, + J={ + { {x=0, y=0}, {x=1, y=0}, {x=2, y=0}, {x=2, y=1} }, + { {x=1, y=0}, {x=1, y=1}, {x=1, y=2}, {x=0, y=2} }, + { {x=0, y=0}, {x=0, y=1}, {x=1, y=1}, {x=2, y=1} }, + { {x=0, y=0}, {x=0, y=1}, {x=0, y=2}, {x=1, y=0} }, + }, + L={ + { {x=0, y=0}, {x=1, y=0}, {x=2, y=0}, {x=0, y=1} }, + { {x=0, y=0}, {x=0, y=1}, {x=0, y=2}, {x=1, y=2} }, + { {x=2, y=0}, {x=0, y=1}, {x=1, y=1}, {x=2, y=1} }, + { {x=0, y=0}, {x=1, y=0}, {x=1, y=1}, {x=1, y=2} }, + }, + 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} }, + }, + -- up to here + S={ + { {x=0, y=0}, {x=1, y=0}, {x=2, y=0}, {x=3, y=0} }, + { {x=0, y=0}, {x=0, y=1}, {x=0, y=2}, {x=0, y=3} }, + { {x=0, y=0}, {x=1, y=0}, {x=2, y=0}, {x=3, y=0} }, + { {x=0, y=0}, {x=0, y=1}, {x=0, y=2}, {x=0, y=3} }, + }, + T={ + { {x=0, y=0}, {x=1, y=0}, {x=2, y=0}, {x=3, y=0} }, + { {x=0, y=0}, {x=0, y=1}, {x=0, y=2}, {x=0, y=3} }, + { {x=0, y=0}, {x=1, y=0}, {x=2, y=0}, {x=3, y=0} }, + { {x=0, y=0}, {x=0, y=1}, {x=0, y=2}, {x=0, y=3} }, + }, + Z={ + { {x=0, y=0}, {x=1, y=0}, {x=2, y=0}, {x=3, y=0} }, + { {x=0, y=0}, {x=0, y=1}, {x=0, y=2}, {x=0, y=3} }, + { {x=0, y=0}, {x=1, y=0}, {x=2, y=0}, {x=3, y=0} }, + { {x=0, y=0}, {x=0, y=1}, {x=0, y=2}, {x=0, y=3} }, + } +} + + +-- Component functions. + +function Tengen: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 + ) 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 + + 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 + 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 + end + +end + +function Tengen:onPieceCreate(piece, grid) + piece.floorkick = 0 +end + +function Tengen:onPieceDrop(piece, grid) + piece.lock_delay = 0 -- step reset +end + +function Tengen:get180RotationValue() return config["reverse_rotate"] and 1 or 3 end +function Tengen:getDefaultOrientation() return 3 end -- downward facing pieces by default + +return Tengen diff --git a/tetris/rulesets/unrefactored_rulesets/shirase.lua b/tetris/rulesets/unrefactored_rulesets/shirase.lua new file mode 100644 index 0000000..d1ad015 --- /dev/null +++ b/tetris/rulesets/unrefactored_rulesets/shirase.lua @@ -0,0 +1,235 @@ +Piece = require("tetris.components.piece") +require("funcs") + +local SRS = {} + +SRS.name = "SHIRASE" +SRS.hash = "Shirase" + +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.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=-2, y=1}, {x=1, y=-2}}, + [2]={}, + [3]={{x=-1, y=0}, {x=2, 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]={{x=0, y=1}, {x=0, y=-1}, {x=0, y=2}, {x=0, y=-2}}, + }, + [2]={ + [0]={}, + [1]={{x=1, y=0}, {x=-2, y=0}, {x=1, y=2}, {x=-2, y=-1}}, + [3]={{x=2, y=0}, {x=-1, y=0}, {x=2, y=-1}, {x=-1, y=2}}, + }, + [3]={ + [0]={{x=1, y=0}, {x=-2, y=0}, {x=1, y=2}, {x=-2, y=-1}}, + [1]={{x=0, y=1}, {x=0, y=-1}, {x=0, y=2}, {x=0, y=-2}}, + [2]={{x=-2, y=0}, {x=1, y=0}, {x=-2, y=1}, {x=1, y=-2}}, + }, +}; + +local basicOffsets = { + [0] = { x = 1, y = 0 }, + [1] = { x = 0, y = 1 }, + [2] = { x = -1, y = 0 }, + [3] = { x = 0, y = -1 } +} + +-- Component functions. + +local function rotatePiece(inputs, piece, grid, prev_inputs) + local new_inputs = {} + + for input, value in pairs(inputs) do + if value and not prev_inputs[input] then + new_inputs[input] = true + end + end + + local rot_dir = 0 + if (new_inputs["rotate_left"] or new_inputs["rotate_left2"]) then + rot_dir = 3 + elseif (new_inputs["rotate_right"] or new_inputs["rotate_right2"]) then + rot_dir = 1 + elseif (new_inputs["rotate_180"]) then + rot_dir = 2 + end + + while rot_dir ~= 0 do + rotated_piece = piece:withRelativeRotation(rot_dir) + rotation_offset = vAdd( + basicOffsets[piece.rotation], + vNeg(basicOffsets[rotated_piece.rotation]) + ) + new_piece = rotated_piece:withOffset(rotation_offset) + + if (grid:canPlacePiece(new_piece)) then + piece:setRelativeRotation(rot_dir) + piece:setOffset(rotation_offset) + piece.lock_delay = 0 -- rotate reset + break + end + + if piece.shape == "I" then + kicks = SRS.wallkicks_line[piece.rotation][new_piece.rotation] + else + kicks = SRS.wallkicks_3x3[piece.rotation][new_piece.rotation] + end + + 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(vAdd(offset, rotation_offset)) + piece.lock_delay = 0 -- rotate reset + rot_dir = 0 + end + if rot_dir == 0 then + break + end + end + + rot_dir = 0 + end + + -- prev_inputs becomes the previous inputs + for input, value in pairs(inputs) do + prev_inputs[input] = inputs[input] + end +end + +local function movePiece(piece, grid, move) + if move == "left" then + if not piece:isMoveBlocked(grid, {x=-1, y=0}) then + piece.lock_delay = 0 -- move reset + end + piece:moveInGrid({x=-1, y=0}, 1, grid) + elseif move == "right" then + if not piece:isMoveBlocked(grid, {x=1, y=0}) then + piece.lock_delay = 0 -- move reset + end + piece:moveInGrid({x=1, y=0}, 1, grid) + end +end + +local function dropPiece(inputs, piece, grid, gravity, drop_speed, drop_locked) + local y = piece.position.y + if inputs["down"] == true and drop_locked == false then + piece:addGravity(gravity + 1, grid):lockIfBottomed(grid) + elseif inputs["up"] == true then + if piece:isDropBlocked(grid) then + return + end + piece:dropToBottom(grid) + else + piece:addGravity(gravity, grid) + end + if piece.position.y ~= y then -- step reset + piece.lock_delay = 0 + end +end + +local function lockPiece(piece, grid, lock_delay) + if piece:isDropBlocked(grid) and piece.lock_delay >= lock_delay then + piece.locked = true + end +end + +function SRS.initializePiece(inputs, data, grid, gravity, prev_inputs, move, lock_delay, drop_speed, drop_locked) + local piece = Piece(shape, 0, { + x = SRS.spawn_positions[shape].x, + y = SRS.spawn_positions[shape].y + }, SRS.block_offsets, 0, 0) + -- have to copy that object otherwise it gets referenced + rotatePiece(inputs, piece, grid, {}) + dropPiece(inputs, piece, grid, gravity, drop_speed, drop_locked) + return piece +end + +function SRS.processPiece(inputs, piece, grid, gravity, prev_inputs, move, lock_delay, drop_speed, drop_locked) + rotatePiece(inputs, piece, grid, prev_inputs) + movePiece(piece, grid, move) + dropPiece(inputs, piece, grid, gravity, drop_speed, drop_locked) + lockPiece(piece, grid, lock_delay) +end + +return SRS diff --git a/tetris/rulesets/unrefactored_rulesets/super302.lua b/tetris/rulesets/unrefactored_rulesets/super302.lua new file mode 100644 index 0000000..8b45a55 --- /dev/null +++ b/tetris/rulesets/unrefactored_rulesets/super302.lua @@ -0,0 +1,174 @@ +Piece = require("tetris.components.piece") + +local BONKERS = {} + +BONKERS.name = "SUPER302" +BONKERS.hash = "Super302" + +BONKERS.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 }, +} + +BONKERS.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} }, + } +} + +-- Component functions. + +local function rotatePiece(inputs, piece, grid, prev_inputs) + local new_inputs = {} + + for input, value in pairs(inputs) do + if value and not prev_inputs[input] then + new_inputs[input] = true + end + end + + local rot_dir = 0 + if (new_inputs["rotate_left"] or new_inputs["rotate_left2"]) then + rot_dir = 3 + elseif (new_inputs["rotate_right"] or new_inputs["rotate_right2"]) then + rot_dir = 1 + elseif (new_inputs["rotate_180"]) then + rot_dir = 2 + end + + while rot_dir ~= 0 do + if piece.filled then break end + + new_piece = piece:withRelativeRotation(rot_dir) + + if (grid:canPlacePiece(new_piece)) and piece.shape ~= "O" then + piece:setRelativeRotation(rot_dir) + piece.lock_delay = 0 -- rotate reset + else + -- set the piece to occupy the whole grid + piece.filled = true + unfilled_block_offsets = {} + for y = 4, 23 do + for x = 0, 9 do + if not grid:isOccupied(x, y) then + table.insert(unfilled_block_offsets, {x=x, y=y}) + end + end + end + piece.position = {x=0, y=0} + piece.getBlockOffsets = function(piece) + return unfilled_block_offsets + end + piece.isDropBlocked = function(piece) + return true + end + end + rot_dir = 0 + end + + -- prev_inputs becomes the previous inputs + for input, value in pairs(inputs) do + prev_inputs[input] = inputs[input] + end +end + +local function movePiece(piece, grid, move) + if move == "left" then + if not piece:isMoveBlocked(grid, {x=-1, y=0}) then + piece.lock_delay = 0 -- move reset + end + piece:moveInGrid({x=-1, y=0}, 1, grid) + elseif move == "right" then + if not piece:isMoveBlocked(grid, {x=1, y=0}) then + piece.lock_delay = 0 -- move reset + end + piece:moveInGrid({x=1, y=0}, 1, grid) + end +end + +local function dropPiece(inputs, piece, grid, gravity, drop_speed, drop_locked) + local y = piece.position.y + if inputs["down"] == true and drop_locked == false then + piece:addGravity(gravity + 1, grid):lockIfBottomed(grid) + elseif inputs["up"] == true then + if piece:isDropBlocked(grid) then + return + end + piece:dropToBottom(grid) + else + piece:addGravity(gravity, grid) + end + if piece.position.y ~= y then -- step reset + piece.lock_delay = 0 + end +end + +local function lockPiece(piece, grid, lock_delay) + if piece:isDropBlocked(grid) and piece.lock_delay >= lock_delay then + piece.locked = true + end +end + +function BONKERS.initializePiece(inputs, data, grid, gravity, prev_inputs, move, lock_delay, drop_speed, drop_locked) + local piece = Piece(shape, 0, { + x = BONKERS.spawn_positions[shape].x, + y = BONKERS.spawn_positions[shape].y + }, BONKERS.block_offsets, 0, 0) + -- have to copy that object otherwise it gets referenced + rotatePiece(inputs, piece, grid, {}) + dropPiece(inputs, piece, grid, gravity, drop_speed, drop_locked) + return piece +end + +function BONKERS.processPiece(inputs, piece, grid, gravity, prev_inputs, move, lock_delay, drop_speed, drop_locked) + rotatePiece(inputs, piece, grid, prev_inputs) + movePiece(piece, grid, move) + dropPiece(inputs, piece, grid, gravity, drop_speed, drop_locked) + lockPiece(piece, grid, lock_delay) +end + +return BONKERS