All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.hamstersimulator.objectsfirst.external.model.HamsterGame Maven / Gradle / Ivy

The newest version!
package de.hamstersimulator.objectsfirst.external.model;

import static de.hamstersimulator.objectsfirst.utils.Preconditions.checkNotNull;
import static de.hamstersimulator.objectsfirst.utils.Preconditions.checkState;

import java.io.InputStream;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
import java.util.function.Function;

import de.hamstersimulator.objectsfirst.adapter.HamsterGameViewModel;
import de.hamstersimulator.objectsfirst.adapter.InputInterface;
import de.hamstersimulator.objectsfirst.commands.Command;
import de.hamstersimulator.objectsfirst.commands.CommandSpecification;
import de.hamstersimulator.objectsfirst.commands.CompositeCommand;
import de.hamstersimulator.objectsfirst.commands.EditCommandStack;
import de.hamstersimulator.objectsfirst.commands.GameCommandStack;
import de.hamstersimulator.objectsfirst.datatypes.Mode;
import de.hamstersimulator.objectsfirst.exceptions.GameAbortedException;
import de.hamstersimulator.objectsfirst.internal.model.GameLog;
import de.hamstersimulator.objectsfirst.internal.model.hamster.command.specification.AbstractHamsterCommandSpecification;
import de.hamstersimulator.objectsfirst.utils.Preconditions;

/**
 * A class representing an instance of a hamster game. A hamster game consists of a territory, on
 * which the game takes place, a game log for messages, and a command stack for keeping track of the
 * game's history.
 * @author Steffen Becker
 *
 */
public class HamsterGame {

    /**
     * Maximum number for the game speed of the hamster game.
     */
    private static final double MAX_SPEED = 10.0;

    /**
     * Constant containing the filename of the default territory file.
     */
    private static final String DEFAULT_HAMSTER_TERRITORY = "/_territories/example01.ter";

    private final HamsterGameViewModel adapter;

    /**
     * Game log object used to print log messages from the game and write commands from hamsters.
     */
    private final GameLog log = new GameLog();

    /**
     * Game command stack object used to execute game commands, i.e., commands coming from
     * the hamster objects on the territory during the simulation run.
     */
    private final GameCommandStack commandStack = new GameCommandStack();

    /**
     * The territory object on which the instance of this game takes place.
     */
    private final Territory territory = new Territory(this);

    private final ExecutorService executorService;


    /**
     * Initialize a hamster game. The hamster game is in mode Initialized
     * in the beginning and needs to have its territory defined or loaded
     * and the game started to be useful.
     *
     * The default constructor initializes the hamster game with a dummy IO
     * interface, i.e., reading values is not allowed and exceptions from
     * the hamster game will be rethrown.
     */
    public HamsterGame() {
        this(Collections.emptySet());
    }

    /**
     * Initialize a hamster game. The hamster game is in mode Initialized
     * in the beginning and needs to have its territory defined or loaded
     * and the game started to be useful.
     *
     * This constructor initializes the hamster game with the given
     * object to handle read commands of the hamster (e.g., from a GUI
     * or from Mockups) and to handle any exception thrown from the user
     * defined hamster game.
     *
     * @param newInputInterfaces The input interfaces this game should use
     * for reading values from the user and to handle exceptions.
     */
    public HamsterGame(final Set newInputInterfaces) {
        super();
        checkNotNull(newInputInterfaces);

        this.executorService = Executors.newCachedThreadPool(r -> {
            final Thread t = Executors.defaultThreadFactory().newThread(r);
            t.setDaemon(true);
            return t;
        });
        this.adapter = new HamsterGameViewModel(this.territory.getInternalTerritory(), this.commandStack, this.log,
                newInputInterfaces);
    }

    /*@
     @ requires gameSpeed >= 0 && gameSpeed <= MAX_SPEED;
     @ ensures getSpeed() == gameSpeed;
     @*/
    /**
     * Set the speed of the hamster game. Valid values are in the range from
     * 0.0 to 10.0,
     * where 0.0 is slow and 10.0 is fast.
     * @param gameSpeed The new game speed's delay. Has to be greater or equal 0.0 and
     *                  less than or equal 10.0.
     */
    public void setSpeed(final double gameSpeed) {
        Preconditions.checkArgument(gameSpeed >= 0.0 && gameSpeed <= MAX_SPEED);
        this.commandStack.setSpeed(gameSpeed);
    }

    /*@
     @ requires true;
     @ ensures gameSpeed >= 0 && gameSpeed <= MAX_SPEED;
     @*/
    /**
     * Gets the speed of the hamster game.
     * @return The hamster game's current speed. Is greater or equal 0.0 and
     *         less than or equal 10.0;
     */
    public double getSpeed() {
        return this.commandStack.speedProperty().get();
    }

