Compare commits
5 Commits
f58cd493bb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c017d4c129 | ||
|
|
b17e88f96d | ||
|
|
f881427a67 | ||
|
|
db537f84ae | ||
|
|
5490ff8b5d |
39
README.md
39
README.md
@@ -4,15 +4,44 @@ Erstellt für das Modul "Anwendungsorientierte Programmierung" im Studiengang Me
|
||||
Dieser Code wurde vollständig von mir und ohne die Verwendung von LLMs geschrieben.
|
||||
An einigen Stellen wurde auf Wissen von StackOverflow und Baeldung zugegriffen – dies ist immer in den JavaDocs gekennzeichnet.
|
||||
Die Commit-Historie ist [auf meinem Gitea](https://git.midnightdust.eu/Motschen/TetrisClone/commits/branch/main) einsehbar.
|
||||
Gradle ist als Build-Tool eingerichtet, um elegant Jar-Dateien zu bauen, dabei aber völlig optional.
|
||||
|
||||
Können Sie meinen Highscore schlagen?
|
||||
|
||||

|
||||
|
||||
## Im Falle einer IllegalArgumentException
|
||||
Eclipse ist unfähig Java-Konventionen zu befolgen und fügt den resources-Ordner nicht automatisch zum classpath hinzu.
|
||||
Um das zu beheben, gehen Sie in die Projekteinstellungen -> Java Build Path -> Source und fügen Sie den resources-Ordner manuell hinzu.
|
||||
|
||||
## Rechtliche Hinweise
|
||||
»Tetris« ist eine eingetragene Marke von The Tetris Company, Inc.
|
||||
Die Verwendung des Namens »Tetris« und des Spielkonzepts erfolgen lediglich zu Bildungszwecken.
|
||||
Die Verwendung des Namens »Tetris« und des Spielkonzepts erfolgen lediglich zu Bildungszwecken.
|
||||
|
||||
# Testbeispiele
|
||||
## Bewegung
|
||||
Spiel starten
|
||||
1. Nach links verschieben mit `A` oder `←`
|
||||
2. Nach rechts verschieben mit `D` oder `→`
|
||||
3. Schneller nach unten mit `S` oder `↓`
|
||||
4. Direkt nach unten mit `Leertaste`
|
||||
5. Rotieren mit `W` oder `↑`
|
||||
|
||||
Bei jeder Bewegung sollte vor dem Umsetzen geprüft werden, ob die neue Position valide (Nicht überschneidend und nicht außerhalb des Spielraums) ist.
|
||||
|
||||
## Logik
|
||||
Sobald eine Reihe vervollständigt wird, sollte sie verschwinden.
|
||||
Wenn die Teile über den oberen Rand ragen, soll das Spiel beendet werden.
|
||||
Im Vorschaufenster sollte immer das nächste Tetrominio angezeigt werden.
|
||||

|
||||
|
||||
## Highscores
|
||||
Sobald das Spiel beendet wird und der Score den aktuell höchsten Score überschreitet, sollte eine Aufforderung zum Eingeben des Namens erscheinen.
|
||||
Nach dem Bestätigen erscheint der Name in der Highscore-Liste und wird über Spielsitzungen hinweg gespeichert.
|
||||

|
||||
|
||||
## Benutzeroberfläche
|
||||
Die Darstellung der Benutzeroberfläche kann in den Einstellungen angepasst werden.
|
||||
Dabei lässt sich die Sprache und die GUI-Skalierung auswählen.
|
||||
Ansonsten sind auch noch Einstellungen zur Lautstärke und Schwierigkeit vorhanden.
|
||||
|
||||
## Sounds und Musik
|
||||
Sofern die Lautstärke angemessen eingestellt und ein Lautsprecher angeschlossen ist, sollten beim Absetzen der Tetrominos, sowie beim Vervollständigen einer/mehrerer Reihen Töne zu hören sein.
|
||||
Auch Musik sollte abgespielt werden, sobald das Spiel gestartet wird.
|
||||
Töne und Musik wurden im Zrythm-DAW erzeugt.
|
||||
BIN
assets/highscore_dialog.png
Normal file
BIN
assets/highscore_dialog.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
BIN
assets/line_completion.png
Normal file
BIN
assets/line_completion.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@@ -1,13 +1,22 @@
|
||||
package eu.midnightdust.yaytris;
|
||||
|
||||
import eu.midnightdust.yaytris.util.Difficulty;
|
||||
import eu.midnightdust.yaytris.util.json.Comment;
|
||||
import eu.midnightdust.yaytris.util.json.NightJson;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@SuppressWarnings("unused") // Comments are unused in code, but are added to the JSON file for readability
|
||||
public class HighScores {
|
||||
private static final NightJson json = new NightJson(HighScores.class, "tetris_scores.json5");
|
||||
public static Map<String, Integer> scores = new HashMap<>();
|
||||
public static Comment c1 = new Comment("Highscores for each difficulty");
|
||||
public static Map<String, Integer> Noob = new HashMap<>();
|
||||
public static Map<String, Integer> Easy = new HashMap<>();
|
||||
public static Map<String, Integer> Normal = new HashMap<>();
|
||||
public static Map<String, Integer> Hard = new HashMap<>();
|
||||
public static Map<String, Integer> Extreme = new HashMap<>();
|
||||
public static Map<String, Integer> WTF = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Saves a new high score for the specified player
|
||||
@@ -16,10 +25,21 @@ public class HighScores {
|
||||
* @param score the score to save
|
||||
*/
|
||||
public static void addScore(String playerName, int score) {
|
||||
scores.put(playerName, score);
|
||||
diffToMap(Settings.difficulty).put(playerName, score);
|
||||
write();
|
||||
}
|
||||
|
||||
public static Map<String, Integer> diffToMap(Difficulty difficulty) {
|
||||
switch (difficulty) {
|
||||
case EASY: return Easy;
|
||||
case NORMAL: return Normal;
|
||||
case HARD: return Hard;
|
||||
case EXTREME: return Extreme;
|
||||
case WTF: return WTF;
|
||||
default: return Noob;
|
||||
}
|
||||
}
|
||||
|
||||
public static void load() {
|
||||
json.readJson();
|
||||
}
|
||||
|
||||
22
src/main/java/eu/midnightdust/yaytris/Main.java
Normal file
22
src/main/java/eu/midnightdust/yaytris/Main.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package eu.midnightdust.yaytris;
|
||||
|
||||
import javax.swing.UIManager;
|
||||
|
||||
public class Main {
|
||||
|
||||
/**
|
||||
* The application's entry point.
|
||||
* Initializes the UI library to match the system style.
|
||||
* Also loads saved settings, translations, and highscores from JSON.
|
||||
*
|
||||
* @param args command line arguments – will be ignored
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
System.setProperty("java.awt.headless", "false");
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
} catch (Exception | Error e) { System.out.printf("%s: %s\n", "Error setting system look and feel", e); }
|
||||
Tetris.init();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package eu.midnightdust.yaytris;
|
||||
|
||||
import eu.midnightdust.yaytris.game.Space;
|
||||
import eu.midnightdust.yaytris.game.Tetromino;
|
||||
import eu.midnightdust.yaytris.ui.ScoreMenu;
|
||||
import eu.midnightdust.yaytris.ui.TetrisUI;
|
||||
import eu.midnightdust.yaytris.util.GravityTimerTask;
|
||||
import eu.midnightdust.yaytris.util.sound.SoundUtil;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.time.LocalTime;
|
||||
import java.util.Random;
|
||||
import java.util.Timer;
|
||||
@@ -20,30 +20,19 @@ public class Tetris {
|
||||
private static TimerTask gravityTask;
|
||||
private static LocalTime startTime;
|
||||
|
||||
/**
|
||||
* The application's entry point.
|
||||
* Initializes the UI library to match the system style.
|
||||
* Also loads saved settings, translations, and highscores from JSON.
|
||||
*
|
||||
* @param args command line arguments – will be ignored
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
System.setProperty("java.awt.headless", "false");
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
} catch (Exception | Error e) { System.out.printf("%s: %s\n", "Error setting system look and feel", e); }
|
||||
public static void init() {
|
||||
Settings.load();
|
||||
Translation.load(Settings.language.locale);
|
||||
HighScores.load();
|
||||
timer = new Timer("Tetris falling pieces");
|
||||
space = new Space();
|
||||
ui = new TetrisUI();
|
||||
Tetris.timer = new Timer("Tetris falling pieces");
|
||||
Tetris.space = new Space();
|
||||
Tetris.ui = new TetrisUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active game space
|
||||
*
|
||||
* @see Space
|
||||
* @see Space
|
||||
*/
|
||||
public static Space getSpace() {
|
||||
return space;
|
||||
@@ -52,7 +41,7 @@ public class Tetris {
|
||||
/**
|
||||
* Get the ui instance
|
||||
*
|
||||
* @see TetrisUI
|
||||
* @see TetrisUI
|
||||
*/
|
||||
public static TetrisUI getUi() {
|
||||
return ui;
|
||||
@@ -61,7 +50,7 @@ public class Tetris {
|
||||
/**
|
||||
* Resets the game space, preparing it for a new game.
|
||||
*
|
||||
* @see Space
|
||||
* @see Space
|
||||
*/
|
||||
public static void resetSpace() {
|
||||
SoundUtil.stopMusic("/music/theme.wav");
|
||||
@@ -72,9 +61,9 @@ public class Tetris {
|
||||
|
||||
/**
|
||||
* Starts a new game of Tetris :D
|
||||
* This involves starting our gravity task, playing music and spawning the first {@link eu.midnightdust.yaytris.game.Tetromino}
|
||||
* This involves starting our gravity task, playing music and spawning the first {@link Tetromino}
|
||||
*
|
||||
* @see Space#spawnTetromino()
|
||||
* @see Space#spawnTetromino()
|
||||
*/
|
||||
public static void startGame() {
|
||||
SoundUtil.playMusic("/music/theme.wav", true);
|
||||
@@ -88,8 +77,8 @@ public class Tetris {
|
||||
* Stops the current game.
|
||||
* Disables falling, fades out music and handles saving of high scores.
|
||||
*
|
||||
* @see ScoreMenu
|
||||
* @see HighScores
|
||||
* @see ScoreMenu
|
||||
* @see HighScores
|
||||
*/
|
||||
public static void stopGame() {
|
||||
SoundUtil.stopMusic("/music/theme.wav");
|
||||
@@ -97,44 +86,46 @@ public class Tetris {
|
||||
timer.purge();
|
||||
if (ui.getMenuPanel() instanceof ScoreMenu) ((ScoreMenu) ui.getMenuPanel()).gameOver();
|
||||
ui.transferFocus();
|
||||
if (HighScores.scores.values().stream().noneMatch(hs -> hs > space.getScore())) ui.showHighscoreDialog(space.getScore());
|
||||
if (HighScores.diffToMap(Settings.difficulty).values().stream().noneMatch(hs -> hs > space.getScore()))
|
||||
ui.showHighscoreDialog(space.getScore());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the displayed score
|
||||
*
|
||||
* @param score the new score
|
||||
* @see ScoreMenu
|
||||
* @param score the new score
|
||||
* @see ScoreMenu
|
||||
*/
|
||||
public static void updateScore(int score) {
|
||||
if (ui.getMenuPanel() instanceof ScoreMenu) ((ScoreMenu) ui.getMenuPanel()).updateScore(score);
|
||||
updateLevel(score);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the elapsed time
|
||||
*
|
||||
* @see ScoreMenu
|
||||
* @see ScoreMenu
|
||||
*/
|
||||
public static void updateTime() {
|
||||
if (ui.getMenuPanel() instanceof ScoreMenu) ((ScoreMenu) ui.getMenuPanel()).updateTime(startTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the displayed level
|
||||
*
|
||||
* @param score the new score, from which the level will be calculated
|
||||
* @see ScoreMenu
|
||||
* @param score the new score, from which the level will be calculated
|
||||
* @see ScoreMenu
|
||||
*/
|
||||
public static void updateLevel(int score) {
|
||||
int newLevel = Math.max(0, (int) (score / 1000f));
|
||||
int newLevel = Math.max(0, (int) (score / 1400f));
|
||||
if (newLevel != space.level) {
|
||||
if (gravityTask != null && Settings.shouldScaleSpeed) {
|
||||
gravityTask.cancel();
|
||||
gravityTask = new GravityTimerTask();
|
||||
timer.scheduleAtFixedRate(gravityTask, 0, Math.max(10, Settings.difficulty.getTimerPeriod() - (Settings.difficulty.getTimerPeriod() / 8) * newLevel));
|
||||
timer.scheduleAtFixedRate(gravityTask, 0, Math.max(10, Settings.difficulty.getTimerPeriod() - (Settings.difficulty.getTimerPeriod() / 16) * newLevel));
|
||||
}
|
||||
space.level = newLevel;
|
||||
if (ui.getMenuPanel() instanceof ScoreMenu) ((ScoreMenu) ui.getMenuPanel()).updateLevel(newLevel);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.midnightdust.yaytris.ui;
|
||||
|
||||
import eu.midnightdust.yaytris.HighScores;
|
||||
import eu.midnightdust.yaytris.Settings;
|
||||
import eu.midnightdust.yaytris.util.CatppuccinColor;
|
||||
|
||||
import javax.swing.*;
|
||||
@@ -8,6 +9,7 @@ import javax.swing.border.LineBorder;
|
||||
import java.awt.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static eu.midnightdust.yaytris.Translation.t;
|
||||
import static eu.midnightdust.yaytris.ui.TetrisUI.scale;
|
||||
@@ -20,11 +22,12 @@ public class HighScoreMenu extends JPanel {
|
||||
this.setBounds(x, y, width, height);
|
||||
this.setLayout(null);
|
||||
|
||||
this.add(new JLabel(t("highscores.title")));
|
||||
this.add(new JLabel(t("highscores.title", Settings.difficulty)));
|
||||
|
||||
List<String> highscores = new ArrayList<>();
|
||||
for (String key : HighScores.scores.keySet()) {
|
||||
highscores.add(String.format("%s – %s", HighScores.scores.get(key), key));
|
||||
Map<String, Integer> scores = HighScores.diffToMap(Settings.difficulty);
|
||||
for (String key : scores.keySet()) {
|
||||
highscores.add(String.format("%s – %s", scores.get(key), key));
|
||||
}
|
||||
highscores.sort((s1, s2) -> Integer.compare(Integer.parseInt(s2.split("–")[0].replace(" ", "")), Integer.parseInt(s1.split("–")[0].replace(" ", ""))));
|
||||
JList<String> highscoreList = new JList<>(highscores.toArray(String[]::new));
|
||||
|
||||
@@ -64,7 +64,7 @@ public class TetrisUI extends JFrame implements KeyListener {
|
||||
private void rescale() {
|
||||
this.setSize((int) (400 * guiScale), (int) (320 * guiScale));
|
||||
titleLabel.setBounds(scale(225), scale(7), scale(110), scale(30));
|
||||
titleLabel.setIcon(new ImageIcon(new ImageIcon(titleImage).getImage().getScaledInstance(scale(110), scale(30), Image.SCALE_DEFAULT)));
|
||||
titleLabel.setIcon(new ImageIcon(titleImage.getScaledInstance(scale(110), scale(30), Image.SCALE_DEFAULT)));
|
||||
gamePanel.setBounds(scale(10), scale(10), scale(150), scale(282));
|
||||
this.setLocationRelativeTo(null);
|
||||
}
|
||||
@@ -165,7 +165,7 @@ public class TetrisUI extends JFrame implements KeyListener {
|
||||
*/
|
||||
public void showHighscoreDialog(int score) {
|
||||
String playerName = JOptionPane.showInputDialog(null, t("dialog.highscore.action"), t("dialog.highscore.title"), JOptionPane.PLAIN_MESSAGE);
|
||||
HighScores.addScore(playerName, score);
|
||||
if (playerName != null) HighScores.addScore(playerName, score);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.util.regex.Pattern;
|
||||
* Concept inspired by GSON
|
||||
*/
|
||||
public class NightJson {
|
||||
private static final String KEY_PATTERN = "\"(-?[A-Za-z-_.]*)\":";
|
||||
private static final String KEY_PATTERN = "\"(.*)\":";
|
||||
Class<?> jsonClass;
|
||||
Field jsonMap;
|
||||
String fileLocation;
|
||||
@@ -175,21 +175,8 @@ public class NightJson {
|
||||
private Object getValue(String s, String key, Function<String, Class<?>> keyToType, Iterator<String> pairIterator) {
|
||||
String val = s.split(KEY_PATTERN, 2)[1];
|
||||
|
||||
if (s.contains("{")) { // Handle maps recursively
|
||||
StringBuilder submapString = new StringBuilder();
|
||||
int level = charAmount(s, '{');
|
||||
submapString.append(val);
|
||||
if (pairIterator.hasNext()) submapString.append(",");
|
||||
while (pairIterator.hasNext()) {
|
||||
String next = pairIterator.next();
|
||||
submapString.append(next);
|
||||
level += charAmount(next, '{');
|
||||
level -= charAmount(next, '}');
|
||||
if (level <= 0) break;
|
||||
if (pairIterator.hasNext()) submapString.append(",");
|
||||
}
|
||||
Optional<Field> field = getField(key);
|
||||
return jsonToMap(String.valueOf(submapString), k -> field.isPresent() ? getTypeArgument(field.get(), 1) : String.class);
|
||||
if (s.contains("{")) {
|
||||
return readJsonMap(key, pairIterator, val);
|
||||
}
|
||||
else {
|
||||
while (val.startsWith(" ")) val = val.substring(1);
|
||||
@@ -200,6 +187,27 @@ public class NightJson {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle maps recursively
|
||||
*/
|
||||
private Map<String, Object> readJsonMap(String key, Iterator<String> pairIterator, String val) {
|
||||
StringBuilder submapString = new StringBuilder();
|
||||
String next = val;
|
||||
int level = 0;
|
||||
while (next != null) {
|
||||
submapString.append(next);
|
||||
level += charAmount(next, '{');
|
||||
level -= charAmount(next, '}');
|
||||
if (level <= 0) break;
|
||||
if (!pairIterator.hasNext()) {
|
||||
submapString.append(",");
|
||||
next = pairIterator.next();
|
||||
} else next = null;
|
||||
}
|
||||
Optional<Field> field = getField(key);
|
||||
return jsonToMap(String.valueOf(submapString), k -> field.isPresent() ? getTypeArgument(field.get(), 1) : String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the amount of appearances of a char in a string.
|
||||
*
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"game.over": "Game over :(",
|
||||
"game.score": "Score: %s",
|
||||
"game.time": "Zeit: %s",
|
||||
"highscores.title": "Highscores:",
|
||||
"highscores.title": "Highscores (%s):",
|
||||
"menu.exit": "Spiel verlassen",
|
||||
"menu.highscores": "Highscores",
|
||||
"menu.settings": "Einstellungen",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"game.over": "Game over :(",
|
||||
"game.score": "Score: %s",
|
||||
"game.time": "Time: %s",
|
||||
"highscores.title": "Highscores:",
|
||||
"highscores.title": "Highscores (%s):",
|
||||
"menu.exit": "Exit Game",
|
||||
"menu.highscores": "Highscores",
|
||||
"menu.settings": "Settings",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"scores": {
|
||||
"MartinProkoph": 8040
|
||||
}
|
||||
// Highscores for each difficulty
|
||||
"Noob": {},
|
||||
"Easy": {},
|
||||
"Normal": {
|
||||
"Martin": 10593
|
||||
},
|
||||
"Hard": {},
|
||||
"Extreme": {},
|
||||
"WTF": {}
|
||||
}
|
||||
Reference in New Issue
Block a user