6 Commits

5 changed files with 199 additions and 63 deletions

View File

@@ -82,11 +82,11 @@ class Tetris extends StatelessGame {
private void onPlayerInteract(@NotNull PlayerUseItemEvent event) { private void onPlayerInteract(@NotNull PlayerUseItemEvent event) {
event.setItemUseTime(0); 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) { 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) { private void onPlayerTick(PlayerTickEvent event) {
@@ -100,11 +100,11 @@ class Tetris extends StatelessGame {
if(player.getGameMode() == GameMode.SPECTATOR) return; if(player.getGameMode() == GameMode.SPECTATOR) return;
if(player.inputs().forward()) tetrisGame.pressedButton(TetrisGame.Button.W); if(player.inputs().forward()) tetrisGame.pressedButtonRaw(TetrisGame.Button.W);
if(player.inputs().backward()) tetrisGame.pressedButton(TetrisGame.Button.S); if(player.inputs().backward()) tetrisGame.pressedButtonRaw(TetrisGame.Button.S);
if(player.inputs().right()) tetrisGame.pressedButton(TetrisGame.Button.D); if(player.inputs().right()) tetrisGame.pressedButtonRaw(TetrisGame.Button.D);
if(player.inputs().left()) tetrisGame.pressedButton(TetrisGame.Button.A); if(player.inputs().left()) tetrisGame.pressedButtonRaw(TetrisGame.Button.A);
if(player.inputs().jump()) tetrisGame.pressedButton(TetrisGame.Button.space); if(player.inputs().jump()) tetrisGame.pressedButtonRaw(TetrisGame.Button.space);
} }
private void letPlayerLoose(Player player) { private void letPlayerLoose(Player player) {

View File

@@ -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;
}
}
}
}

View File

@@ -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<Orientation, int[][]> 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<Orientation, int[][]> 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<Orientation, int[][]> 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);
};
}
}

View File