    /**
     * Getter for the HamsterGameViewModel object of this game. Cannot be null.
     * The adapter can be used to connect a UI or a test framework to this game.
     * @return The adapter object of this game
     */
    public /*@ pure @*/ HamsterGameViewModel getModelViewAdapter() {
        return this.adapter;
    }

    /**
     * Getter for the territory object of this game. Cannot be null.
     * @return The territory object of this game.
     */
    public /*@ pure @*/ Territory getTerritory() {
        return territory;
    }

    /**
     * Initialize a new hamster game by loading the default territory.
     *
     * Warning: this executes a hard reset which cannot be undone
     */
    public void initialize() {
        try {
            initialize(getClass().getResourceAsStream(DEFAULT_HAMSTER_TERRITORY));
        } catch (final RuntimeException e) {
            e.printStackTrace();
            throw new IllegalStateException("Unable to load default territory. "
                    + "This should not happen. Check jar for completeness.");
        }
    }

    /**
     * Initialize a new hamster game by loading the default territory.
     * @param territoryBuilder A territory builder which has been used before to
     *        build the territory.
     *
     * Warning: this executes a hard reset which cannot be undone
     */
    public void initialize(final TerritoryBuilder territoryBuilder) {
        this.hardReset();

        new EditCommandStack().execute(territoryBuilder.build());
    }

    /**
     * Initialize a new hamster game by loading the territory from the passed
     * territoryContent (which is a territory encoded in a string)
     * Warning: this executes a hard reset which cannot be undone
     *
     * @param territoryContent A territory encoded as string
     */
    public void initialize(final String territoryContent) {
        this.hardReset();

        getTerritory().loadFromString(territoryContent);
    }

    /**
     * Initialize a new hamster game by loading the territory from the passed
     * territory file path.
     * Warning: this executes a hard reset which cannot be undone
     *
     * @param inputStream The input stream to load the territory from.
     */
    public void initialize(final InputStream inputStream) {
        this.hardReset();

        getTerritory().loadFromInputStream(inputStream);
    }

    /**
     * Return a territory builder for this game's territory. The builder can be passed to an initialize call
     * later to use the created territory.
     * @return A territory builder for this territory
     */
    public TerritoryBuilder getNewTerritoryBuilder() {
        return TerritoryBuilder.getTerritoryBuilderForTerritory(getTerritory());
    }

