Compare commits

...

31 Commits

Author SHA1 Message Date
0a1ae69f53 Merge branch 'refs/heads/develop-tetris' into develop
# Conflicts:
#	src/main/java/eu/mhsl/minenet/minigames/instance/game/GameList.java
#	src/main/resources/lang/locales.map.csv
2024-12-16 21:02:50 +01:00
378d872283 changed computeIfAbsent back to if statement 2024-11-24 00:47:15 +01:00
bc27c35f1a removed debug outputs 2024-11-23 23:58:54 +01:00
18689ac0df fixed score 2024-11-23 23:56:18 +01:00
3fe57d5fe9 continued fixing pr comments 2024-11-23 22:24:08 +01:00
0699206c21 started fixing pr comments 2024-11-23 14:26:19 +01:00
e663f3f105 made tetris spectators invisible, added barrier walls to elytra race 2024-11-04 21:53:54 +01:00
55be88b7da removed countdown when no player left 2024-11-04 15:04:09 +01:00
f85ebbbb5d made BoolOption show true or false, shortened tetris Timeout after first death 2024-10-24 23:37:41 +02:00
720d3c8d65 changed Player spawn position 2024-10-24 23:10:09 +02:00
e397d69d7a added score above playfield 2024-10-23 22:43:14 +02:00
bef697e5fc added Tetris options to translation files 2024-10-23 21:15:47 +02:00
6638a48677 added combos 2024-10-23 17:41:05 +02:00
73a374e529 added timeout for tetris and display points at end of PointsWinScore 2024-10-23 17:26:31 +02:00
f838317af0 centered hold and next tetrominoes 2024-10-23 14:53:07 +02:00
3c25f5f079 changed player spawn position 2024-10-23 13:55:39 +02:00
179fa2e4d7 added combat system and options for game list 2024-10-23 01:08:52 +02:00
075db7a91b added border around next and hold tetromino 2024-10-22 21:21:00 +02:00
c9d00f4f77 added tetris to translation file 2024-10-22 17:16:26 +02:00
8594f8029c fixed score at end of tetris not working 2024-10-22 17:05:24 +02:00
e192ae4433 moved tetris from prototype to other 2024-10-22 16:45:32 +02:00
1b448e749e fixed leaderboard with new PointsWinScore class 2024-10-22 16:34:23 +02:00
2799a40c58 made multiplayer possible 2024-10-22 15:51:00 +02:00
710838645f added ghost tetromino 2024-10-22 00:36:57 +02:00
f56023004a removed unused import 2024-10-21 19:22:51 +02:00
55a98b6464 changed background, added hold, show score 2024-10-21 19:22:31 +02:00
236d372746 fixed collision detection, added rotation 2024-10-21 15:55:54 +02:00
1332a42bf6 added tetromino controlls 2024-10-21 00:27:58 +02:00
5acfe8f6af added basic tetris classes 2024-10-20 18:24:53 +02:00
9d287f7c2f added control input 2024-10-19 22:57:05 +02:00
7ea39f9aad started tetris 2024-10-19 16:38:43 +02:00
17 changed files with 1126 additions and 27 deletions

View File

@ -11,6 +11,7 @@ import eu.mhsl.minenet.minigames.instance.Spawnable;
import eu.mhsl.minenet.minigames.instance.room.Room;
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.Player;
import net.minestom.server.event.item.ItemDropEvent;
import net.minestom.server.event.player.PlayerBlockBreakEvent;
@ -98,7 +99,11 @@ public abstract class Game extends MineNetInstance implements Spawnable {
public void unload() {
this.onUnload();
getPlayers().forEach(Room::setOwnRoom);
getPlayers().forEach(player -> {
Room.setOwnRoom(player);
player.setGameMode(GameMode.SURVIVAL);
player.setInvisible(false);
});
scheduler().scheduleTask(() -> {

View File

@ -11,6 +11,7 @@ import eu.mhsl.minenet.minigames.instance.game.stateless.types.deathcube.Deathcu
import eu.mhsl.minenet.minigames.instance.game.stateless.types.minerun.MinerunFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.spleef.SpleefFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.stickfight.StickFightFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.TetrisFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.tntrun.TntRunFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.TowerdefenseFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.trafficlightrace.TrafficLightRaceFactory;
@ -23,9 +24,10 @@ public enum GameList {
TOWERDEFENSE(new TowerdefenseFactory(), GameType.PROTOTYPE),
BEDWARS(new BedwarsFactory(), GameType.PROTOTYPE),
BACKROOMS(new BackroomsFactory(), GameType.PROTOTYPE),
ANVILRUN(new AnvilRunFactory(), GameType.PROTOTYPE),
TETRIS(new TetrisFactory(), GameType.OTHER),
TNTRUN(new TntRunFactory(), GameType.OTHER),
ACIDRAIN(new AcidRainFactory(), GameType.PVE),
ANVILRUN(new AnvilRunFactory(), GameType.PVE),
ELYTRARACE(new ElytraRaceFactory(), GameType.PVP),
SPLEEF(new SpleefFactory(), GameType.PVP),
BOWSPLEEF(new BowSpleefFactory(), GameType.PVP);

View File

@ -50,8 +50,8 @@ public class StatelessGame extends Game {
int timeLeft = timeLimit - timePlayed;
switch (timeLeft) {
case 60, 30, 10, 5, 4, 3, 2, 1 ->
new ChatMessage(Icon.SCIENCE).appendStatic("Noch " + timeLeft + " Sekunden!").send(getPlayers());
case 90, 60, 30, 10, 5, 4, 3, 2, 1 ->
new ChatMessage(Icon.SCIENCE).appendStatic("Noch " + timeLeft + " Sekunden!").send(getPlayers());
}
timePlayed++;

View File

@ -9,6 +9,7 @@ import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import java.util.List;
import java.util.Objects;
public abstract class Option<T> {
private RestrictionHandler restrictionHandler;
@ -25,7 +26,7 @@ public abstract class Option<T> {
this.name = name;
this.options = options;
currentValue = options.get(0);
currentValue = options.getFirst();
}
public void setRestrictionHandler(RestrictionHandler restrictionHandler) {
@ -44,11 +45,11 @@ public abstract class Option<T> {
}
public ItemStack getCurrent(Player p) {
int amount = Integer.parseInt(options.get(pointer).toString());
String value = options.get(pointer).toString();
return ItemStack.builder(item)
.customName(name.getAssembled(p))
.lore(TranslatedComponent.byId("optionCommon#value").setColor(NamedTextColor.GOLD).getAssembled(p)
.append(Component.text(": ")).append(Component.text(amount)))
.append(Component.text(": ")).append(Component.text(value)))
.build();
}
@ -56,6 +57,10 @@ public abstract class Option<T> {
return Integer.parseInt(getAsString());
}
public boolean getAsBoolean() {
return Objects.equals(getAsString(), "true") || Objects.equals(getAsString(), "1");
}
public String getAsString() {
return currentValue.toString();
}

View File

@ -6,8 +6,8 @@ import net.minestom.server.item.Material;
import java.util.List;
public class BoolOption extends Option<Boolean> {
public class BoolOption extends Option<String> {
public BoolOption(String id, Material item, TranslatedComponent name) {
super(id, item, name, List.of(true, false));
super(id, item, name, List.of("true", "false"));
}
}

View File

@ -40,6 +40,7 @@ public class ElytraRace extends StatelessGame {
private final ValeGenerator vale = new ValeGenerator();
private final int gameHeight = 0;
private final int seaLevel = -55;
private final int ringSpacing = 100;
private final int ringCount;
@ -62,7 +63,7 @@ public class ElytraRace extends StatelessGame {
this.ringCount = ringCount;
setGenerator(vale);
vale.setCalculateSeaLevel(point -> -55);
vale.setCalculateSeaLevel(point -> seaLevel);
vale.setXShiftMultiplier(integer -> NumberUtil.map(integer, 50, 500, 0, 1));
vale.addMixIn(new PlaneTerrainGenerator(gameHeight, Block.BARRIER));
@ -106,9 +107,7 @@ public class ElytraRace extends StatelessGame {
@Override
protected void onLoad(@NotNull CompletableFuture<Void> callback) {
Point spawnpoint = new Pos(vale.getXShiftAtZ(0), -46, 0);
GeneratorUtils.iterateArea(spawnpoint.sub(5, 0, 5), spawnpoint.add(5, 0, 5), point -> {
setBlock(point, BlockPallet.STREET.rnd());
});
GeneratorUtils.iterateArea(spawnpoint.sub(5, 0, 5), spawnpoint.add(5, 0, 5), point -> setBlock(point, BlockPallet.STREET.rnd()));
generateRing(ringSpacing);
generateRing(ringSpacing * 2);
@ -195,8 +194,8 @@ public class ElytraRace extends StatelessGame {
Point ringPos = getRingPositionAtZ(zPos);
GeneratorUtils.iterateArea(
ringPos.sub(100, 0, 0).withY(0), // TODO 0 was before update getDimensionType().getMinY, might not work correctly
ringPos.add(100, 0, 0).withY(gameHeight),
ringPos.sub(100, 0, 0).withY(0),
ringPos.add(100, 0, 0).withY(seaLevel),
point -> batch.setBlock(point, Block.BARRIER)
);
GeneratorUtils.iterateArea(

View File

@ -0,0 +1,185 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris;
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game.TetrisGame;
import eu.mhsl.minenet.minigames.instance.Dimension;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game.Tetromino;
import eu.mhsl.minenet.minigames.score.PointsWinScore;
import net.kyori.adventure.text.Component;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.Player;
import net.minestom.server.event.player.*;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import org.jetbrains.annotations.NotNull;
import java.util.*;
class Tetris extends StatelessGame {
private final Map<Player, TetrisGame> tetrisGames = new WeakHashMap<>();
private final int nextTetrominoesCount;
private final boolean isFast;
private final boolean hasCombat;
private boolean setTimeLimit = false;
private final long randomSeed;
public Tetris(int nextTetrominoesCount, boolean isFast, boolean hasCombat) {
super(Dimension.THE_END.key, "Tetris", new PointsWinScore());
eventNode()
.addListener(PlayerUseItemEvent.class, this::onPlayerInteract)
.addListener(PlayerHandAnimationEvent.class, this::onPlayerAttack)
.addListener(PlayerTickEvent.class, this::onPlayerTick);
this.nextTetrominoesCount = nextTetrominoesCount;
this.isFast = isFast;
this.hasCombat = hasCombat;
Random random = new Random();
this.randomSeed = random.nextLong();
}
@Override
protected void onStart() {
this.getEntities().stream()
.filter(entity -> entity.getEntityType().equals(Tetromino.getGhostEntityType()))
.forEach(Entity::remove);
if(this.hasCombat) {
this.tetrisGames.values().forEach(tetrisGame -> tetrisGame.updateOtherTetrisGames(this.tetrisGames.values()));
}
this.tetrisGames.forEach((player, tetrisGame) -> tetrisGame.start());
}
@Override
protected void onStop() {
this.tetrisGames.forEach((player, tetrisGame) -> {
tetrisGame.loose();
getScore().insertResult(player, tetrisGame.getScore());
tetrisGame.sidebar.removeViewer(player);
});
}
@Override
protected void onPlayerLeave(Player p) {
this.tetrisGames.get(p).sidebar.removeViewer(p);
this.letPlayerLoose(p);
}
@Override
protected void onPlayerMove(@NotNull PlayerMoveEvent event) {
Player player = event.getPlayer();
Pos previousPosition = event.getPlayer().getPosition();
Pos currentPosition = event.getNewPosition();
TetrisGame tetrisGame = this.tetrisGames.get(player);
if(tetrisGame == null) {
event.setCancelled(true);
return;
}
if(tetrisGame.lost) return;
if(player.getGameMode() == GameMode.SPECTATOR) return;
event.setNewPosition(tetrisGame.getPlayerSpawnPosition().withView(currentPosition));
player.setSprinting(false);
Vec movementVector = currentPosition.asVec().sub(previousPosition.asVec());
float yaw = player.getPosition().yaw();
double yawRadians = Math.toRadians(yaw);
double forwardX = -Math.sin(yawRadians);
double forwardZ = Math.cos(yawRadians);
Vec forward = new Vec(forwardX, 0, forwardZ).normalize();
Vec left = forward.cross(new Vec(0, 1, 0)).normalize();
double forwardAmount = movementVector.dot(forward);
double leftAmount = movementVector.dot(left);
double buttonPressAmount = 0.018;
if (forwardAmount > buttonPressAmount) {
tetrisGame.pressedButton(TetrisGame.Button.W);
} else if (forwardAmount < -buttonPressAmount) {
tetrisGame.pressedButton(TetrisGame.Button.S);
}
if (leftAmount > buttonPressAmount) {
tetrisGame.pressedButton(TetrisGame.Button.D);
} else if (leftAmount < -buttonPressAmount) {
tetrisGame.pressedButton(TetrisGame.Button.A);
}
if(previousPosition.y() < currentPosition.y()) tetrisGame.pressedButton(TetrisGame.Button.space);
}
protected void onPlayerInteract(@NotNull PlayerUseItemEvent event) {
this.tetrisGames.get(event.getPlayer()).pressedButton(TetrisGame.Button.mouseRight);
}
protected void onPlayerAttack(@NotNull PlayerHandAnimationEvent event) {
this.tetrisGames.get(event.getPlayer()).pressedButton(TetrisGame.Button.mouseLeft);
}
protected void onPlayerTick(PlayerTickEvent event) {
Player player = event.getPlayer();
TetrisGame tetrisGame = this.tetrisGames.get(player);
if(tetrisGame == null) return;
if(tetrisGame.lost && player.getGameMode() != GameMode.SPECTATOR) {
letPlayerLoose(player);
}
}
private void letPlayerLoose(Player player) {
TetrisGame tetrisGame = this.tetrisGames.get(player);
player.setGameMode(GameMode.SPECTATOR);
player.setInvisible(true);
getScore().insertResult(player, tetrisGame.getScore());
boolean allGamesLost = this.tetrisGames.values().stream()
.filter(game -> !game.lost)
.toList()
.isEmpty();
if(!setTimeLimit && !allGamesLost) {
this.setTimeLimit(90);
setTimeLimit = true;
}
}
@Override
protected boolean onPlayerJoin(Player p) {
p.getInventory().setItemStack(0, ItemStack.builder(Material.BIRCH_BUTTON).customName(Component.text("Controller")).build());
p.setSprinting(false);
if(this.tetrisGames.get(p) == null) {
this.tetrisGames.put(p, new TetrisGame(
this,
getSpawn().sub(6, 8, 15).add(this.tetrisGames.size()*30, 0, 0),
Tetromino.Shape.J,
this.nextTetrominoesCount,
this.isFast,
this.hasCombat,
this.randomSeed
));
this.tetrisGames.get(p).generate();
this.tetrisGames.values().forEach(tetrisGame -> tetrisGame.updateOtherTetrisGames(this.tetrisGames.values()));
}
TetrisGame tetrisGame = this.tetrisGames.get(p);
p.teleport(tetrisGame.getPlayerSpawnPosition());
tetrisGame.sidebar.addViewer(p);
return super.onPlayerJoin(p);
}
@Override
public Pos getSpawn() {
return new Pos(0, 30, 15).withView(180, 0);
}
}

View File

@ -0,0 +1,43 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris;
import eu.mhsl.minenet.minigames.instance.game.Game;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.BoolOption;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.NumericOption;
import eu.mhsl.minenet.minigames.instance.room.Room;
import eu.mhsl.minenet.minigames.message.component.TranslatedComponent;
import net.minestom.server.item.Material;
import java.util.Map;
public class TetrisFactory implements GameFactory {
@Override
public TranslatedComponent name() {
return TranslatedComponent.byId("game_Tetris#name");
}
@Override
public TranslatedComponent description() {
return TranslatedComponent.byId("game_Tetris#description");
}
@Override
public ConfigManager configuration() {
return new ConfigManager()
.addOption(new NumericOption("nextTetrominoesCount", Material.LADDER, TranslatedComponent.byId("game_Tetris#nextTetrominoesCount"), 3, 4, 5, 1, 2))
.addOption(new BoolOption("isFast", Material.MINECART, TranslatedComponent.byId("game_Tetris#isFast")))
.addOption(new BoolOption("hasCombat", Material.DIAMOND_SWORD, TranslatedComponent.byId("game_Tetris#hasCombat")));
}
@Override
public Game manufacture(Room parent, Map<String, Option<?>> configuration) {
return new Tetris(configuration.get("nextTetrominoesCount").getAsInt(), configuration.get("isFast").getAsBoolean(), configuration.get("hasCombat").getAsBoolean()).setParent(parent);
}
@Override
public Material symbol() {
return Material.PURPLE_WOOL;
}
}

View File

@ -0,0 +1,191 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game;
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
import eu.mhsl.minenet.minigames.util.BatchUtil;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.instance.batch.AbsoluteBlockBatch;
import net.minestom.server.instance.block.Block;
import org.apache.commons.lang3.ArrayUtils;
import java.util.Random;
public class Playfield {
private final Pos lowerLeftCorner;
private final StatelessGame instance;
private final static int height = 22;
private final static Block scoreBlock = Block.STONE;
private final int nextTetrominoesCount;
private final Random random;
public Playfield(Pos lowerLeftCorner, StatelessGame instance, int nextTetrominoesCount) {
this.nextTetrominoesCount = nextTetrominoesCount;
this.lowerLeftCorner = lowerLeftCorner;
this.instance = instance;
this.random = new Random();
}
public Pos getPlayerSpawnPosition() {
return this.lowerLeftCorner.add(6, 9, 20).withView(180, 0);
}
public Pos getTetrominoSpawnPosition() {
return this.lowerLeftCorner.add(5, 21, 1);
}
public Pos getHoldPosition() {
return this.lowerLeftCorner.add(-4, 18, 1);
}
public Pos getNextPosition() {
return this.lowerLeftCorner.add(15, 18, 1);
}
public Pos getScorePosition() {
return this.lowerLeftCorner.add(-5, height+3, 0);
}
public void generate() {
AbsoluteBlockBatch batch = new AbsoluteBlockBatch();
// actual playfield:
for(int x=0; x<12; x++) {
for(int y = 0; y< height; y++) {
batch.setBlock(this.lowerLeftCorner.add(x, y, 0), Block.GLASS);
batch.setBlock(this.lowerLeftCorner.add(x, y, -1), Block.BLACK_CONCRETE);
if(x==0 || x==11 || y==0) {
batch.setBlock(this.lowerLeftCorner.add(x, y, 1), Block.GRAY_CONCRETE);
batch.setBlock(this.lowerLeftCorner.add(x, y, 0), Block.GRAY_CONCRETE);
}
}
}
// hold position:
for(int x = 0; x < 4; x++) {
for(int y = 0; y < 4; y++) {
batch.setBlock(this.getHoldPosition().add(x-1, y-1, -1), Block.QUARTZ_BLOCK);
}
}
// next positions:
for(int x = 0; x < 4; x++) {
for(int y = -4*this.nextTetrominoesCount+4; y < 4; y++) {
batch.setBlock(this.getNextPosition().add(x-1, y-1, -1), Block.QUARTZ_BLOCK);
}
}
batch.setBlock(getPlayerSpawnPosition().sub(0, 1, 0), Block.STONE);
batch.setBlock(getPlayerSpawnPosition().sub(1, 1, 0), Block.STONE);
batch.setBlock(getPlayerSpawnPosition().sub(1, 1, 1), Block.STONE);
batch.setBlock(getPlayerSpawnPosition().sub(0, 1, 1), Block.STONE);
BatchUtil.loadAndApplyBatch(batch, this.instance, () -> {});
}
public int removeFullLines() {
int removedLinesCounter = 0;
for(int y = 1; y< height; y++) {
boolean isFullLine = true;
for(int x=1; x<11; x++) {
if(this.instance.getBlock(this.lowerLeftCorner.add(x, y, 1)) == Block.AIR) isFullLine = false;
}
if(isFullLine) {
removeFullLine(y);
removedLinesCounter += 1;
y -= 1;
}
}
return removedLinesCounter;
}
public void addLines(int lines) {
int xPosMissing = random.nextInt(1, 10);
for (int i = 0; i < lines; i++) {
moveAllLinesUp();
for (int x = 1; x < 11; x++) {
if(x != xPosMissing) {
this.instance.setBlock(this.lowerLeftCorner.add(x, 1, 1), Block.LIGHT_GRAY_CONCRETE);
} else {
this.instance.setBlock(this.lowerLeftCorner.add(x, 1, 1), Block.AIR);
}
}
}
}
public void updateAttackingLines(int attackingLines) {
for (int y = 0; y < height + 5; y++) {
if(attackingLines > 0) {
this.instance.setBlock(this.lowerLeftCorner.add(12, y, 1), Block.REDSTONE_BLOCK);
attackingLines -= 1;
} else {
this.instance.setBlock(this.lowerLeftCorner.add(12, y, 1), Block.AIR);
}
}
}
public void updateScore(int score) {
this.removeDigits();
String scoreString = String.valueOf(score);
char[] characters = scoreString.toCharArray();
ArrayUtils.reverse(characters);
for(int i = 6; i > 0; i--) {
char digit;
if(i <= characters.length) {
digit = characters[i-1];
} else {
digit = '0';
}
this.displayDigit(digit, 6-i);
}
}
private void displayDigit(char digit, int positionFromLeft) {
int[][] digitArray;
switch (digit) {
case '1' -> digitArray = new int[][]{{0,0,1},{0,1,1},{0,0,1},{0,0,1},{0,0,1}};
case '2' -> digitArray = new int[][]{{1,1,1},{0,0,1},{1,1,1},{1,0,0},{1,1,1}};
case '3' -> digitArray = new int[][]{{1,1,1},{0,0,1},{0,1,1},{0,0,1},{1,1,1}};
case '4' -> digitArray = new int[][]{{1,0,1},{1,0,1},{1,1,1},{0,0,1},{0,0,1}};
case '5' -> digitArray = new int[][]{{1,1,1},{1,0,0},{1,1,1},{0,0,1},{1,1,1}};
case '6' -> digitArray = new int[][]{{1,1,1},{1,0,0},{1,1,1},{1,0,1},{1,1,1}};
case '7' -> digitArray = new int[][]{{1,1,1},{0,0,1},{0,1,0},{0,1,0},{0,1,0}};
case '8' -> digitArray = new int[][]{{1,1,1},{1,0,1},{1,1,1},{1,0,1},{1,1,1}};
case '9' -> digitArray = new int[][]{{1,1,1},{1,0,1},{1,1,1},{0,0,1},{1,1,1}};
default -> digitArray = new int[][]{{1,1,1},{1,0,1},{1,0,1},{1,0,1},{1,1,1}};
}
for (int x = 0; x < 3; x++) {
for (int y = 0; y < 5; y++) {
if(digitArray[4-y][x] == 1) this.instance.setBlock(this.getScorePosition().add(positionFromLeft*4+x, y, 0), scoreBlock);
}
}
}
private void removeDigits() {
for (int x = 0; x < 4 * 6; x++) {
for (int y = 0; y < 5; y++) {
this.instance.setBlock(this.getScorePosition().add(x, y, 0), Block.AIR);
}
}
}
private void moveAllLinesUp() {
for (int y = height + 3; y > 1; y--) {
for (int x = 1; x < 11; x++) {
Block blockBeneath = this.instance.getBlock(this.lowerLeftCorner.add(x, y - 1, 1));
this.instance.setBlock(this.lowerLeftCorner.add(x, y, 1), blockBeneath);
}
}
}
private void removeFullLine(int positionY) {
for(int y = positionY; y< height; y++) {
for(int x=1; x<11; x++) {
Block blockAbove = this.instance.getBlock(this.lowerLeftCorner.add(x, y+1, 1));
this.instance.setBlock(this.lowerLeftCorner.add(x, y, 1), blockAbove);
}
}
}
}

View File

@ -0,0 +1,318 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game;
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
import net.kyori.adventure.text.Component;
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.scoreboard.Sidebar;
import net.minestom.server.timer.Scheduler;
import net.minestom.server.timer.TaskSchedule;
import java.util.*;
public class TetrisGame {
private final StatelessGame instance;
private final Playfield playfield;
private final boolean isFast;
private int level = 1;
private int lines = 0;
private int score = 0;
private int combo = 0;
private int attackingLines = 0;
public boolean lost = false;
public boolean paused = true;
private final boolean hasCombat;
public Tetromino currentTetromino;
private final List<Tetromino> nextTetrominoes = new ArrayList<>();
private Tetromino holdTetromino;
private final List<Tetromino> tetrominoBag = new ArrayList<>();
private boolean holdPossible = true;
private final Pos nextPosition;
private final Pos holdPosition;
private final Pos tetrominoSpawnPosition;
public Sidebar sidebar = new Sidebar(Component.text("Info:"));
private final Map<Button, Long> lastPresses = new HashMap<>();
private final List<TetrisGame> otherTetrisGames = new ArrayList<>();
private final Random random;
public enum Button {
W,
A,
S,
D,
mouseLeft,
mouseRight,
space
}
public TetrisGame(StatelessGame instance, Pos lowerLeftCorner, Tetromino.Shape startTetrominoShape, int nextTetrominoesCount, boolean isfast, boolean hasCombat, long randomSeed) {
this.isFast = isfast;
this.hasCombat = hasCombat;
this.instance = instance;
this.playfield = new Playfield(lowerLeftCorner, this.instance, nextTetrominoesCount);
this.random = new Random(randomSeed);
this.holdPosition = this.playfield.getHoldPosition();
this.nextPosition = this.playfield.getNextPosition();
this.tetrominoSpawnPosition = this.playfield.getTetrominoSpawnPosition();
this.buildSidebar();
this.currentTetromino = new Tetromino(this.instance, startTetrominoShape);
for (int i = 0; i < nextTetrominoesCount; i++) {
this.updateNextTetrominoes();
}
}
public void pressedButton(Button button) {
final int standardButtonDelay = 100;
final int buttonDebounce = 70;
if(this.lastPresses.getOrDefault(button, 0L) >= System.currentTimeMillis()-standardButtonDelay) return;
this.lastPresses.put(button, System.currentTimeMillis());
if(button == Button.W) this.lastPresses.put(button, System.currentTimeMillis()+buttonDebounce);
if(button == Button.S) this.lastPresses.put(button, System.currentTimeMillis()-buttonDebounce);
if(this.lost || this.paused) return;
switch (button) {
case A -> this.currentTetromino.moveLeft();
case S -> this.moveDown();
case D -> this.currentTetromino.moveRight();
case W -> this.hardDrop();
case mouseLeft -> this.currentTetromino.rotate(false);
case mouseRight -> this.currentTetromino.rotate(true);
case space -> this.switchHold();
}
}
public Pos getPlayerSpawnPosition() {
return this.playfield.getPlayerSpawnPosition();
}
public void start() {
this.paused = false;
Scheduler scheduler = MinecraftServer.getSchedulerManager();
scheduler.submitTask(() -> {
if(this.lost) return TaskSchedule.stop();
int standardTickDelay = 40;
if(this.isFast) standardTickDelay = 20;
TaskSchedule nextTick = TaskSchedule.tick(Math.round((float) standardTickDelay /this.level));
if(this.paused) return nextTick;
this.tick();
return nextTick;
});
this.updateInfo();
this.nextTetrominoes.forEach(tetromino -> {
double xChange = -tetromino.getXChange();
tetromino.setPosition(this.nextPosition.sub(xChange, 4*this.nextTetrominoes.indexOf(tetromino), 0));
tetromino.drawAsEntities();
});
}
public void generate() {
this.playfield.generate();
this.currentTetromino.setPosition(this.tetrominoSpawnPosition);
this.currentTetromino.draw(false);
}
public void tick() {
if(this.lost || this.paused) return;
if(!currentTetromino.moveDown()) {
setActiveTetrominoDown();
}
}
public int getScore() {
return this.score;
}
public void updateOtherTetrisGames(Collection<TetrisGame> tetrisGames) {
List<TetrisGame> games = new ArrayList<>(tetrisGames);
games.remove(this);
games.removeIf(tetrisGame -> tetrisGame.lost);
this.otherTetrisGames.clear();
this.otherTetrisGames.addAll(games);
}
public void getAttacked(int lines) {
if(this.hasCombat && !this.lost) {
this.attackingLines += lines;
this.playfield.updateAttackingLines(this.attackingLines);
}
}
public void loose() {
this.lost = true;
}
private boolean moveDown() {
if(!this.currentTetromino.moveDown()) {
this.setActiveTetrominoDown();
return false;
}
this.score += 1;
this.updateInfo();
return true;
}
private boolean hardDrop() {
if(!this.currentTetromino.moveDown()) {
this.setActiveTetrominoDown();
return false;
}
this.score += 2;
this.updateInfo();
while(this.currentTetromino.moveDown()) {
this.score += 2;
this.updateInfo();
}
this.setActiveTetrominoDown();
return true;
}
private boolean switchHold() {
if(!holdPossible) return false;
Tetromino newCurrentTetromino;
if(this.holdTetromino == null) {
newCurrentTetromino = this.nextTetrominoes.removeFirst();
newCurrentTetromino.remove();
this.updateNextTetrominoes();
} else {
newCurrentTetromino = this.holdTetromino;
this.holdTetromino.remove();
}
this.currentTetromino.remove();
this.holdTetromino = new Tetromino(this.instance, this.currentTetromino.getShape());
this.currentTetromino = newCurrentTetromino;
this.currentTetromino.setPosition(this.tetrominoSpawnPosition);
this.currentTetromino.draw();
if(!this.currentTetromino.moveDown()) loose();
double xChange = this.holdTetromino.getXChange();
this.holdTetromino.setPosition(this.holdPosition.add(xChange, 0, 0));
this.holdTetromino.drawAsEntities();
this.holdPossible = false;
return true;
}
private void updateNextTetrominoes() {
if(this.tetrominoBag.isEmpty()) {
for(Tetromino.Shape shape : Tetromino.Shape.values()) {
this.tetrominoBag.add(new Tetromino(this.instance, shape));
}
Collections.shuffle(this.tetrominoBag, this.random);
}
if(!this.nextTetrominoes.isEmpty()) this.nextTetrominoes.forEach(Tetromino::remove);
Tetromino newTetromino = this.tetrominoBag.removeFirst();
this.nextTetrominoes.add(newTetromino);
this.nextTetrominoes.forEach(tetromino -> {
double xChange = -tetromino.getXChange();
tetromino.setPosition(this.nextPosition.sub(xChange, 4*this.nextTetrominoes.indexOf(tetromino), 0));
tetromino.drawAsEntities();
});
}
private void buildSidebar() {
this.sidebar.createLine(new Sidebar.ScoreboardLine(
"0",
Component.text("Score: "),
0
));
this.sidebar.createLine(new Sidebar.ScoreboardLine(
"1",
Component.text("Lines: "),
0
));
this.sidebar.createLine(new Sidebar.ScoreboardLine(
"2",
Component.text("Level: "),
1
));
}
private void updateInfo() {
this.playfield.updateScore(this.score);
this.sidebar.updateLineScore("0", this.score);
this.sidebar.updateLineScore("1", this.lines);
this.sidebar.updateLineScore("2", this.level);
}
private void setActiveTetrominoDown() {
this.currentTetromino.removeOwnEntities();
this.currentTetromino = this.nextTetrominoes.removeFirst();
this.currentTetromino.remove();
this.updateNextTetrominoes();
int removedLines = this.playfield.removeFullLines();
int combatLines = 0;
this.combo += 1;
switch (removedLines) {
case 0 -> this.combo = 0;
case 1 -> {
this.lines += 1;
this.score += 40 * this.level;
}
case 2 -> {
combatLines = 1;
this.lines += 2;
this.score += 100 * this.level;
}
case 3 -> {
combatLines = 2;
this.lines += 3;
this.score += 300 * this.level;
}
case 4 -> {
combatLines = 4;
this.lines += 4;
this.score += 1200 * this.level;
}
}
this.score += 50 * this.combo * this.level;
if(this.combo >= 2) {
combatLines += (int) Math.floor((double) this.combo /2);
}
if(this.hasCombat && this.attackingLines > 0) {
if(combatLines > 0 && this.attackingLines >= combatLines) {
this.attackingLines -= combatLines;
combatLines = 0;
} else if(combatLines > 0) {
combatLines -= this.attackingLines;
this.attackingLines = 0;
} else {
this.playfield.addLines(this.attackingLines);
this.attackingLines = 0;
}
this.playfield.updateAttackingLines(this.attackingLines);
}
if(this.hasCombat && !this.otherTetrisGames.isEmpty()) {
Collections.shuffle(this.otherTetrisGames);
this.otherTetrisGames.getFirst().getAttacked(combatLines);
}
this.level = (int) Math.floor((double) this.lines / 10) + 1;
this.holdPossible = true;
this.updateInfo();
this.currentTetromino.setPosition(this.tetrominoSpawnPosition);
this.currentTetromino.draw();
if(!this.currentTetromino.moveDown()) {
loose();
}
}
}

View File

@ -0,0 +1,246 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game;
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.metadata.other.FallingBlockMeta;
import net.minestom.server.instance.block.Block;
import net.minestom.server.tag.Tag;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
public class Tetromino {
private final Shape shape;
private final StatelessGame instance;
private Pos position;
private int[][] shapeArray;
private final static EntityType ghostEntityType = EntityType.FALLING_BLOCK;
private final UUID uuid;
private final static Tag<String> uuidTag = Tag.String("uuid");
public enum Shape {
I,
J,
L,
O,
S,
T,
Z
}
public Tetromino(StatelessGame instance, Shape shape) {
this.instance = instance;
this.shape = shape;
this.uuid = UUID.randomUUID();
switch (this.shape) {
case I -> shapeArray = new int[][]{{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}};
case J -> shapeArray = new int[][]{{1,0,0}, {1,1,1}, {0,0,0}};
case L -> shapeArray = new int[][]{{0,0,1}, {1,1,1}, {0,0,0}};
case O -> shapeArray = new int[][]{{1,1}, {1,1}};
case S -> shapeArray = new int[][]{{0,1,1}, {1,1,0}, {0,0,0}};
case T -> shapeArray = new int[][]{{0,1,0}, {1,1,1}, {0,0,0}};
case Z -> shapeArray = new int[][]{{1,1,0}, {0,1,1}, {0,0,0}};
}
}
public static EntityType getGhostEntityType() {
return ghostEntityType;
}
public void setPosition(Pos newPosition) {
this.position = newPosition;
}
public boolean rotate(boolean clockwise) {
int[][] newShapeArray = this.getTurnedShapeArray(clockwise);
return checkCollisionAndMove(this.position, newShapeArray);
}
public boolean moveDown() {
Pos newPosition = this.position.sub(0, 1, 0);
return checkCollisionAndMove(newPosition, this.shapeArray);
}
public boolean moveLeft() {
Pos newPosition = this.position.sub(1, 0, 0);
return checkCollisionAndMove(newPosition, this.shapeArray);
}
public boolean moveRight() {
Pos newPosition = this.position.add(1, 0, 0);
return checkCollisionAndMove(newPosition, this.shapeArray);
}
public void draw() {
this.draw(true);
}
public void draw(boolean withGhost) {
if(withGhost) {
Pos ghostPos = this.position;
while (!checkCollision(ghostPos.sub(0, 1, 0), this.shapeArray)) {
ghostPos = ghostPos.sub(0, 1, 0);
}
Pos positionChange = this.position.sub(ghostPos);
getBlockPositions().forEach(pos -> {
Entity ghostBlock = new Entity(ghostEntityType);
((FallingBlockMeta) ghostBlock.getEntityMeta()).setBlock(this.getGhostBlock());
ghostBlock.setNoGravity(true);
ghostBlock.setGlowing(true);
ghostBlock.setTag(uuidTag, this.uuid.toString());
ghostBlock.setInstance(this.instance, pos.sub(positionChange).add(0.5, 0, 0.5));
});
}
getBlockPositions().forEach(pos -> this.instance.setBlock(pos, this.getColoredBlock()));
}
public void drawAsEntities() {
getBlockPositions().forEach(pos -> {
Entity ghostBlock = new Entity(ghostEntityType);
((FallingBlockMeta) ghostBlock.getEntityMeta()).setBlock(this.getColoredBlock());
ghostBlock.setNoGravity(true);
ghostBlock.setTag(uuidTag, this.uuid.toString());
ghostBlock.setInstance(this.instance, pos.add(0.5, 0, 0.5));
});
}
public void remove() {
this.removeOwnEntities();
this.getBlockPositions().forEach(pos -> this.instance.setBlock(pos, Block.AIR));
}
public Block getColoredBlock() {
Block returnBlock;
switch (this.shape) {
case I -> returnBlock = Block.LIGHT_BLUE_CONCRETE;
case J -> returnBlock = Block.BLUE_CONCRETE;
case L -> returnBlock = Block.ORANGE_CONCRETE;
case O -> returnBlock = Block.YELLOW_CONCRETE;
case S -> returnBlock = Block.GREEN_CONCRETE;
case T -> returnBlock = Block.PURPLE_CONCRETE;
case Z -> returnBlock = Block.RED_CONCRETE;
default -> returnBlock = Block.WHITE_CONCRETE;
}
return returnBlock;
}
public void removeOwnEntities() {
this.instance.getEntities().stream()
.filter(entity -> {
String tagValue = entity.getTag(uuidTag);
if(tagValue == null) return false;
return entity.getTag(uuidTag).equals(this.uuid.toString());
})
.forEach(Entity::remove);
}
public double getXChange() {
switch (this.shape) {
case O, I -> {
return 0;
}
case null, default -> {
return 0.5;
}
}
}
public Shape getShape() {
return this.shape;
}
private Block getGhostBlock() {
Block returnBlock;
switch (this.shape) {
case I -> returnBlock = Block.LIGHT_BLUE_STAINED_GLASS;
case J -> returnBlock = Block.BLUE_STAINED_GLASS;
case L -> returnBlock = Block.ORANGE_STAINED_GLASS;
case O -> returnBlock = Block.YELLOW_STAINED_GLASS;
case S -> returnBlock = Block.GREEN_STAINED_GLASS;
case T -> returnBlock = Block.PURPLE_STAINED_GLASS;
case Z -> returnBlock = Block.RED_STAINED_GLASS;
default -> returnBlock = Block.WHITE_STAINED_GLASS;
}
return returnBlock;
}
private int[][] getTurnedShapeArray(boolean clockwise) {
int iterations = 1;
if(!clockwise) iterations = 3;
int arrayLength = this.shapeArray.length;
int[][] startArray = Arrays.stream(this.shapeArray).map(int[]::clone).toArray(int[][]::new);
int[][] returnArray = new int[arrayLength][arrayLength];
for(int k=0; k<iterations; k++) {
for(int i=0; i<arrayLength; i++) {
for(int j=0; j<arrayLength; j++) {
returnArray[i][arrayLength-1-j] = startArray[j][i];
}
}
startArray = Arrays.stream(returnArray).map(int[]::clone).toArray(int[][]::new);
}
return returnArray;
}
private boolean isPartOfTetromino(Pos position) {
return this.getBlockPositions().stream()
.anyMatch(pos -> pos.equals(position));
}
private List<Pos> getBlockPositions() {
return this.getBlockPositions(this.position, this.shapeArray);
}
private List<Pos> getBlockPositions(Pos position, int[][] shapeArray) {
List<Pos> returnList = new ArrayList<>();
if(position == null) return returnList;
int arrayLength = shapeArray.length;
for(int x=0; x<arrayLength; x++) {
for(int y=0; y<arrayLength; y++) {
if(shapeArray[arrayLength-1-y][x] == 1) {
switch (this.shape) {
case I -> returnList.add(position.add(x-1, y-2, 0));
case O -> returnList.add(position.add(x, y, 0));
default -> returnList.add(position.add(x-1, y-1, 0));
}
}
}
}
return returnList;
}
private boolean checkCollision(Pos newPosition, int[][] newShapeArray) {
List<Pos> newBlockPositions = getBlockPositions(newPosition, newShapeArray);
for(Pos pos : newBlockPositions) {
if(isPartOfTetromino(pos)) continue;
if(this.instance.getBlock(pos) == this.getGhostBlock()) continue;
if(this.instance.getBlock(pos) != Block.AIR) return true;
}
return false;
}
private boolean checkCollisionAndMove(Pos newPosition, int[][] newShapeArray) {
if(!checkCollision(newPosition, newShapeArray)) {
this.remove();
this.shapeArray = Arrays.stream(newShapeArray).map(int[]::clone).toArray(int[][]::new);
this.setPosition(newPosition);
this.draw();
return true;
}
return false;
}
}

View File

@ -10,7 +10,7 @@ import java.util.Set;
public class LastWinsScore extends Score {
@Override
public void insertResultImplementation(Set<Player> p) {
getScores().add(0, p);
getScores().addFirst(p);
}
@Override

View File

@ -0,0 +1,65 @@
package eu.mhsl.minenet.minigames.score;
import eu.mhsl.minenet.minigames.message.Icon;
import eu.mhsl.minenet.minigames.message.TranslatableMessage;
import eu.mhsl.minenet.minigames.message.type.ChatMessage;
import eu.mhsl.minenet.minigames.message.type.TitleMessage;
import eu.mhsl.minenet.minigames.util.MapUtil;
import net.minestom.server.entity.Player;
import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;
public class PointsWinScore extends Score {
private Map<Set<Player>, Integer> scoreOrder = new HashMap<>();
@Override
protected void insertResultImplementation(Set<Player> p, int currentPoints) {
this.scoreOrder.put(p, currentPoints);
this.scoreOrder = MapUtil.sortByValue(this.scoreOrder);
getScores().clear();
this.scoreOrder.forEach((player, integer) -> getScores().addFirst(player));
}
@Override
protected void insertResultImplementation(Set<Player> p) {
this.insertResultImplementation(p, 0);
}
@Override
public void setDone() {
if(isClosed()) return;
close();
new ChatMessage(Icon.STAR, true)
.appendTranslated("score#result")
.newLine()
.indent()
.numberedList(
getScores()
.stream()
.filter(players -> !players.stream()
.filter(player -> !player.getUsername().isBlank())
.toList()
.isEmpty())
.map(players -> players
.stream()
.filter(player -> scoreOrder.get(Set.of(player)) != null)
.map(player -> player.getUsername()+" : "+scoreOrder.get(Set.of(player)).toString())
.collect(Collectors.joining(", "))
)
.toList()
)
.undent()
.newLine()
.appendTranslated("score#thanks")
.send(instance.getPlayers());
instance.stop();
}
@Override
protected TranslatableMessage scoreMessage() {
return new TitleMessage(Duration.ofMillis(1000), Duration.ofSeconds(1)).appendTranslated("score#death");
}
}

View File

@ -8,6 +8,7 @@ import net.minestom.server.entity.Player;
import net.minestom.server.event.instance.AddEntityToInstanceEvent;
import net.minestom.server.event.instance.RemoveEntityFromInstanceEvent;
import net.minestom.server.event.player.PlayerDisconnectEvent;
import org.apache.commons.lang3.NotImplementedException;
import java.util.ArrayList;
import java.util.List;
@ -22,10 +23,19 @@ public abstract class Score {
public Score() {}
public Score(int ignoreLastPlayers) {
this.ignoreLastPlayers = ignoreLastPlayers;
protected abstract void insertResultImplementation(Set<Player> p);
protected void insertResultImplementation(Set<Player> p, int points) {
throw new NotImplementedException("This Score type is not able to process points");
}
public void insertResult(Player p) {
this.insertResultProcessor(p, () -> this.insertResultImplementation(Set.of(p)));
}
public void insertResult(Player p, int points) {
this.insertResultProcessor(p, () -> this.insertResultImplementation(Set.of(p), points));
}
public void attachListeners() {
this.instance.eventNode()
@ -45,15 +55,8 @@ public abstract class Score {
setDone();
}
}
protected abstract void insertResultImplementation(Set<Player> p);
protected abstract TranslatableMessage scoreMessage();
public void insertResult(Player p) {
if(hasResult(p)) return;
this.scoreMessage().send(p);
this.insertResultImplementation(Set.of(p));
this.checkGameEnd();
}
protected abstract TranslatableMessage scoreMessage();
public void insertRemainingPlayers(Set<Player> players) {
this.insertResultImplementation(players.stream().filter(p -> !hasResult(p)).collect(Collectors.toSet()));
@ -93,10 +96,19 @@ public abstract class Score {
instance.stop();
}
private void insertResultProcessor(Player p, Runnable callback) {
if(hasResult(p)) return;
this.scoreMessage().send(p);
callback.run();
this.checkGameEnd();
}
public boolean isClosed() {
return isClosed;
}
public void close() { isClosed = true; }
protected void onGameEnd() {
this.instance.stop();
}

View File

@ -0,0 +1,20 @@
package eu.mhsl.minenet.minigames.util;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class MapUtil {
public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map) {
List<Map.Entry<K, V>> list = new ArrayList<>(map.entrySet());
list.sort(Map.Entry.comparingByValue());
Map<K, V> result = new LinkedHashMap<>();
for (Map.Entry<K, V> entry : list) {
result.put(entry.getKey(), entry.getValue());
}
return result;
}
}

View File

@ -17,4 +17,5 @@ api:
port: 8080
admins:
- minetec
- minetec
- 28Pupsi28

View File

@ -92,6 +92,13 @@ name;Spleef;Spleef;
description;Spleef other players and be the last survivor;Zerstöre Blöcke unter anderen Spielern und sei der letzte im Feld
shovelName;Snow thrower;Schneeflug
;;
ns:game_Tetris#;;
name;Tetris;Tetris
description;Sort falling blocks and clear lines;Sortiere fallende Blöcke und kläre Linien
nextTetrominoesCount;Number of upcoming Tetrominos displayed;Anzahl der angezeigten kommenden Tetrominos
isFast;Fast mode;Schneller Modus
hasCombat;Competitive mode;Kompetitiver Modus
;;
ns:game_AnvilRun#;;
name;Anvil Run;Anvil Run
description;Run away from falling anvils;Renne von fallenden Ambossen davon
Can't render this file because it has a wrong number of fields in line 91.