Compare commits

...

5 Commits

Author SHA1 Message Date
Martin Prokoph
d10313ea76 feat: highscore dialog and list 2025-08-01 20:24:14 +02:00
Martin Prokoph
93c0ee5f95 feat: save highscores 2025-08-01 19:07:20 +02:00
Martin Prokoph
8037f9a323 feat(NightJson): add support for maps! 2025-08-01 19:06:44 +02:00
Martin Prokoph
6cc7b95852 feat: scale speed based on level 2025-08-01 17:59:48 +02:00
Martin Prokoph
a228e9ab1a feat: support and target Java 11 2025-08-01 17:02:46 +02:00
14 changed files with 214 additions and 31 deletions

2
.idea/misc.xml generated
View File

@@ -4,7 +4,7 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@@ -0,0 +1,24 @@
package eu.midnightdust.yaytris;
import eu.midnightdust.yaytris.util.NightJson;
import java.util.HashMap;
import java.util.Map;
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 void addScore(String playerName, int score) {
scores.put(playerName, score);
write();
}
public static void load() {
json.readJson();
}
public static void write() {
json.writeJson();
}
}

View File

@@ -9,6 +9,7 @@ public class Settings {
public static int musicVolume = 100;
public static int soundVolume = 100;
public static float guiScale = 3.f;
public static boolean shouldScaleSpeed = true;
public static Difficulty difficulty = Difficulty.NORMAL;
public static void load() {

View File

@@ -25,6 +25,7 @@ public class Tetris {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception | Error e) { System.out.printf("%s: %s\n", "Error setting system look and feel", e); }
Settings.load();
HighScores.load();
timer = new Timer("Tetris falling pieces");
space = new Space();
ui = new TetrisUI();
@@ -55,14 +56,28 @@ 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());
}
public static void updateScore(int score) {
if (ui.getMenuPanel() instanceof ScoreMenu) ((ScoreMenu) ui.getMenuPanel()).updateScore(score);
updateLevel(score);
}
public static void updateTime() {
if (ui.getMenuPanel() instanceof ScoreMenu) ((ScoreMenu) ui.getMenuPanel()).updateTime(startTime);
}
public static void updateLevel(int score) {
int newLevel = Math.max(0, (int) (score / 1000f));
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));
}
space.level = newLevel;
if (ui.getMenuPanel() instanceof ScoreMenu) ((ScoreMenu) ui.getMenuPanel()).updateLevel(newLevel);
}
}
public static class GravityTimerTask extends TimerTask {
@Override

View File

@@ -13,6 +13,7 @@ public class Space {
private TetrominoShape nextShape;
private Tetromino currentTetromino;
private int score;
public int level;
public Space() {
gameMap = new Color[14][8];
@@ -64,6 +65,10 @@ public class Space {
return gameMap;
}
public int getScore() {
return score;
}
public void onLinesChanged(Tetromino tetromino, int... lines) {
int combo = 0;
Set<Integer> completedLines = new TreeSet<>();
@@ -79,7 +84,7 @@ public class Space {
gameMap[line][i] = newBlobs[i];
}
if (Arrays.stream(gameMap[line]).noneMatch(Objects::isNull)) { // Line completed
combo += 10;
combo += 40;
completedLines.add(line);
combo *= completedLines.size();
}

View File

@@ -9,8 +9,12 @@ import static eu.midnightdust.yaytris.ui.TetrisUI.setFontScale;
public class AbstractMenu extends JPanel {
@Override
public Component add(Component comp) {
comp.setBounds(scale(60), scale(20+23*this.getComponentCount()), scale(100), scale(20));
comp.setBounds(scale(60), scale(20+getSpacing()*this.getComponentCount()), scale(100), scale(20));
if (comp instanceof JComponent) setFontScale((JComponent) comp);
return super.add(comp);
}
public int getSpacing() {
return 23;
}
}

View File

@@ -0,0 +1,45 @@
package eu.midnightdust.yaytris.ui;
import eu.midnightdust.yaytris.HighScores;
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import static eu.midnightdust.yaytris.ui.TetrisUI.scale;
public class HighScoreMenu extends JPanel {
final TetrisUI ui;
HighScoreMenu(int x, int y, int width, int height, TetrisUI ui) {
this.ui = ui;
this.setBounds(x, y, width, height);
this.setLayout(null);
this.add(new JLabel("Highscores:"));
List<String> highscores = new ArrayList<>();
for (String key : HighScores.scores.keySet()) {
highscores.add(String.format("%s %s", HighScores.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));
JScrollPane highscoreScrollPane = new JScrollPane(highscoreList);
this.add(highscoreScrollPane);
highscoreScrollPane.setBounds(scale(60), scale(43), scale(100), scale(80));
JButton backButton = new JButton("Zurück");
backButton.addActionListener(ui::openMainMenu);
backButton.setBounds(scale(60), scale(140), scale(100), scale(20));
this.add(backButton);
}
@Override
public Component add(Component comp) {
if (comp instanceof JLabel) {
comp.setBounds(scale(60), scale(30), scale(100), scale(7));
}
return super.add(comp);
}
}

View File

@@ -18,6 +18,10 @@ public class MainMenu extends AbstractMenu {
settingsButton.addActionListener(ui::openSettings);
this.add(settingsButton);
JButton highscoreButton = new JButton("Highscores");
highscoreButton.addActionListener(ui::openHighscores);
this.add(highscoreButton);
JButton leaveButton = new JButton("Spiel verlassen");
leaveButton.addActionListener(e -> System.exit(0));
this.add(leaveButton);

View File

@@ -15,6 +15,7 @@ public class ScoreMenu extends AbstractMenu {
final PreviewCanvas previewCanvas;
final JLabel gameOverLabel;
final JLabel currentScoreLabel;
final JLabel currentLevelLabel;
final JLabel currentTimeLabel;
ScoreMenu(int x, int y, int width, int height, TetrisUI ui) {
@@ -34,6 +35,9 @@ public class ScoreMenu extends AbstractMenu {
this.currentScoreLabel = new JLabel("Score: 0");
this.add(currentScoreLabel);
this.currentLevelLabel = new JLabel("Level: 0");
this.add(currentLevelLabel);
this.currentTimeLabel = new JLabel("Zeit: 00:00");
this.add(currentTimeLabel);
@@ -43,7 +47,7 @@ public class ScoreMenu extends AbstractMenu {
}
public void updateScore(int score) {
this.currentScoreLabel.setText("Score: %s".formatted(score));
this.currentScoreLabel.setText(String.format("Score: %s", score));
this.repaint();
}
@@ -51,11 +55,21 @@ public class ScoreMenu extends AbstractMenu {
LocalTime timeElapsed = LocalTime.ofNanoOfDay(LocalTime.now().toNanoOfDay() - startingTime.toNanoOfDay());
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern(timeElapsed.getHour() > 0 ? "HH:mm:ss" : "mm:ss");
this.currentTimeLabel.setText("Zeit: %s".formatted(timeFormatter.format(timeElapsed)));
this.currentTimeLabel.setText(String.format("Zeit: %s", timeFormatter.format(timeElapsed)));
this.repaint();
}
public void gameOver() {
this.gameOverLabel.setText("Game over :(");
}
public void updateLevel(int level) {
this.currentLevelLabel.setText(String.format("Level: %s", level));
this.repaint();
}
@Override
public int getSpacing() {
return 17;
}
}

View File

@@ -41,6 +41,14 @@ public class SettingsMenu extends JPanel {
});
this.add(scaleSlider);
this.add(new JLabel("Geschwindigkeitszunahme:"));
JCheckBox speedCheckbox = new JCheckBox("Aktiviert", Settings.shouldScaleSpeed);
speedCheckbox.addChangeListener(change -> {
Settings.shouldScaleSpeed = speedCheckbox.isSelected();
Settings.write();
});
this.add(speedCheckbox);
this.add(new JLabel("Schwierigkeit:"));
JComboBox<Difficulty> difficultySelector = new JComboBox<>(Difficulty.values());
difficultySelector.setSelectedItem(Settings.difficulty);
@@ -62,7 +70,7 @@ public class SettingsMenu extends JPanel {
public Component add(Component comp) {
comp.setBounds(scale(60), scale(20+23*this.getComponentCount()-labelAmount*10), scale(100), scale(20));
if (comp instanceof JLabel) {
comp.setBounds(scale(60), scale(50+23*(this.getComponentCount()-1)-labelAmount*10), scale(100), scale(5));
comp.setBounds(scale(60), scale(50+23*(this.getComponentCount()-1)-labelAmount*10), scale(100), scale(7));
labelAmount++;
}
if (comp instanceof JComponent) setFontScale((JComponent) comp);

View File

@@ -1,5 +1,6 @@
package eu.midnightdust.yaytris.ui;
import eu.midnightdust.yaytris.HighScores;
import eu.midnightdust.yaytris.Tetris;
import eu.midnightdust.yaytris.util.CatppuccinColor;
@@ -114,6 +115,20 @@ public class TetrisUI extends JFrame implements KeyListener {
this.repaint();
}
public void openHighscores(ActionEvent actionEvent) {
if (this.menuPanel != null) this.remove(menuPanel);
menuPanel = new HighScoreMenu(scale(170), scale(40), scale(220), scale(226), this);
menuPanel.setBackground(CatppuccinColor.BASE.getColor());
menuPanel.setBorder(new LineBorder(CatppuccinColor.SURFACE0.getColor(), scale(2)));
this.add(menuPanel);
this.repaint();
}
public void showHighscoreDialog(int score) {
String playerName = JOptionPane.showInputDialog(null, "Gib deinen Namen ein:", "Neuer Highscore!", JOptionPane.PLAIN_MESSAGE);
HighScores.addScore(playerName, score);
}
// Source: https://stackoverflow.com/a/19746437
private void setWindowPosition(JFrame window, int screen) {
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();

View File

@@ -4,6 +4,7 @@ import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
@@ -17,15 +18,8 @@ public class NightJson {
Class<?> jsonClass;
String fileLocation;
public NightJson(Class<?> jsonClass) {
this.jsonClass = jsonClass;
}
public NightJson(Class<?> jsonClass, String fileLocation) {
this(jsonClass);
this.fileLocation = fileLocation;
}
public void setFileLocation(String fileLocation) {
this.jsonClass = jsonClass;
this.fileLocation = fileLocation;
}
@@ -40,10 +34,11 @@ public class NightJson {
Field field = it.next();
jsonFile.write("\t");
if (field.getType() == Comment.class) {
jsonFile.write("// %s\n".formatted(((Comment) field.get(null)).commentString));
jsonFile.write(String.format("// %s\n", ((Comment) field.get(null)).commentString));
continue;
}
jsonFile.write((field.getType() == String.class || field.getType().isEnum() ? "\"%s\": \"%s\"" : "\"%s\": %s").formatted(field.getName(), field.get(null)));
jsonFile.write(String.format("\"%s\": ", field.getName()));
jsonFile.write(objToString(field.get(null), field.getType()));
jsonFile.write(it.hasNext() ? ",\n" : "\n");
}
jsonFile.write("}");
@@ -72,9 +67,13 @@ public class NightJson {
lastKey.set(s.replaceAll("([\":])", ""));
jsonKeyValuePairs.put(lastKey.get(), "");
}
else jsonKeyValuePairs.put(lastKey.get(), (jsonKeyValuePairs.get(
lastKey.get()).isEmpty() ? "" : jsonKeyValuePairs.get(lastKey.get()) + " "
) + s.replaceAll("([\",])", ""));
else {
String val = s.replaceAll("(\")", "");
if (val.endsWith(",")) val = val.substring(0, val.length()-1);
jsonKeyValuePairs.put(lastKey.get(), (jsonKeyValuePairs.get(
lastKey.get()).isEmpty() ? "" : jsonKeyValuePairs.get(lastKey.get()) + " "
) + val);
}
}
});
@@ -85,17 +84,7 @@ public class NightJson {
try { field = jsonClass.getField(key);
} catch (NoSuchFieldException e) {continue;}
Object value = switch (field.getType().getName()) {
case "byte" -> Byte.parseByte(currentString);
case "int" -> Integer.parseInt(currentString);
case "long" -> Long.parseLong(currentString);
case "float" -> Float.parseFloat(currentString);
case "double" -> Double.parseDouble(currentString);
default -> currentString;
};
if (field.getType().isEnum()) value = Arrays.stream(field.getType().getEnumConstants())
.filter(enumConstant -> Objects.equals(enumConstant.toString(), currentString)).findFirst().orElseThrow();
field.set(field, value);
field.set(field, stringToFieldObj(currentString, field));
}
jsonFile.close();
} catch (IOException | IllegalAccessException | NoSuchElementException e) {
@@ -104,6 +93,61 @@ public class NightJson {
}
}
private String objToString(Object value, Class<?> type) throws IllegalAccessException {
if (type == Map.class) {
StringBuilder mapPairs = new StringBuilder();
Map<?, ?> map = (Map<?, ?>) value;
Iterator<?> it = map.keySet().iterator();
if (it.hasNext()) mapPairs.append("{");
while (it.hasNext()) {
Object key = it.next();
Object val = map.get(key);
mapPairs.append(String.format("%s: %s", objToString(key, key.getClass()), objToString(val, val.getClass())));
if (it.hasNext()) mapPairs.append(",");
else mapPairs.append("}");
}
return mapPairs.toString();
}
return String.format(type == String.class || type.isEnum() ? "\"%s\"" : "%s", value);
}
private Object stringToFieldObj(String currentString, Field field) {
if (field.getType() == Map.class) {
Map<Object, Object> map = new HashMap<>();
Iterator<String> it = Arrays.stream(currentString.substring(1, currentString.length()-1).split(",")).iterator();
while (it.hasNext()) {
String pair = it.next();
if (!pair.contains(":")) break;
int semicolonPos = pair.indexOf(":");
Class<?> keyType = getPrimitiveType((Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]);
Class<?> valType = getPrimitiveType((Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[1]);
map.put(stringToObj(pair.substring(0, semicolonPos), keyType),
stringToObj(pair.substring(semicolonPos+2), valType));
}
return map;
}
return stringToObj(currentString, field.getType());
}
private Object stringToObj(String currentString, Class<?> type) {
switch (type.getName()) {
case "byte": return Byte.parseByte(currentString);
case "int": return Integer.parseInt(currentString);
case "long": return Long.parseLong(currentString);
case "float": return Float.parseFloat(currentString);
case "double": return Double.parseDouble(currentString);
case "boolean": return Boolean.parseBoolean(currentString);
}
if (type.isEnum()) return Arrays.stream(type.getEnumConstants())
.filter(enumConstant -> Objects.equals(enumConstant.toString(), currentString)).findFirst().orElseThrow();
else return currentString;
}
public static Class<?> getPrimitiveType(Class<?> rawType) {
try { return (Class<?>) rawType.getField("TYPE").get(null); // Tries to get primitive types from non-primitives (e.g. Boolean -> boolean)
} catch (NoSuchFieldException | IllegalAccessException ignored) { return rawType; }
}
public static class Comment {
final String commentString;
public Comment(String commentString) {

3
tetris_scores.json5 Normal file
View File

@@ -0,0 +1,3 @@
{
"scores": {"Martin": 5703}
}

View File

@@ -1,6 +1,7 @@
{
"musicVolume": 100,
"musicVolume": 19,
"soundVolume": 100,
"guiScale": 6.0,
"shouldScaleSpeed": true,
"difficulty": "Normal"
}