    /*@
     @ requires getCurrentGameMode() != Mode.INITIALIZING;
     @ ensures !this.commandStack.canUndoProperty().get();
     @ ensures (\old(getCurrentGameMode()) == Mode.RUNNING) ==> (getCurrentGameMode() == Mode.PAUSED);
     @ ensures (\old(getCurrentGameMode()) == Mode.PAUSED) ==> (getCurrentGameMode() == Mode.PAUSED);
     @ ensures (\old(getCurrentGameMode()) == Mode.STOPPED) ==> (getCurrentGameMode() == Mode.STOPPED);
     @*/
    /**
     * Soft-Reset the hamster game to its initial state (after started running). This undoes all steps. 
* If the game was running or paused, the game is paused, however, * it is possible to execute further steps, which redoes all previous steps first.
* If the game is stopped, it also undoes all steps previous steps and it is possible to redo them, * but it is not possible to execute new steps. To do this, a hard reset is necessary.
* It is not possible to call this during initialisation. * * @throws IllegalStateException if getCurrentGameMode() == Mode.INITIALIZING */ public void reset() { checkState(getCurrentGameMode() != Mode.INITIALIZING, "soft reset is not possible during initialization"); final Mode currentMode = getCurrentGameMode(); if (currentMode == Mode.RUNNING) { pauseGame(); this.commandStack.undoAll(); } else if (currentMode == Mode.PAUSED || currentMode.isStoppedOrAborted()) { this.commandStack.undoAll(); } } /*@ @ requires true; @ ensures !this.commandStack.canUndoProperty().get(); @ ensures !this.commandStack.canRedoProperty().get(); @ ensures getCurrentGameMode() == Mode.INITIALIZING; @*/ /** * Hard-Reset the hamster game to its initial state (after started running). It is not possible to * repeat the simulation via redo. The mode is set to initializing, so it is possible to load another territory * file, however, it is necessary to call startGame again.
* This does not unload the currently loaded territory!
*/ public void hardReset() { stopGame(); this.commandStack.undoAll(); this.commandStack.hardReset(); } /** * Run a given hamster program until it terminates. Termination * is either by ending the hamster game regularly or by throwing * an exception and not handling it inside the provided hamster program. * * The game will be started automatically if it had not been started before. * The game will be in paused mode, suitable for GUI runs, if it is started by * this method. After returning from this method the game will be in stopped mode * no matter whether an exception was thrown or the program terminated regularly. * * Precondition to running the game is that the territory has been defined or loaded * and that an IO interface is attached for reading values and handling exceptions. * * @param hamsterProgram The hamster program to run. * @throws IllegalStateException if hamsterProgram is null or if no IO interface is defined */ public void runGame(final Consumer hamsterProgram) { checkNotNull(hamsterProgram); checkState(!this.adapter.getInputInterfaces().isEmpty(), "Running a hamster game implies a defined IO interface first."); startGameIfNotStarted(); try { hamsterProgram.accept(this.territory); } catch (final GameAbortedException e) { } catch (final RuntimeException e) { this.confirmAlert(e); throw e; } finally { stopGame(); } } /*@ @ requires getCurrentGameMode() == Mode.INITIALIZING; @ ensures getCurrentGameMode() == Mode.RUNNING; @*/ /** * Start the execution of a hamster game. Before executing start, no commands can be * executed by the hamster objects in the game. * This is only possible if the current mode is Mode.INITIALIZING * The game will be started in running mode * @throws IllegalStateException if getCurrentGameMode() != Mode.INITIALIZING */ public void startGame() { checkState(getCurrentGameMode() == Mode.INITIALIZING, "start game is only possible during initialization"); this.commandStack.startGame(false); } /*@ @ requires getCurrentGameMode() == Mode.INITIALIZING; @ ensures getCurrentGameMode() == Mode.PAUSED; @*/ /** * Start the execution of a hamster game. Before executing start, no commands can be * executed by the hamster objects in the game. * This is only possible if the current mode is Mode.INITIALIZING * The game will be started in pause mode * @throws IllegalStateException if getCurrentGameMode() != Mode.INITIALIZING */ public void startGamePaused() { checkState(getCurrentGameMode() == Mode.INITIALIZING, "start game is only possible during initialization"); this.commandStack.startGame(true); } /*@ @ requires true; @ ensures getCurrentGameMode() == Mode.STOPPED; */ /** * Stop the execution of the game. The game is finished and needs to be reset / hardReset * or closed. * If the game is already stopped, this does nothing */ public void stopGame() { this.commandStack.stopGame(); } /*@ @ requires getCurrentGameMode() == Mode.RUNNING; @ ensures getCurrentGameMode() == Mode.PAUSED; @*/ /** * Pauses the HamsterGame. * It is only possible to execute this in running mode. * * @throws IllegalStateException if getCurrentGameMode() != Mode.RUNNING */ public void pauseGame() { checkState(getCurrentGameMode() == Mode.RUNNING, "pauseGame is only possible in running mode"); this.commandStack.pause(); } /*@ @ requires getCurrentGameMode() == Mode.PAUSED; @ ensures getCurrentGameMode() == Mode.RUNNING; @*/ /** * Pauses the HamsterGame. * It is only possible to execute this in paused mode. * * @throws IllegalStateException if getCurrentGameMode() != Mode.PAUSED */ public void resumeGame() { checkState(getCurrentGameMode() == Mode.PAUSED, "resumeGame is only possible in paused mode"); this.commandStack.resume(); } /*@ @ requires true; @ pure; @ ensures \result != null; @*/ /** * Get the current mode of this game. * @return The current mode of this game. */ public Mode getCurrentGameMode() { return this.commandStack.modeProperty().get(); } /*@ @ requires true; @ ensures ((\old(getCurrentGameMode()) == MODE.INITIALIZING) || (\old(getCurrentGameMode()) == MODE.STOPPED)) @ ==> (getCurrentGameMode() == Mode.PAUSED); @*/ /** * Start a hamster game, if it is not started yet or if it is stopped. * The game will be started in paused mode, suited * for GUI interaction.
* Note: if the game is stopped, a hard reset is performed before restarting * Note: If the game is already started and running, it is not paused */ private void startGameIfNotStarted() { if (getCurrentGameMode().isStoppedOrAborted()) { this.hardReset(); this.startGamePaused(); } else if (getCurrentGameMode() == Mode.INITIALIZING) { this.startGamePaused(); } } /** * This is a central method of the hamster simulation engine. It implements the mediator pattern. * It accepts command specifications of game commands and distributes it to all game entities for * their partial execution. For example, each command specification is sent to the game log so that * it can create an appropriate log entry. * @param specification The command specification of the command to be executed in this game. */ void processCommandSpecification(final CommandSpecification specification) { final Optional territoryCommandPart = territory.getInternalTerritory().getCommandFromSpecification(specification); final Optional logCommandPart = this.log.getCommandFromSpecification(specification); final Optional hamsterPart; if (specification instanceof AbstractHamsterCommandSpecification) { final AbstractHamsterCommandSpecification hamsterCommandSpec = (AbstractHamsterCommandSpecification) specification; hamsterPart = hamsterCommandSpec.getHamster().getCommandFromSpecification(specification); } else { hamsterPart = Optional.empty(); } final Command composite = new CompositeCommand() { @Override protected void buildBeforeFirstExecution(final CompositeCommandBuilder builder) { if (territoryCommandPart.isPresent()) { builder.add(territoryCommandPart.get()); } if (hamsterPart.isPresent()) { builder.add(hamsterPart.get()); } if (logCommandPart.isPresent()) { builder.add(logCommandPart.get()); } } }; this.commandStack.execute(composite); } /** * Inform a user about an abnormal execution aborting. * This blocks until it returns or is aborted * @param throwable The throwable which lead to aborting the program. * @throws IllegalStateException if no input interface is registered */ public void confirmAlert(final Throwable throwable) { // Temporary fix for #12 // Ensures meaningful exceptions if (!this.adapter.getInputInterfaces().isEmpty()) { this.executeAndGetFirstResult(inputInterface -> () -> { inputInterface.confirmAlert(throwable); return Optional.empty(); }); } } /** * Read an integer value from a user. This blocks until there is * an integer to return or it is aborted * @param message The message used in the prompt for the number. * @return The integer value read or an empty optional, if aborted. * @throws IllegalStateException if no input interface is registered * or if nothing was returned */ protected int readInteger(final String message) { return this.executeAndGetFirstResultIfPresent(inputInterface -> () -> inputInterface.readInteger(message)); } /** * Read a string value from a user. This blocks until there is a * String to return or it is aborted * @param message The message used in the prompt for the string. * @return The string value read or an empty optional, if aborted. * @throws IllegalStateException if no input interface is registered * or if nothing was returned */ protected String readString(final String message) { return this.executeAndGetFirstResultIfPresent(inputInterface -> () -> inputInterface.readString(message)); } /** * executes the callable with every input interface in parallel and returns the first result, but only if * it is present, otherwise an Exception is thrown * requires that there is at least one InputInterface * @param callableFactory factory to create the Callable out of the InputInterface * @param the return type * @return the result of the first callable that completes * @throws IllegalStateException if the first is empty */ private R executeAndGetFirstResultIfPresent(final Function>> callableFactory) { final Optional result = executeAndGetFirstResult(callableFactory); if (result.isPresent()) { return result.get(); } else { throw new IllegalStateException("nothing returned"); } } /** * executes the callable with every input interface in parallel * requires that there is at least one InputInterface * @param callableFactory factory to create the Callable out of the InputInterface * @param the return type * @return an Optional with the result of the first callable that completes */ private Optional executeAndGetFirstResult(final Function>> callableFactory) { if (this.adapter.getInputInterfaces().isEmpty()) { throw new IllegalStateException("No input interface registered"); } final CompletionService> completionService = submitInputRequests(callableFactory); try { return getFirstResult(completionService); } finally { abortInputRequests(completionService); } } /** * Creates a new CompletionService and submits a callable to each input interface to handle the input request * @param callableFactory creates the callable for the CompletionService on invocation * @param the return type of the input request * @return the CompletionService which handles all input requests */ private CompletionService> submitInputRequests(final Function>> callableFactory) { final CompletionService> completionService = new ExecutorCompletionService<>(this.executorService); for (final InputInterface inputInterface : this.adapter.getInputInterfaces()) { completionService.submit(callableFactory.apply(inputInterface)); } return completionService; } /** * Takes the first result from the completionService and return its result if possible, * otherwise throws an exception * @param completionService the service which handles all input requests * @param the return type of the input request * @return an Optional with the the first result if possible * @throws IllegalStateException if an internal error occurs */ private Optional getFirstResult(final CompletionService> completionService) { try { return completionService.take().get(); } catch (final InterruptedException e) { throw new IllegalStateException("interrupted"); } catch (final ExecutionException e) { throw new RuntimeException("nothing returned", e); } } /** * Aborts all remaining input requests by calling abort on every input interface * @param completionService the service which handles all input requests * @param the return type of the input request */ private void abortInputRequests(final CompletionService> completionService) { for (final InputInterface inputInterface : this.adapter.getInputInterfaces()) { inputInterface.abort(); } for (int i = 0; i < this.adapter.getInputInterfaces().size() - 1; i++) { try { completionService.take(); } catch (final InterruptedException e) { //ignore } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy