Compare commits

...

15 Commits

Author SHA1 Message Date
dustineversmann 1a32962ba8 feature(domain): Verwaltung der Highscores und der Durchschnittszeiten
Implementierung des HighscoreManagers, der die erzielten Highscores und die Durschnittszeit ausliest und in der entsprechenden Highscore-Datei speichert.
2025-01-06 22:34:08 +01:00
dustineversmann 57147ae818 feature(domain): Datenstruktur eines Highscores
Implementierung der Highscoredatenstruktur zur Nutzung durch den Highscoremanager
2025-01-06 22:25:13 +01:00
dustineversmann 981547383f feature:
Implementierung der Datenstruktur, die, welche die Hitorispieldaten und die Lösung enthalten
2025-01-06 22:11:30 +01:00
Dustin Fabian Eversmann e21e289f36 Merge pull request 'Update der main mit den von mir veränderten Daten nach Teamgespräch' (#2) from Löhle into main
Reviewed-on: #2
2025-01-06 19:53:59 +01:00
Leon Maximilian Löhle ae5fb68bd4 Musste neuen Commit machen, nachdem Datenstand nicht den gleichen Stand hatte wie main um einen Pull request in main zu gestatten 2025-01-06 19:42:02 +01:00
Leon Maximilian Löhle da6ffa8379 Merge branch 'main' into Löhle
# Conflicts:
#	src/main/java/de/deversmann/gui/GameView.java
#	src/main/java/de/deversmann/gui/HitoriApp.java
2025-01-06 19:31:31 +01:00
Leon Löhle a866e7f2bf Verbesserung der Gui und dem Ablauf nach Absprache mit Team kollegen 2025-01-06 18:17:04 +01:00
Leon Löhle f6cc17002c Erstellung einer Facade 2025-01-06 18:15:24 +01:00
Leon Löhle 7b77f7b381 Verbesserung der Zeiterfassung nach Team gespräch 2025-01-06 18:14:47 +01:00
dustineversmann 4394a89a9c feature: Trennung des Einlesens der Dateien von der Erstellung des Spielfelds 2025-01-02 18:03:21 +01:00
Leon Löhle e832fc5f6f Highsocre manager für benutztung von Highscores eingefügt 2025-01-01 13:17:07 +01:00
Leon Maximilian Löhle d1fcac31ca GUI ermöglicht nun Start und Zeitmessung. Noch keine Idee für korrekte Implementierung der Spielfeld auswahl 2024-12-27 15:02:48 +01:00
Leon Maximilian Löhle bb8a2ad265 Verbesserung der Spielfeld Klasse um Einsatz im Programm zu ermöglichen als auch die Anforderungen zu erfüllen zwecks Ausgabe und Farben 2024-12-21 14:04:30 +01:00
Leon Maximilian Löhle 53b012f6a7 Anpassung der Bezeichnung für CSV Dateien und erste CSV Datei einlesen und ausgeben 2024-12-12 21:24:54 +01:00
Leon Maximilian Löhle c0d96e2119 Erstellung einer ersten Version einer Stoppuhr für die Zeiterfassung 2024-12-10 15:55:55 +01:00
18 changed files with 812 additions and 6 deletions

View File

@ -0,0 +1,83 @@
7,1,2,9,12,15,8,11,11,9,11,14,13,6,3
2,3,8,1,2,11,10,9,5,8,14,3,12,13,15
4,14,13,9,4,15,9,10,12,6,5,3,11,5,12
15,9,5,6,10,15,1,15,8,3,5,4,6,2,8
5,11,7,9,15,1,4,3,8,1,9,2,10,13,2
15,15,10,3,1,14,8,12,11,1,9,8,2,7,2
10,7,7,12,9,3,15,2,5,2,10,5,1,7,4
3,8,9,14,1,6,12,4,15,2,13,11,5,10,11
8,6,7,15,11,4,5,11,2,10,3,13,8,12,9
2,2,3,3,4,13,5,6,5,11,5,15,8,9,12
2,15,15,11,13,7,6,5,3,13,8,10,5,1,11
12,5,11,13,13,2,2,8,8,4,10,9,3,2,5
1,13,8,2,1,7,11,4,9,15,4,12,9,3,10
13,10,12,5,15,3,2,7,13,14,12,12,9,11,6
7,12,4,8,14,10,13,13,7,4,2,6,15,15,11
//Lösung
1,4
1,6
1,8
1,11
2,1
2,3
2,9
2,12
2,14
3,5
3,7
3,11
3,15
4,1
4,3
4,6
4,9
4,13
5,4
5,10
5,15
6,2
6,5
6,7
6,9
6,11
6,13
7,1
7,3
7,6
7,8
7,12
7,14
8,5
8,9
8,15
9,3
9,8
9,13
10,1
10,4
10,7
10,9
10,11
11,3
11,10
11,13
11,15
12,2
12,5
12,7
12,9
12,11
12,14
13,1
13,6
13,8
13,13
14,3
14,5
14,9
14,12
15,1
15,7
15,10
15,14
Can't render this file because it has a wrong number of fields in line 17.

View File

@ -2,6 +2,7 @@ package de.deversmann;
public class Main {
public static void main(String[] args) {
System.out.println("Hello, World!");
de.deversmann.gui.HitoriApp app = new de.deversmann.gui.HitoriApp();
app.start();
}
}

View File

@ -0,0 +1,39 @@
package de.deversmann.domain;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
public class CSVReader {
/**
* Liest die CSV-Datei ein und gibt die enthaltenen Werte als zweidimensionales int-Array zurück.
*
* @param csvFile Pfad zur CSV-Datei
* @return zweidimensionales Array, das die Zahlen aus der CSV enthält
* @throws IOException wenn ein Fehler beim Lesen auftritt
*/
public int[][] readCsvToIntArray(String csvFile) throws IOException {
ArrayList<int[]> rows = new ArrayList<>();
try (BufferedReader br = new BufferedReader(new FileReader(csvFile))) {
String line;
while ((line = br.readLine()) != null) {
String[] tokens = line.split(",");
int[] row = new int[tokens.length];
for (int i = 0; i < tokens.length; i++) {
row[i] = Integer.parseInt(tokens[i].trim());
}
rows.add(row);
}
}
// ArrayList in 2D-Array umwandeln
int[][] result = new int[rows.size()][];
for (int i = 0; i < rows.size(); i++) {
result[i] = rows.get(i);
}
return result;
}
}

View File

@ -0,0 +1,31 @@
package de.deversmann.domain;
public class HighscoreEntry {
private final String playerName;
private final long timeSeconds;
private final int errorCount;
public HighscoreEntry(String playerName, long timeSeconds, int errorCount) {
this.playerName = playerName;
this.timeSeconds = timeSeconds;
this.errorCount = errorCount;
}
public String getPlayerName() {
return playerName;
}
public long getTimeSeconds() {
return timeSeconds;
}
public int getErrorCount() {
return errorCount;
}
@Override
public String toString() {
return playerName + " - " + timeSeconds + "s (Fehler: " + errorCount + ")";
}
}

View File

@ -0,0 +1,102 @@
package de.deversmann.domain;
import java.io.*;
import java.util.*;
public class HighscoreManager {
private Map<String, List<HighscoreEntry>> highscoreMap;
public HighscoreManager() {
this.highscoreMap = new HashMap<>();
}
public void addHighscore(String puzzleName, String playerName, long timeSeconds, int errorCount) {
highscoreMap.putIfAbsent(puzzleName, new ArrayList<>());
highscoreMap.get(puzzleName).add(new HighscoreEntry(playerName, timeSeconds, errorCount));
}
public List<HighscoreEntry> getHighscores(String puzzleName) {
return highscoreMap.getOrDefault(puzzleName, Collections.emptyList());
}
public double getAverageTime(String puzzleName) {
List<HighscoreEntry> entries = highscoreMap.get(puzzleName);
if (entries == null || entries.isEmpty()) {
return -1;
}
long sum = 0;
for (HighscoreEntry e : entries) {
sum += e.getTimeSeconds();
}
return (double) sum / entries.size();
}
public void saveSinglePuzzle(String puzzleName, String filename) throws IOException {
List<HighscoreEntry> entries = getHighscores(puzzleName);
entries.sort(Comparator
.comparingInt(HighscoreEntry::getErrorCount)
.thenComparingLong(HighscoreEntry::getTimeSeconds));
double avgTime = getAverageTime(puzzleName);
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) {
if (avgTime < 0) {
writer.write("Durchschnittszeit: Keine Einträge");
} else {
writer.write(String.format("Durchschnittszeit: %.2f s", avgTime));
}
writer.newLine();
writer.newLine();
writer.write(String.format("%-6s | %-5s | %-6s | %-15s",
"Platz", "Zeit", "Fehler", "Name"));
writer.newLine();
writer.write("------------------------------------------------------------");
writer.newLine();
int place = 1;
for (HighscoreEntry e : entries) {
writer.write(String.format("%-6s | %-5d | %-6d | %-15s",
place + ".",
e.getTimeSeconds(),
e.getErrorCount(),
e.getPlayerName()));
writer.newLine();
place++;
}
}
}
public void loadSinglePuzzle(String puzzleName, String filename) throws IOException {
File file = new File(filename);
if (!file.exists()) {
System.out.println("Highscore-Datei " + filename + " existiert nicht; wird neu erstellt.");
return;
}
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
// Durchschnittszeit und Header überspringen
reader.readLine();
reader.readLine();
reader.readLine();
reader.readLine();
String line;
while ((line = reader.readLine()) != null) {
String[] parts = line.split("\\|");
if (parts.length < 4) continue;
String timeStr = parts[1].trim();
String errorStr = parts[2].trim();
String nameStr = parts[3].trim();
long timeSeconds = Long.parseLong(timeStr);
int errorCount = Integer.parseInt(errorStr);
addHighscore(puzzleName, nameStr, timeSeconds, errorCount);
}
}
}
}

View File

@ -0,0 +1,22 @@
package de.deversmann.domain;
import java.util.List;
public class PuzzleData {
private final int[][] puzzle;
private final List<int[]> solutionCoordinates;
public PuzzleData(int[][] puzzle, List<int[]> solutionCoordinates) {
this.puzzle = puzzle;
this.solutionCoordinates = solutionCoordinates;
}
public int[][] getPuzzle() {
return puzzle;
}
public List<int[]> getSolutionCoordinates() {
return solutionCoordinates;
}
}

View File

@ -1,4 +1,110 @@
package de.deversmann.domain;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Random;
public class Spielfeld {
private int[][] feld;
private String[][] zustand; // "grau", "schwarz", "weiß"
public Spielfeld() {
}
public void ladeSpielfeld(String dateiPfad) {
try (BufferedReader br = new BufferedReader(new FileReader(dateiPfad))) {
String line;
int zeilenZaehler = 0;
while ((line = br.readLine()) != null) {
zeilenZaehler++;
}
br.close();
feld = new int[zeilenZaehler][];
zustand = new String[zeilenZaehler][];
try (BufferedReader br2 = new BufferedReader(new FileReader(dateiPfad))) {
int row = 0;
while ((line = br2.readLine()) != null) {
String[] werte = line.split(",");
feld[row] = new int[werte.length];
zustand[row] = new String[werte.length];
for (int col = 0; col < werte.length; col++) {
feld[row][col] = Integer.parseInt(werte[col]);
zustand[row][col] = "grau";
}
row++;
}
}
} catch (IOException e) {
System.err.println("Fehler beim Laden des Spielfelds: " + e.getMessage());
}
}
public void ladeZufaelligesSpielfeld(String ordnerPfad) {
File ordner = new File(ordnerPfad);
File[] dateien = ordner.listFiles((dir, name) -> name.endsWith(".csv"));
if (dateien != null && dateien.length > 0) {
Random random = new Random();
File zufallsDatei = dateien[random.nextInt(dateien.length)];
ladeSpielfeld(zufallsDatei.getAbsolutePath());
} else {
System.err.println("Keine CSV-Dateien im Verzeichnis gefunden!");
}
}
public void setzeZustand(int x, int y, String neuerZustand) {
if (istImBereich(x, y)) {
zustand[x][y] = neuerZustand;
} else {
System.err.println("Koordinaten außerhalb des Spielfelds!");
}
}
public String getZustand(int x, int y) {
if (istImBereich(x, y)) {
return zustand[x][y];
} else {
System.err.println("Koordinaten außerhalb des Spielfelds!");
return null;
}
}
public int getWert(int x, int y) {
if (istImBereich(x, y)) {
return feld[x][y];
} else {
System.err.println("Koordinaten außerhalb des Spielfelds!");
return -1;
}
}
public void reset() {
for (int i = 0; i < zustand.length; i++) {
for (int j = 0; j < zustand[i].length; j++) {
zustand[i][j] = "grau";
}
}
}
public int getBreite() {
return feld[0].length;
}
public int getHoehe() {
return feld.length;
}
private boolean istImBereich(int x, int y) {
return x >= 0 && x < feld.length && y >= 0 && y < feld[0].length;
}
}

View File

@ -0,0 +1,37 @@
package de.deversmann.domain;
public class Zeiterfassung {
private long startTime;
private long endTime;
private boolean isRunning;
public Zeiterfassung() {
this.isRunning = false;
}
public void start() {
if (!isRunning) {
startTime = System.currentTimeMillis();
isRunning = true;
System.out.println("Zeiterfassung gestartet");
}
}
public void stop() {
if (isRunning) {
endTime = System.currentTimeMillis();
isRunning = false;
System.out.println("Zeiterfassung gestoppt, diff=" + getElapsedTimeInSeconds() + "s");
}
}
public long getElapsedTimeInSeconds() {
if (isRunning) {
long current = System.currentTimeMillis();
return (current - startTime) / 1000;
} else {
return (endTime - startTime) / 1000;
}
}
}

View File

@ -1,4 +1,4 @@
package de.deversmann.facade;
public class Facade {
}
}

View File

@ -0,0 +1,235 @@
package de.deversmann.gui;
import de.deversmann.facade.Facade;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.util.List;
import java.util.Random;
public class GameView extends JFrame {
private final Facade facade;
private SpielfeldGUI spielfeldGUI;
private JComboBox<String> cbSpielfelder;
private JButton btnLoad;
private JButton btnRandom;
private JButton btnCheckSolution;
private JButton btnShowSolution;
private JButton btnReset;
private JLabel timeLabel;
private Timer timer;
private final String[] spielfelder = {
"4x4.csv",
"5x5.csv",
"8x8_leicht.csv",
"8x8_medium.csv",
"10x10.csv",
"15x15.csv"
};
public GameView(Facade facade) {
this.facade = facade;
setTitle("Hitori App");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(800, 700);
initComponents();
startTimer();
setVisible(true);
}
private void initComponents() {
JPanel controlPanel = new JPanel();
cbSpielfelder = new JComboBox<>(spielfelder);
btnLoad = new JButton("Spielfeld laden");
btnRandom = new JButton("Zufällig");
btnCheckSolution = new JButton("Lösung prüfen");
btnShowSolution = new JButton("Lösung anzeigen");
btnReset = new JButton("Reset");
timeLabel = new JLabel("Time: 0s");
btnLoad.addActionListener(e -> {
String selectedFile = (String) cbSpielfelder.getSelectedItem();
if (selectedFile != null) {
ladeSpielfeld("src/Spielfelder/" + selectedFile);
}
});
btnRandom.addActionListener(e -> {
Random rand = new Random();
int idx = rand.nextInt(spielfelder.length);
String randomField = spielfelder[idx];
cbSpielfelder.setSelectedItem(randomField);
ladeSpielfeld("src/Spielfelder/" + randomField);
});
btnCheckSolution.addActionListener(e -> checkSolution());
btnShowSolution.addActionListener(e -> showSolution());
btnReset.addActionListener(e -> resetField());
controlPanel.add(new JLabel("Wähle ein Spielfeld:"));
controlPanel.add(cbSpielfelder);
controlPanel.add(btnLoad);
controlPanel.add(btnRandom);
controlPanel.add(btnCheckSolution);
controlPanel.add(btnShowSolution);
controlPanel.add(btnReset);
controlPanel.add(timeLabel);
getContentPane().setLayout(new BorderLayout());
getContentPane().add(controlPanel, BorderLayout.NORTH);
spielfeldGUI = new SpielfeldGUI(1, 1);
getContentPane().add(spielfeldGUI, BorderLayout.CENTER);
}
private void startTimer() {
timer = new Timer(1000, e -> {
long seconds = facade.getElapsedTimeSoFar();
timeLabel.setText("Time: " + seconds + "s");
});
timer.start();
}
private void ladeSpielfeld(String pfad) {
try {
facade.ladeSpielfeld(pfad);
facade.loadCurrentPuzzleHighscoreFromFile();
int[][] feld = facade.getAktuellesPuzzle();
if (feld != null) {
spielfeldGUI.updateGrid(feld);
timeLabel.setText("Time: 0s");
}
} catch (IOException ex) {
JOptionPane.showMessageDialog(this,
"Fehler beim Laden: " + ex.getMessage(),
"Fehler",
JOptionPane.ERROR_MESSAGE
);
}
}
private void checkSolution() {
var blackCells = spielfeldGUI.getBlackCells();
var solutionCoordinates = facade.getLoesungsKoordinaten();
if (solutionCoordinates == null) {
JOptionPane.showMessageDialog(this, "Kein Puzzle geladen!", "Fehler", JOptionPane.ERROR_MESSAGE);
return;
}
Set<String> solutionSet = new HashSet<>();
for (int[] sol : solutionCoordinates) {
solutionSet.add((sol[0] - 1) + "," + (sol[1] - 1));
}
int errorsThisCheck = 0;
for (int[] bc : blackCells) {
String bcStr = bc[0] + "," + bc[1];
if (!solutionSet.contains(bcStr)) {
spielfeldGUI.markCellAsError(bc[0], bc[1]);
facade.incrementErrorCount();
errorsThisCheck++;
}
}
if (errorsThisCheck > 0) {
JOptionPane.showMessageDialog(this,
"Noch nicht korrekt! " + errorsThisCheck + " falsche Felder markiert.",
"Ergebnis",
JOptionPane.WARNING_MESSAGE
);
return;
}
boolean allSet = true;
for (String sol : solutionSet) {
if (!containsCell(blackCells, sol)) {
allSet = false;
break;
}
}
if (!allSet) {
JOptionPane.showMessageDialog(this,
"Nicht alle richtigen Felder sind schwarz!",
"Ergebnis",
JOptionPane.WARNING_MESSAGE
);
return;
}
long finalTime = facade.stopZeiterfassung();
timeLabel.setText("Time: " + finalTime + "s");
String playerName = JOptionPane.showInputDialog(
this,
"Gratulation, richtig gelöst!\nBitte Namen eingeben:",
"Name eingeben",
JOptionPane.QUESTION_MESSAGE
);
if (playerName == null || playerName.isBlank()) {
playerName = "Unbekannt";
}
int totalErrors = facade.getCurrentErrorCount();
facade.addHighscoreForCurrentPuzzle(playerName, finalTime, totalErrors);
try {
facade.saveCurrentPuzzleHighscoreToFile();
} catch (IOException e) {
System.out.println("Konnte Puzzle-spezifischen Highscore nicht speichern: " + e.getMessage());
}
double avgTime = facade.getAverageTimeForCurrentPuzzle();
StringBuilder sb = new StringBuilder();
sb.append("Spiel gelöst!\n");
sb.append("Deine Zeit: ").append(finalTime).append("s\n");
sb.append("Fehler: ").append(totalErrors).append("\n");
if (avgTime < 0) {
sb.append("Durchschnittszeit: Keine Einträge\n");
} else {
sb.append(String.format("Durchschnittszeit: %.2f s\n", avgTime));
}
JOptionPane.showMessageDialog(this, sb.toString(), "Ergebnis", JOptionPane.INFORMATION_MESSAGE);
}
private boolean containsCell(List<int[]> blackCells, String sol) {
String[] parts = sol.split(",");
int rr = Integer.parseInt(parts[0]);
int cc = Integer.parseInt(parts[1]);
for (int[] bc : blackCells) {
if (bc[0] == rr && bc[1] == cc) {
return true;
}
}
return false;
}
private void showSolution() {
var feld = facade.getAktuellesPuzzle();
if (feld == null) return;
spielfeldGUI.updateGrid(feld);
var solution = facade.getLoesungsKoordinaten();
if (solution != null) {
for (int[] coord : solution) {
spielfeldGUI.setCellBlack(coord[0] - 1, coord[1] - 1);
}
}
spielfeldGUI.setAllNonBlackToWhite();
}
private void resetField() {
var feld = facade.getAktuellesPuzzle();
if (feld != null) {
spielfeldGUI.updateGrid(feld);
timeLabel.setText("Time: 0s");
}
}
}

