develop-tetris-srs #11

Open
Pupsi wants to merge 6 commits from develop-tetris-srs into develop
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) {
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) {

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 -> {
Review

ist default hier überhaupt erreichbar?

ist default hier überhaupt erreichbar?
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(
Review

zeilenumbruch

zeilenumbruch
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 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<Button, Long> lastPresses = new HashMap<>();
private final List<TetrisGame> 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;
}
do {
this.score += 2;
} while(this.currentTetromino.moveDown());
this.updateInfo();
while(this.currentTetromino.moveDown()) {
this.score += 2;
this.updateInfo();
}
this.setActiveTetrominoDown();
return true;
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(() -> {
Review

tasks hierfür fühlen sich falsch an...
Wo kommen die Limits (500ms) her? Ist das im Original auch genau so?

tasks hierfür fühlen sich falsch an... Wo kommen die Limits (500ms) her? Ist das im Original auch genau so?
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();

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.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<String> 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<Pos> 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<Pos> newBlockPositions = this.getBlockPositions(newPosition, newShapeArray);
for(Pos pos : newBlockPositions) {
@@ -222,16 +231,18 @@ 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)) {
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;
}
return false;
}
public enum Shape {
I,