ApplicationController.java
package ch.ladestation.connectncharge.controller;
import ch.ladestation.connectncharge.model.game.gamelogic.Edge;
import ch.ladestation.connectncharge.model.game.gamelogic.Game;
import ch.ladestation.connectncharge.model.game.gamelogic.Hint;
import ch.ladestation.connectncharge.model.game.gamelogic.Node;
import ch.ladestation.connectncharge.pui.GamePUI;
import ch.ladestation.connectncharge.services.file.TextFileEditor;
import ch.ladestation.connectncharge.util.mvcbase.ControllerBase;
import ch.ladestation.connectncharge.util.mvcbase.ObservableArray;
import com.github.mbelling.ws281x.Color;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
/**
* This Class is the controller of the element with the components.
*/
public class ApplicationController extends ControllerBase<Game> {
private Map<Integer, List<Object>> levels;
private int currentLevel = 0;
private GamePUI gamePUI;
private boolean isToBeRemoved = false;
private ScheduledExecutorService blinkingEdgeScheduler;
/**
* This is the constructor of the ApplicationController
*
* @param model
*/
public ApplicationController(Game model) {
super(model);
loadLevels();
model.activatedEdges.onChange((oldValue, newValue) -> {
if (!model.gameStarted.getValue()) {
return;
}
updateScore(sumEdgeCost(newValue));
checkScore(sumEdgeCost(newValue));
syncSet(model.hasCycle, hasCycle(newValue));
});
model.hasCycle.onChange((oldValue, newValue) -> {
if (newValue) {
addHint(Hint.HINT_CYCLE);
} else {
removeHint(Hint.HINT_CYCLE);
}
});
model.isTippOn.onChange(((oldValue, newValue) -> {
if (newValue) {
addHint(isToBeRemoved ? Hint.HINT_REMOVE_EDGE : Hint.HINT_PICK_EDGE);
} else if (oldValue) {
removeHint(isToBeRemoved ? Hint.HINT_REMOVE_EDGE : Hint.HINT_PICK_EDGE);
}
}));
model.isCountdownFinished.onChange((oldValue, newValue) -> {
if (!oldValue && newValue) {
instanceTerminals();
stopIgnoringInputs();
}
});
model.isFinished.onChange(((oldValue, newValue) -> {
if (!oldValue && newValue) {
startIgnoringInputs();
} else if (oldValue && !newValue) {
stopIgnoringInputs();
}
}));
model.activeHints.onChange((oldValue, newValue) -> {
if (!Arrays.stream(model.activeHints.getValues()).toList().isEmpty()) {
syncSet(model.activeHint,
Arrays.stream(model.activeHints.getValues())
.min(Comparator.comparingInt(Hint::getPriority))
.get());
} else {
syncSet(model.activeHint, Hint.HINT_EMPTY_HINT);
}
});
}
public static int sumEdgeCost(Edge[] arr) {
return Arrays.stream(arr).mapToInt(Edge::getCost).sum();
}
public static int sumEdgeCost(ObservableArray<Edge> arr) {
return sumEdgeCost(get(arr));
}
/**
* This method checks if the edge array has a cycle.
*
* @param edgeArray
* @return boolean
*/
public static boolean hasCycle(Edge[] edgeArray) {
// Create an adjacency list to store the nodes and their neighbors
Map<Node, List<Node>> adjList = new HashMap<>();
// Create a list of selected edges
List<Edge> selectedEdges = Arrays.stream(edgeArray).toList();
// If there are less than 2 selected edges, no cycle can be formed
if (selectedEdges.size() < 2) {
return false;
}
// Create the adjacency list by adding the nodes and their neighbors from the
// selected edges
for (Edge edge : selectedEdges) {
Node node1 = edge.getFromNode();
Node node2 = edge.getToNode();
if (!adjList.containsKey(node1)) {
adjList.put(node1, new ArrayList<>());
}
if (!adjList.containsKey(node2)) {
adjList.put(node2, new ArrayList<>());
}
adjList.get(node1).add(node2);
adjList.get(node2).add(node1);
}
// Create a set to keep track of visited nodes and a map to keep track of their
// parent node in the DFS tree
Set<Node> visited = new HashSet<>();
Map<Node, Node> parent = new HashMap<>();
//get all the edges that haven't been visited yet
var islands =
selectedEdges.stream().flatMap(e -> Stream.of(e.getFromNode(), e.getToNode())).distinct().toList();
while (islands.size() > 0) {
// Create a stack to perform depth-first search starting from the first node in
// the first selected edge
Stack<Node> stack = new Stack<>();
Node startNode = islands.get(0);
stack.push(startNode);
parent.put(startNode, null);
while (!stack.empty()) {
Node currNode = stack.pop();
visited.add(currNode);
List<Node> neighbors = adjList.get(currNode);
for (Node neighbor : neighbors) {
// If the neighbor node has not been visited, add it to the stack and set its
// parent to the current node
if (!visited.contains(neighbor)) {
stack.push(neighbor);
parent.put(neighbor, currNode);
} else if (parent.get(currNode) != neighbor) {
return true;
}
}
}
islands = selectedEdges.stream().flatMap(e -> Stream.of(e.getFromNode(), e.getToNode()))
.filter(n -> !visited.contains(n)).distinct().toList();
}
// No cycle is formed
return false;
}
public static boolean allTerminalsConnected(Edge[] activatedEdges, Node[] terminals) {
Set<Node> visitedNodes = new HashSet<>();
List<Edge> edges = Arrays.asList(activatedEdges);
if (!edges.isEmpty()) {
Edge firstEdge = edges.get(0);
Node startNode = firstEdge.getFromNode();
if (startNode == null) {
startNode = firstEdge.getToNode();
}
Stack<Node> stack = new Stack<>();
stack.push(startNode);
while (!stack.isEmpty()) {
Node currentNode = stack.pop();
visitedNodes.add(currentNode);
boolean allTerminalsConnected = true;
for (Node terminal : terminals) {
if (!visitedNodes.contains(terminal)) {
allTerminalsConnected = false;
break;
}
}
if (allTerminalsConnected) {
return true;
}
for (Edge edge : edges) {
if (edge.getFromNode() == currentNode && !visitedNodes.contains(edge.getToNode())) {
stack.push(edge.getToNode());
} else if (edge.getToNode() == currentNode && !visitedNodes.contains(edge.getFromNode())) {
stack.push(edge.getFromNode());
}
}
}
}
return false;
}
private void startIgnoringInputs() {
model.ignoringInputs = true;
}
private void stopIgnoringInputs() {
model.ignoringInputs = false;
}
/**
* This method is the entry point for the state machine (for the game)
* It starts a new round.
*/
public void startRound() {
increaseCurrentLevel();
loadCurrentLevel();
syncSet(model.isCountdownFinished, false);
syncSet(model.isFinished, false);
startBlinkingEdge((Edge) gamePUI.lookUpSegmentIdToSegment(90));
}
/**
* This method is a setter for the gamePUI.
*
* @param gamePUI
*/
public void setGPUI(GamePUI gamePUI) {
this.gamePUI = gamePUI;
}
/**
* This method loads all the levels from the text files in a {@code Map<Integer, List<Object>>}.
*/
private void loadLevels() {
levels = TextFileEditor.readLevels();
}
/**
* This method loads the current round.
*/
private void loadCurrentLevel() {
List<Object> level = levels.get(currentLevel);
List<List<Integer>> solution = (List<List<Integer>>) level.get(1);
var solutionEdges =
solution.stream().map((sol) -> gamePUI.lookUpEdge(sol.get(0), sol.get(1))).toArray(Edge[]::new);
setSolution(solutionEdges);
}
private void instanceTerminals() {
List<Object> level = levels.get(currentLevel);
List<Integer> terminals = (List<Integer>) level.get(0);
var terminalNodes =
terminals.stream().map(gamePUI::lookUpSegmentIdToSegment).map(seg -> (Node) seg).toArray(Node[]::new);
setTerminals(terminalNodes);
}
/**
* This setter method declares the attribute isCountdownFinished to true.
*/
public void setCountdownFinished() {
syncSet(model.isCountdownFinished, true);
}
private void increaseCurrentLevel() {
if (currentLevel + 1 > model.MAX_LEVEL) {
currentLevel = 1;
} else {
currentLevel++;
}
}
/**
* This method is called by {@link GamePUI} every time an edge is pressed.
* <p>
* It is arguably the most important method because it triggers all logic
* calculations.
*
* @param edge
*/
public void edgePressed(Edge edge) {
async(() -> {
if (!get(model.gameStarted)) {
if (edge == model.blinkingEdge) {
stopBlinkingTheEdge();
syncSet(model.gameStarted, true);
startIgnoringInputs();
}
return;
}
if (model.ignoringInputs || edge == null) {
return;
}
if (get(model.isTippOn) && edge.equals(model.tippEdge) && !isToBeRemoved) {
deactivateEdge(edge);
}
removeTippEdge();
toggleEdge(edge);
});
}
private void stopBlinkingTheEdge() {
syncSet(model.isEdgeBlinking, false);
blinkingEdgeScheduler.shutdown();
async(() -> model.blinkingEdge = null);
}
/**
* This method initialize the attribute gameStarted to the param.
*
* @param state
*/
public void setGameStarted(boolean state) {
syncSet(model.gameStarted, state);
}
private void toggleEdge(Edge edge) {
if (edge != null) {
if (!edge.isOn()) {
activateEdge(edge);
} else {
deactivateEdge(edge);
}
}
}
private void activateEdge(Edge edge) {
syncAdd(model.activatedEdges, edge);
}
private void deactivateEdge(Edge edge) {
syncRemove(model.activatedEdges, edge);
}
private void deactivateAllEdges() {
syncSet(model.activatedEdges, new Edge[0]);
}
private void deactivateAllNodes() {
syncSet(model.terminals, new Node[0]);
}
/**
* This method makes the given edge blinking.
*
* @param edg
*/
private void startBlinkingEdge(Edge edg) {
model.blinkingEdge = edg;
blinkingEdgeScheduler = Executors.newScheduledThreadPool(1);
blinkingEdgeScheduler.scheduleAtFixedRate(() -> toggleValue(model.isEdgeBlinking), 0, 1, TimeUnit.SECONDS);
}
/**
* This method updates the attribute currentScore.
*
* @param score
*/
public void updateScore(int score) {
syncSet(model.currentScore, score);
}
/**
* This method checks the score for the correct solution.
*
* @param score
*/
public void checkScore(int score) {
int solutionScore = sumEdgeCost(model.solution);
if (allTerminalsConnected(get(model.activatedEdges), get(model.terminals))) {
if (score <= solutionScore) {
finishGame();
} else {
addHint(Hint.HINT_SOLUTION_NOT_FOUND);
}
} else {
removeHint(Hint.HINT_SOLUTION_NOT_FOUND);
}
}
/**
* This method sets the attribute terminals.
*
* @param terms
*/
public void setTerminals(Node[] terms) {
syncSet(model.terminals, terms);
}
/**
* This method sets the attribute solution.
*
* @param edges
*/
public void setSolution(Edge[] edges) {
syncSet(model.solution, edges);
}
public void handleTipp() {
computeTippEdge();
}
/**
* This method computes the tipp edge.
*/
public void computeTippEdge() {
List<Edge> edgesToSelect;
List<Edge> edgesToRemove;
edgesToSelect = Arrays.stream(model.solution.getValues())
.filter(solEdge -> !Arrays.stream(model.activatedEdges.getValues())
.toList().contains(solEdge)).toList();
edgesToRemove = Arrays.stream(model.activatedEdges.getValues())
.filter((activatedEdge) -> !Arrays.stream(model.solution.getValues()).toList().contains(activatedEdge))
.toList();
Edge tippEdge;
if (!edgesToSelect.isEmpty()) {
tippEdge = getRandomEdge(edgesToSelect);
isToBeRemoved = false;
} else {
tippEdge = getRandomEdge(edgesToRemove);
isToBeRemoved = true;
}
setTippEdge(tippEdge);
}
public void setTippEdge(Edge edge) {
model.tippEdge = edge;
model.tippEdge.setColor(isToBeRemoved ? Hint.HINT_REMOVE_EDGE.getColor() : Hint.HINT_PICK_EDGE.getColor());
syncSet(model.isTippOn, true);
}
private Edge getRandomEdge(List<Edge> edges) {
return edges.stream().skip(new Random().nextInt(edges.size())).findFirst().get();
}
/**
* This method remove this tip edge.
*/
public void removeTippEdge() {
syncSet(model.isTippOn, false);
if (model.tippEdge != null) {
model.tippEdge.setColor(Color.GREEN);
}
}
public void finishGame() {
syncSet(model.isFinished, true);
}
/**
* This method starts the game again.
*/
public void playAgain() {
quitGame();
startRound();
}
public void quitGame() {
deactivateAllEdges();
deactivateAllNodes();
stopBlinkingTheEdge();
syncSet(model.gameStarted, false);
}
public void setEndTime(String endTime) {
log.info("setting endTime={}", endTime);
model.endTime.set(endTime);
}
public void addHint(Hint hint) {
log.trace("adding hint={}", hint);
syncAddUnique(model.activeHints, hint);
}
public synchronized void removeHint(Hint hint) {
log.trace("removing hint={}", hint);
syncRemove(model.activeHints, hint);
}
public void toggleMute() {
syncSet(model.muted, !get(model.muted));
}
}