View File

@ -0,0 +1,18 @@
package de.deversmann.gui;
import de.deversmann.facade.Facade;
public class HitoriApp {
private Facade facade;
public HitoriApp() {
facade = new Facade();
}
public void start() {
GameView view = new GameView(facade);
view.setVisible(true);
}
}

View File

@ -1,4 +0,0 @@
package de.deversmann.gui;
public class Spielfeld {
}

View File

@ -0,0 +1,136 @@
package de.deversmann.gui;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.*;
import java.util.List;
public class SpielfeldGUI extends JPanel {
private JLabel[][] grid;
private Set<String> errorCells = new HashSet<>();
public SpielfeldGUI(int rows, int cols) {
setLayout(new GridLayout(rows, cols));
grid = new JLabel[rows][cols];
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
JLabel label = createCellLabel("-", r, c);
grid[r][c] = label;
add(label);
}
}
}
public void updateGrid(int[][] feld) {
if (feld == null) {
return;
}
removeAll();
int rows = feld.length;
int cols = feld[0].length;
setLayout(new GridLayout(rows, cols));
grid = new JLabel[rows][cols];
errorCells.clear();
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
String text = String.valueOf(feld[r][c]);
JLabel label = createCellLabel(text, r, c);
grid[r][c] = label;
add(label);
}
}
revalidate();
repaint();
}
public void setAllNonBlackToWhite() {
if (grid == null) return;
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[r].length; c++) {
JLabel lbl = grid[r][c];
if (lbl != null) {
if (!lbl.getBackground().equals(Color.BLACK)) {
lbl.setBackground(Color.WHITE);
lbl.setForeground(Color.BLACK);
clearErrorMark(r, c);
}
}
}
}
}
public void markCellAsError(int row, int col) {
if (!isValidCell(row, col)) return;
errorCells.add(row + "," + col);
grid[row][col].setBorder(BorderFactory.createLineBorder(Color.RED, 3));
}
public void clearErrorMark(int row, int col) {
errorCells.remove(row + "," + col);
if (!isValidCell(row, col)) return;
grid[row][col].setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY, 1));
}
public List<int[]> getBlackCells() {
List<int[]> black = new ArrayList<>();
if (grid == null) return black;
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[r].length; c++) {
JLabel lbl = grid[r][c];
if (lbl != null && lbl.getBackground().equals(Color.BLACK)) {
black.add(new int[]{r, c});
}
}
}
return black;
}
public void setCellBlack(int row, int col) {
if (!isValidCell(row, col)) return;
JLabel lbl = grid[row][col];
lbl.setBackground(Color.BLACK);
lbl.setForeground(Color.WHITE);
clearErrorMark(row, col);
}
private JLabel createCellLabel(String text, int row, int col) {
JLabel label = new JLabel(text, SwingConstants.CENTER);
label.setOpaque(true);
label.setBackground(Color.GRAY);
label.setForeground(Color.BLACK);
label.setFont(new Font("Arial", Font.BOLD, 24));
label.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY, 1));
label.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
toggleColor(label, row, col);
}
});
return label;
}
private void toggleColor(JLabel label, int row, int col) {
if (label.getBackground().equals(Color.GRAY) || label.getBackground().equals(Color.WHITE)) {
label.setBackground(Color.BLACK);
label.setForeground(Color.WHITE);
} else {
label.setBackground(Color.WHITE);
label.setForeground(Color.BLACK);
}
if (errorCells.contains(row + "," + col)) {
clearErrorMark(row, col);
}
}
private boolean isValidCell(int r, int c) {
if (grid == null) return false;
return (r >= 0 && r < grid.length && c >= 0 && c < grid[r].length);
}
}