diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/Tetris.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/Tetris.java index 0f3336f..80318c8 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/Tetris.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/Tetris.java @@ -82,11 +82,11 @@ class Tetris extends StatelessGame { private void onPlayerInteract(@NotNull PlayerUseItemEvent event) { event.setItemUseTime(0); - this.tetrisGames.get(event.getPlayer()).pressedButton(TetrisGame.Button.mouseRight); + this.tetrisGames.get(event.getPlayer()).pressedButtonRaw(TetrisGame.Button.mouseRight); } private void onPlayerAttack(@NotNull PlayerHandAnimationEvent event) { - this.tetrisGames.get(event.getPlayer()).pressedButton(TetrisGame.Button.mouseLeft); + this.tetrisGames.get(event.getPlayer()).pressedButtonRaw(TetrisGame.Button.mouseLeft); } private void onPlayerTick(PlayerTickEvent event) { @@ -100,11 +100,11 @@ class Tetris extends StatelessGame { if(player.getGameMode() == GameMode.SPECTATOR) return; - if(player.inputs().forward()) tetrisGame.pressedButton(TetrisGame.Button.W); - if(player.inputs().backward()) tetrisGame.pressedButton(TetrisGame.Button.S); - if(player.inputs().right()) tetrisGame.pressedButton(TetrisGame.Button.D); - if(player.inputs().left()) tetrisGame.pressedButton(TetrisGame.Button.A); - if(player.inputs().jump()) tetrisGame.pressedButton(TetrisGame.Button.space); + if(player.inputs().forward()) tetrisGame.pressedButtonRaw(TetrisGame.Button.W); + if(player.inputs().backward()) tetrisGame.pressedButtonRaw(TetrisGame.Button.S); + if(player.inputs().right()) tetrisGame.pressedButtonRaw(TetrisGame.Button.D); + if(player.inputs().left()) tetrisGame.pressedButtonRaw(TetrisGame.Button.A); + if(player.inputs().jump()) tetrisGame.pressedButtonRaw(TetrisGame.Button.space); } private void letPlayerLoose(Player player) { diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/Orientation.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/Orientation.java new file mode 100644 index 0000000..6ebf10d --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/Orientation.java @@ -0,0 +1,28 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game; + +public enum Orientation { + NONE, + RIGHT, + LEFT, + UPSIDE_DOWN; + + public Orientation rotated(boolean clockwise) { + switch(this) { + case NONE -> { + return clockwise ? Orientation.RIGHT : Orientation.LEFT; + } + case RIGHT -> { + return clockwise ? Orientation.UPSIDE_DOWN : Orientation.NONE; + } + case UPSIDE_DOWN -> { + return clockwise ? Orientation.LEFT : Orientation.RIGHT; + } + case LEFT -> { + return clockwise ? Orientation.NONE : Orientation.UPSIDE_DOWN; + } + default -> { + return Orientation.NONE; + } + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/RotationChecker.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/RotationChecker.java new file mode 100644 index 0000000..f0f91ef --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/RotationChecker.java @@ -0,0 +1,52 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game; + +import java.util.Map; +import java.util.stream.IntStream; + +public final class RotationChecker {private static final Map STANDARD_WALL_KICKS = Map.of( + Orientation.NONE, new int[][] {{0,0}, {0,0}, {0,0}, {0,0}, {0,0}}, + Orientation.RIGHT, new int[][] {{0,0}, {1,0}, {1,-1}, {0,2}, {1,2}}, + Orientation.UPSIDE_DOWN, new int[][] {{0,0}, {0,0}, {0,0}, {0,0}, {0,0}}, + Orientation.LEFT, new int[][] {{0,0}, {-1,0}, {-1,-1}, {0,2}, {-1,2}} + ); + + private static final Map I_WALL_KICKS = Map.of( + Orientation.NONE, new int[][] {{0,0}, {-1,0}, {2,0}, {-1,0}, {2,0}}, + Orientation.RIGHT, new int[][] {{-1,0}, {0,0}, {0,0}, {0,1}, {0,-2}}, + Orientation.UPSIDE_DOWN, new int[][] {{-1,1}, {1,1}, {-2,1}, {1,0}, {-2,0}}, + Orientation.LEFT, new int[][] {{0,1}, {0,1}, {0,1}, {0,-1}, {0,2}} + ); + + private static final Map O_WALL_KICKS = Map.of( + Orientation.NONE, new int[][] {{0,0}}, + Orientation.RIGHT, new int[][] {{0,-1}}, + Orientation.UPSIDE_DOWN, new int[][] {{-1,-1}}, + Orientation.LEFT, new int[][] {{-1,0}} + ); + + public static int[][] getKicksArray(Orientation from, Orientation to, Tetromino.Shape shape) { + int[][] firstOffsets = getOffsetData(from, shape); + int[][] secondOffsets = getOffsetData(to, shape); + int[][] result = new int[firstOffsets.length][2]; + + for(int i = 0; i < firstOffsets.length; i++) { + result[i] = subtractInt(firstOffsets[i], secondOffsets[i]); + } + + return result; + } + + private static int[] subtractInt(int[] first, int[] second) { + return IntStream.range(0, first.length) + .map(i -> first[i] - second[i]) + .toArray(); + } + + private static int[][] getOffsetData(Orientation orientation, Tetromino.Shape shape) { + return switch(shape) { + case J, L, S, T, Z -> STANDARD_WALL_KICKS.get(orientation); + case I -> I_WALL_KICKS.get(orientation); + case O -> O_WALL_KICKS.get(orientation); + }; + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/TetrisGame.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/TetrisGame.java index 44dbe5f..8695f9f 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/TetrisGame.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/TetrisGame.java @@ -2,10 +2,9 @@ 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.Task; import net.minestom.server.timer.TaskSchedule; import java.util.*; @@ -23,6 +22,8 @@ public class TetrisGame { private final Map lastPresses = new HashMap<>(); private final List otherTetrisGames = new ArrayList<>(); private final Random random; + private Task tetrominoLockTask; + private Task hardTetrominoLockTask; public boolean lost = false; public boolean paused = true; public Tetromino currentTetromino; @@ -53,25 +54,28 @@ public class TetrisGame { } } - public void pressedButton(Button button) { - final int standardButtonDelay = 100; - final int buttonDebounce = 70; + public void pressedButtonRaw(Button button) { + final int standardButtonDelay = 95; + final int wsButtonDebounce = 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); + switch(button) { + case W -> this.lastPresses.put(button, System.currentTimeMillis() + wsButtonDebounce); + case S -> this.lastPresses.put(button, System.currentTimeMillis() - wsButtonDebounce); + case mouseLeft, mouseRight -> this.lastPresses.put(button, 0L); + default -> this.lastPresses.put(button, System.currentTimeMillis()); + } if(this.lost || this.paused) return; switch(button) { - case A -> this.currentTetromino.moveLeft(); + case A -> this.moveLeft(); case S -> this.moveDown(); - case D -> this.currentTetromino.moveRight(); + case D -> this.moveRight(); case W -> this.hardDrop(); - case mouseLeft -> this.currentTetromino.rotate(false); - case mouseRight -> this.currentTetromino.rotate(true); + case mouseLeft -> this.rotate(false); + case mouseRight -> this.rotate(true); case space -> this.switchHold(); } } @@ -82,8 +86,7 @@ public class TetrisGame { public void start() { this.paused = false; - Scheduler scheduler = MinecraftServer.getSchedulerManager(); - scheduler.submitTask(() -> { + this.instance.scheduler().submitTask(() -> { if(this.lost) return TaskSchedule.stop(); int standardTickDelay = 40; if(this.isFast) standardTickDelay = 20; @@ -111,8 +114,9 @@ public class TetrisGame { public void tick() { if(this.lost || this.paused) return; + if(!this.currentTetromino.isGrounded()) this.stopTetrominoLockTask(true); if(!this.currentTetromino.moveDown()) { - this.setActiveTetrominoDown(); + this.scheduleTetrominoLock(); } } @@ -139,33 +143,42 @@ public class TetrisGame { this.lost = true; } - private boolean moveDown() { + private void moveDown() { if(!this.currentTetromino.moveDown()) { - this.setActiveTetrominoDown(); - return false; + this.scheduleTetrominoLock(); + return; } this.score += 1; this.updateInfo(); - return true; } - private boolean hardDrop() { + private void moveLeft() { + if(this.currentTetromino.moveLeft()) this.stopTetrominoLockTask(false); + } + + private void moveRight() { + if(this.currentTetromino.moveRight()) this.stopTetrominoLockTask(false); + } + + private void rotate(boolean clockwise) { + if(this.currentTetromino.rotate(clockwise)) this.stopTetrominoLockTask(false); + } + + private void hardDrop() { if(!this.currentTetromino.moveDown()) { - this.setActiveTetrominoDown(); - return false; + this.lockActiveTetromino(); + return; } - this.score += 2; - this.updateInfo(); - while(this.currentTetromino.moveDown()) { + do { this.score += 2; - this.updateInfo(); - } - this.setActiveTetrominoDown(); - return true; + } while(this.currentTetromino.moveDown()); + this.updateInfo(); + this.lockActiveTetromino(); } - private boolean switchHold() { - if(!this.holdPossible) return false; + private void switchHold() { + if(!this.holdPossible) return; + this.stopTetrominoLockTask(true); Tetromino newCurrentTetromino; if(this.holdTetromino == null) { @@ -189,7 +202,6 @@ public class TetrisGame { this.holdTetromino.setPosition(this.holdPosition.add(xChange, 0, 0)); this.holdTetromino.drawAsEntities(); this.holdPossible = false; - return true; } private void updateNextTetrominoes() { @@ -235,7 +247,40 @@ public class TetrisGame { this.sidebar.updateLineScore("2", this.level); } - private void setActiveTetrominoDown() { + private void scheduleTetrominoLock() { + if(this.tetrominoLockTask == null || !this.tetrominoLockTask.isAlive()) + this.tetrominoLockTask = this.instance.scheduler().scheduleTask(() -> { + if(this.currentTetromino.isGrounded()) { + this.lockActiveTetromino(); + } else { + this.stopTetrominoLockTask(true); + } + return TaskSchedule.stop(); + }, TaskSchedule.millis(500)); + if(this.hardTetrominoLockTask == null || !this.hardTetrominoLockTask.isAlive()) + this.hardTetrominoLockTask = this.instance.scheduler().scheduleTask(() -> { + if(this.currentTetromino.isGrounded()) { + this.lockActiveTetromino(); + } else { + this.stopTetrominoLockTask(true); + } + return TaskSchedule.stop(); + }, TaskSchedule.millis(6000)); + } + + private void stopTetrominoLockTask(boolean resetHard) { + if(this.tetrominoLockTask != null) { + this.tetrominoLockTask.cancel(); + this.tetrominoLockTask = null; + } + if(resetHard && this.hardTetrominoLockTask != null) { + this.hardTetrominoLockTask.cancel(); + this.hardTetrominoLockTask = null; + } + } + + private void lockActiveTetromino() { + this.stopTetrominoLockTask(true); this.currentTetromino.removeOwnEntities(); this.currentTetromino = this.nextTetrominoes.removeFirst(); this.currentTetromino.remove(); diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/Tetromino.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/Tetromino.java index 7acf4fe..bce04ed 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/Tetromino.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/Tetromino.java @@ -8,14 +8,12 @@ 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; +import java.util.*; public class Tetromino { private final static EntityType ghostEntityType = EntityType.FALLING_BLOCK; private final static Tag uuidTag = Tag.String("uuid"); + private Orientation orientation = Orientation.NONE; private final Shape shape; private final StatelessGame instance; private final UUID uuid; @@ -28,10 +26,10 @@ public class Tetromino { this.uuid = UUID.randomUUID(); switch(this.shape) { - case I -> this.shapeArray = new int[][]{{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}}; + case I -> this.shapeArray = new int[][]{{0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 1, 1, 1, 1}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}}; case J -> this.shapeArray = new int[][]{{1, 0, 0}, {1, 1, 1}, {0, 0, 0}}; case L -> this.shapeArray = new int[][]{{0, 0, 1}, {1, 1, 1}, {0, 0, 0}}; - case O -> this.shapeArray = new int[][]{{1, 1}, {1, 1}}; + case O -> this.shapeArray = new int[][]{{0, 1, 1}, {0, 1, 1}, {0, 0, 0}}; case S -> this.shapeArray = new int[][]{{0, 1, 1}, {1, 1, 0}, {0, 0, 0}}; case T -> this.shapeArray = new int[][]{{0, 1, 0}, {1, 1, 1}, {0, 0, 0}}; case Z -> this.shapeArray = new int[][]{{1, 1, 0}, {0, 1, 1}, {0, 0, 0}}; @@ -43,12 +41,23 @@ public class Tetromino { } public void setPosition(Pos newPosition) { - this.position = newPosition; + this.position = new Pos(newPosition.x(), newPosition.y(), newPosition.z()); } public boolean rotate(boolean clockwise) { int[][] newShapeArray = this.getTurnedShapeArray(clockwise); - return this.checkCollisionAndMove(this.position, newShapeArray); + Orientation newOrientation = this.orientation.rotated(clockwise); + + int[][] kicksArray = RotationChecker.getKicksArray(this.orientation, newOrientation, this.shape); + for(int[] k : kicksArray) { + Pos candidate = new Pos(this.position.x() + k[0], this.position.y() + k[1], this.position.z()); + if(this.checkCollisionAndMove(candidate, newShapeArray)) { + this.orientation = newOrientation; + return true; + } + } + + return false; } public boolean moveDown() { @@ -73,7 +82,7 @@ public class Tetromino { public void draw(boolean withGhost) { if(withGhost) { Pos ghostPos = this.position; - while(!this.checkCollision(ghostPos.sub(0, 1, 0), this.shapeArray)) { + while(!this.hasCollision(ghostPos.sub(0, 1, 0), this.shapeArray)) { ghostPos = ghostPos.sub(0, 1, 0); } Pos positionChange = this.position.sub(ghostPos); @@ -182,7 +191,7 @@ public class Tetromino { private boolean isPartOfTetromino(Pos position) { return this.getBlockPositions().stream() - .anyMatch(pos -> pos.equals(position)); + .anyMatch(pos -> pos.sameBlock(position)); } private List getBlockPositions() { @@ -198,10 +207,10 @@ public class Tetromino { 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)); + if(Objects.requireNonNull(this.shape) == Shape.I) { + returnList.add(position.add(x - 2, y - 2, 0)); + } else { + returnList.add(position.add(x - 1, y - 1, 0)); } } } @@ -210,7 +219,7 @@ public class Tetromino { return returnList; } - private boolean checkCollision(Pos newPosition, int[][] newShapeArray) { + private boolean hasCollision(Pos newPosition, int[][] newShapeArray) { List newBlockPositions = this.getBlockPositions(newPosition, newShapeArray); for(Pos pos : newBlockPositions) { @@ -222,15 +231,17 @@ public class Tetromino { return false; } + public boolean isGrounded() { + return this.hasCollision(this.position.sub(0, 1, 0), this.shapeArray); + } + private boolean checkCollisionAndMove(Pos newPosition, int[][] newShapeArray) { - if(!this.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; + if(this.hasCollision(newPosition, newShapeArray)) return false; + this.remove(); + this.shapeArray = Arrays.stream(newShapeArray).map(int[]::clone).toArray(int[][]::new); + this.setPosition(newPosition); + this.draw(); + return true; } public enum Shape {