package org.drooms.impl;
import org.drooms.api.*;
import org.drooms.api.Node.Type;
import org.drooms.impl.util.Detectors;
import org.drooms.impl.util.GameProperties;
import java.math.BigDecimal;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* On top of the rules implemented by {@link GameController}, this game
* implementation also puts forward some of its own. Those are:
*
* <dl>
* <dt>Collision detection</dt>
* <dd>When a worm reaches a node at the same time as another worm, both are terminated. When a worm moves into a body
* of another worm or into a wall, only this worm is terminated. Specific probabilities and values come from the game
* config.</dd>
* <dt>Various types of collectibles</dt>
* <dd>This class implements three types of collectibles with varying probabilities of appearance, expirations and
* valuations. There are cheap ones that occur all the time, good ones that occur sometimes and extremely lucrative ones
* that occur scarcely and don't last long.</dd>
* <dt>Simultaneous collections</dt>
* <dd>When two worms collect the same item at the same time, it is considered a collision. Both worms are terminated
* and neither is awarded points for the item.</dd>
* <dt>Survival bonuses</dt>
* <dd>In the turn when at least one worm is removed from the game, either due to crashing or inactivity, every
* surviving worm is rewarded. The amount of the reward is "the number of worms gone so far" multiplied by the bonus
* factor.</dd>
* <dt>Inactivity enforcement</dt>
* <dd>This implementation will terminate worms for inactivity, as described in the super class.</dd>
* </dl>
*/
public class DefaultGame extends GameController {
@Override
protected Map<Collectible, Player> performCollectibleCollection(final Collection<Player> players) {
final Map<Collectible, Player> collections = new HashMap<>();
for (final Player p : players) {
final Node headPosition = this.getPlayerPosition(p).getHeadNode();
final Collectible c = this.getCollectible(headPosition);
if (c != null) { // successfully collected
collections.put(c, p);
}
}
return Collections.unmodifiableMap(collections);
}
@Override
protected Collection<Collectible> performCollectibleDistribution(final GameProperties gameConfig,
final Playground playground, final Collection<Player> players, final int currentTurnNumber) {
return Collections.unmodifiableSet(gameConfig.getCollectibleTypes().stream().filter(ct -> {
final BigDecimal probability = ct.getProbabilityOfAppearance();
final BigDecimal chosen = BigDecimal.valueOf(GameController.RANDOM.nextDouble());
return probability.compareTo(chosen) > 0;
}).map(ct -> {
final double expirationAdjustmentRate = GameController.RANDOM.nextDouble() + 0.5;
final double turnsToLast = expirationAdjustmentRate * ct.getExpiration();
final int expiresIn = (int) Math.round(currentTurnNumber + turnsToLast);
final int points = ct.getPoints();
final Node target = this.pickRandomUnusedNode(playground, players);
return new Collectible(target, points, expiresIn);
}).collect(Collectors.toSet()));
}
@Override
protected Set<Player> performCollisionDetection(final Playground playground,
final Collection<Player> currentPlayers) {
return Detectors.detectCollision(currentPlayers.stream().map(this::getPlayerPosition).collect(
Collectors.toSet())).stream().map(PlayerPosition::getPlayer).collect(Collectors.toSet());
}
@Override
protected Set<Player> performInactivityDetection(final Collection<Player> currentPlayers,
final int currentTurnNumber, final int allowedInactiveTurns) {
return Detectors.detectInactivity(allowedInactiveTurns, currentPlayers.stream().collect(
Collectors.toMap(Function.identity(), this::getDecisionRecord)));
}
@Override
protected PlayerPosition performPlayerAction(final PlayerPosition currentPos, final Action decision) {
// move the head of the worm
final Node currentHeadPos = currentPos.getHeadNode();
final Playground playground = currentPos.getPlayground();
Node newHeadPos;
switch (decision) {
case REVERSE: // reverse the snake and do nothing else
return currentPos.reverse();
case MOVE_UP:
newHeadPos = playground.getNodeAt(currentHeadPos.getX(), currentHeadPos.getY() + 1);
break;
case MOVE_DOWN:
newHeadPos = playground.getNodeAt(currentHeadPos.getX(), currentHeadPos.getY() - 1);
break;
case MOVE_LEFT:
newHeadPos = playground.getNodeAt(currentHeadPos.getX() - 1, currentHeadPos.getY());
break;
case MOVE_RIGHT:
newHeadPos = playground.getNodeAt(currentHeadPos.getX() + 1, currentHeadPos.getY());
break;
case ENTER:
if (currentHeadPos.getType() == Type.PORTAL) {
// the current node is a PORTAL. move to the other end
newHeadPos = playground.getOtherEndOfPortal(currentHeadPos);
break;
}
// else this command makes no sense and we STAY
// FIXME this is a problem for inactivity detection
case NOTHING:
// do not modify the worm in any way
return currentPos;
default:
throw new IllegalStateException("Unknown action!");
}
// just to be sure; in theory can never happen as you first always hit a wall
if (newHeadPos == null) {
throw new IllegalStateException("Moving to a non-existent node!");
}
// move the head of the snake
if (newHeadPos != currentHeadPos) {
final PlayerPosition newPosition = currentPos.newHead(newHeadPos);
return newPosition.ensureMaxLength(this.getPlayerLength(currentPos.getPlayer()));
} else {
return currentPos.ensureMaxLength(this.getPlayerLength(currentPos.getPlayer()));
}
}
@Override
protected Map<Player, Integer> performSurvivalRewarding(final Collection<Player> allPlayers,
final Collection<Player> survivingPlayers, final int removedInThisRound, final int rewardAmount) {
if (removedInThisRound < 1) {
return Collections.emptyMap();
}
final int amount = rewardAmount * (allPlayers.size() - survivingPlayers.size());
return Collections.unmodifiableMap(survivingPlayers.stream().collect(Collectors.toMap(player -> player,
player -> amount)));
}
private Node pickRandomUnusedNode(final Playground p, final Collection<Player> players) {
final List<Node> nodes = new LinkedList<>();
// locate available nodes
for (int x = 0; x < p.getWidth(); x++) {
for (int y = 0; y < p.getHeight(); y++) {
if (p.isAvailable(x, y)) {
nodes.add(p.getNodeAt(x, y));
}
}
}
// exclude nodes where worms are
players.forEach(player -> nodes.removeAll(this.getPlayerPosition(player).getNodes()));
// exclude nodes where collectibles are
final List<Node> finalNodes = nodes.stream().filter(n -> this.getCollectible(n) == null).collect(Collectors
.toList());
if (finalNodes.size() == 0) {
return null;
} else {
return finalNodes.get(GameController.RANDOM.nextInt(finalNodes.size()));
}
}
}