@@ -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 eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Pos;
import net.minestom.server.scoreboard.Sidebar; 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 net.minestom.server.timer.TaskSchedule;
import java.util.*; import java.util.*;
@@ -23,6 +22,8 @@ public class TetrisGame {
private final Map<Button, Long> lastPresses = new HashMap<>(); private final Map<Button, Long> lastPresses = new HashMap<>();
private final List<TetrisGame> otherTetrisGames = new ArrayList<>(); private final List<TetrisGame> otherTetrisGames = new ArrayList<>();
private final Random random; private final Random random;
private Task tetrominoLockTask;
private Task hardTetrominoLockTask;
public boolean lost = false; public boolean lost = false;
public boolean paused = true; public boolean paused = true;
public Tetromino currentTetromino; public Tetromino currentTetromino;
@@ -53,25 +54,28 @@ public class TetrisGame {
} }
} }
public void pressedButton(Button button) { public void pressedButtonRaw(Button button) {
final int standardButtonDelay = 100; final int standardButtonDelay = 95;
final int buttonDebounce = 70; final int wsButtonDebounce = 70;
if(this.lastPresses.getOrDefault(button, 0L) >= System.currentTimeMillis() - standardButtonDelay) return; if(this.lastPresses.getOrDefault(button, 0L) >= System.currentTimeMillis() - standardButtonDelay) return;
this.lastPresses.put(button, System.currentTimeMillis()); switch(button) {
if(button == Button.W) this.lastPresses.put(button, System.currentTimeMillis() + buttonDebounce); case W -> this.lastPresses.put(button, System.currentTimeMillis() + wsButtonDebounce);
if(button == Button.S) this.lastPresses.put(button, System.currentTimeMillis() - buttonDebounce); 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; if(this.lost || this.paused) return;
switch(button) { switch(button) {
case A -> this.currentTetromino.moveLeft(); case A -> this.moveLeft();
case S -> this.moveDown(); case S -> this.moveDown();
case D -> this.currentTetromino.moveRight(); case D -> this.moveRight();
case W -> this.hardDrop(); case W -> this.hardDrop();
case mouseLeft -> this.currentTetromino.rotate(false); case mouseLeft -> this.rotate(false);
case mouseRight -> this.currentTetromino.rotate(true); case mouseRight -> this.rotate(true);
case space -> this.switchHold(); case space -> this.switchHold();
} }
} }
@@ -82,8 +86,7 @@ public class TetrisGame {
public void start() { public void start() {
this.paused = false; this.paused = false;
Scheduler scheduler = MinecraftServer.getSchedulerManager(); this.instance.scheduler().submitTask(() -> {
scheduler.submitTask(() -> {
if(this.lost) return TaskSchedule.stop(); if(this.lost) return TaskSchedule.stop();
int standardTickDelay = 40; int standardTickDelay = 40;
if(this.isFast) standardTickDelay = 20; if(this.isFast) standardTickDelay = 20;
@@ -111,8 +114,9 @@ public class TetrisGame {
public void tick() { public void tick() {
if(this.lost || this.paused) return; if(this.lost || this.paused) return;
if(!this.currentTetromino.isGrounded()) this.stopTetrominoLockTask(true);
if(!this.currentTetromino.moveDown()) { if(!this.currentTetromino.moveDown()) {
this.setActiveTetrominoDown(); this.scheduleTetrominoLock();
} }
} }
@@ -139,33 +143,42 @@ public class TetrisGame {
this.lost = true; this.lost = true;
} }
private boolean moveDown() { private void moveDown() {
if(!this.currentTetromino.moveDown()) { if(!this.currentTetromino.moveDown()) {
this.setActiveTetrominoDown(); this.scheduleTetrominoLock();
return false; return;
} }
this.score += 1; this.score += 1;
this.updateInfo(); 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()) { if(!this.currentTetromino.moveDown()) {
this.setActiveTetrominoDown(); this.lockActiveTetromino();
return false; return;
} }
this.score += 2; do {
this.updateInfo();
while(this.currentTetromino.moveDown()) {
this.score += 2; this.score += 2;
this.updateInfo(); } while(this.currentTetromino.moveDown());
} this.updateInfo();
this.setActiveTetrominoDown(); this.lockActiveTetromino();
return true;
} }
private boolean switchHold() { private void switchHold() {
if(!this.holdPossible) return false; if(!this.holdPossible) return;
this.stopTetrominoLockTask(true);
Tetromino newCurrentTetromino; Tetromino newCurrentTetromino;
if(this.holdTetromino == null) { if(this.holdTetromino == null) {
@@ -189,7 +202,6 @@ public class TetrisGame {
this.holdTetromino.setPosition(this.holdPosition.add(xChange, 0, 0)); this.holdTetromino.setPosition(this.holdPosition.add(xChange, 0, 0));
this.holdTetromino.drawAsEntities(); this.holdTetromino.drawAsEntities();
this.holdPossible = false; this.holdPossible = false;
return true;
} }
private void updateNextTetrominoes() { private void updateNextTetrominoes() {
@@ -235,7 +247,40 @@ public class TetrisGame {
this.sidebar.updateLineScore("2", this.level); 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.removeOwnEntities();
this.currentTetromino = this.nextTetrominoes.removeFirst(); this.currentTetromino = this.nextTetrominoes.removeFirst();
this.currentTetromino.remove(); this.currentTetromino.remove();

View File

@@ -8,14 +8,12 @@ import net.minestom.server.entity.metadata.other.FallingBlockMeta;
import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.Block;
import net.minestom.server.tag.Tag; import net.minestom.server.tag.Tag;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
public class Tetromino { public class Tetromino {
private final static EntityType ghostEntityType = EntityType.FALLING_BLOCK; private final static EntityType ghostEntityType = EntityType.FALLING_BLOCK;
private final static Tag<String> uuidTag = Tag.String("uuid"); private final static Tag<String> uuidTag = Tag.String("uuid");
private Orientation orientation = Orientation.NONE;
private final Shape shape; private final Shape shape;
private final StatelessGame instance; private final StatelessGame instance;
private final UUID uuid; private final UUID uuid;
@@ -28,10 +26,10 @@ public class Tetromino {
this.uuid = UUID.randomUUID(); this.uuid = UUID.randomUUID();
switch(this.shape) { 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 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 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 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 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}}; 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) { public void setPosition(Pos newPosition) {
this.position = newPosition; this.position = new Pos(newPosition.x(), newPosition.y(), newPosition.z());
} }
public boolean rotate(boolean clockwise) { public boolean rotate(boolean clockwise) {
int[][] newShapeArray = this.getTurnedShapeArray(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() { public boolean moveDown() {
@@ -73,7 +82,7 @@ public class Tetromino {
public void draw(boolean withGhost) { public void draw(boolean withGhost) {
if(withGhost) { if(withGhost) {
Pos ghostPos = this.position; 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); ghostPos = ghostPos.sub(0, 1, 0);
} }
Pos positionChange = this.position.sub(ghostPos); Pos positionChange = this.position.sub(ghostPos);
@@ -182,7 +191,7 @@ public class Tetromino {
private boolean isPartOfTetromino(Pos position) { private boolean isPartOfTetromino(Pos position) {
return this.getBlockPositions().stream() return this.getBlockPositions().stream()
.anyMatch(pos -> pos.equals(position)); .anyMatch(pos -> pos.sameBlock(position));
} }
private List<Pos> getBlockPositions() { private List<Pos> getBlockPositions() {
@@ -198,10 +207,10 @@ public class Tetromino {
for(int x = 0; x < arrayLength; x++) { for(int x = 0; x < arrayLength; x++) {
for(int y = 0; y < arrayLength; y++) { for(int y = 0; y < arrayLength; y++) {
if(shapeArray[arrayLength - 1 - y][x] == 1) { if(shapeArray[arrayLength - 1 - y][x] == 1) {
switch(this.shape) { if(Objects.requireNonNull(this.shape) == Shape.I) {
case I -> returnList.add(position.add(x - 1, y - 2, 0)); returnList.add(position.add(x - 2, y - 2, 0));
case O -> returnList.add(position.add(x, y, 0)); } else {
default -> returnList.add(position.add(x - 1, y - 1, 0)); returnList.add(position.add(x - 1, y - 1, 0));
} }
} }
} }
@@ -210,7 +219,7 @@ public class Tetromino {
return returnList; return returnList;
} }
private boolean checkCollision(Pos newPosition, int[][] newShapeArray) { private boolean hasCollision(Pos newPosition, int[][] newShapeArray) {
List<Pos> newBlockPositions = this.getBlockPositions(newPosition, newShapeArray); List<Pos> newBlockPositions = this.getBlockPositions(newPosition, newShapeArray);
for(Pos pos : newBlockPositions) { for(Pos pos : newBlockPositions) {
@@ -222,15 +231,17 @@ public class Tetromino {
return false; return false;
} }
public boolean isGrounded() {
return this.hasCollision(this.position.sub(0, 1, 0), this.shapeArray);
}
private boolean checkCollisionAndMove(Pos newPosition, int[][] newShapeArray) { private boolean checkCollisionAndMove(Pos newPosition, int[][] newShapeArray) {
if(!this.checkCollision(newPosition, newShapeArray)) { if(this.hasCollision(newPosition, newShapeArray)) return false;
this.remove(); this.remove();
this.shapeArray = Arrays.stream(newShapeArray).map(int[]::clone).toArray(int[][]::new); this.shapeArray = Arrays.stream(newShapeArray).map(int[]::clone).toArray(int[][]::new);
this.setPosition(newPosition); this.setPosition(newPosition);
this.draw(); this.draw();
return true; return true;
}
return false;
} }
public enum Shape { public enum Shape {