package me.stieglmaier.sphereMiners.model.physics; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Random; import java.util.Set; import java.util.stream.Collectors; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; import javafx.scene.paint.Color; import me.stieglmaier.sphereMiners.main.Constants; import me.stieglmaier.sphereMiners.model.ai.Player; import me.stieglmaier.sphereMiners.model.util.MutableSphere; import me.stieglmaier.sphereMiners.model.util.Position; import me.stieglmaier.sphereMiners.model.util.Sphere; import me.stieglmaier.sphereMiners.model.util.Tick; /** * This class handles all the physical computations such as moving and "eating" * smaller spheres that are in the necessary range. The move compuatation is done * based on a certain FPS number which is divided into smaller computation parts. * * @author stieglma * */ public class Physics { private final BiMap<MutableSphere, Sphere> aiSpheres = HashBiMap.create(); private final Set<MutableSphere> dots = new HashSet<>(); private final Constants constants; private final double tickLength; private final double partialTick; private final Random random = new Random(); private final Set<Sphere> spheresForAisNextTurn = new HashSet<>(); /** * Creates a physics object. * @param constants The constants object the physics will base the computation on. */ public Physics(Constants constants) { this.constants = constants; tickLength = 1.0 / constants.getFramesPerSecond(); partialTick = tickLength / constants.getCalcsPerTick(); } /** * Creates the initial tick for a simulation. This also deletes the previously * done simulation. * * @param playingAIs the list of players that should take part * @return the computed initial tick */ public Tick createInitialTick(List<Player> playingAIs) { aiSpheres.clear(); spheresForAisNextTurn.clear(); Position initalPos = new Position(constants.getFieldWidth() / 2, constants.getFieldHeight() / 2); double angle = 360.0 / playingAIs.size(); // b² = c² - a², c = 1 im Einheitskreis, a = sin Alpha * c im Einheitskreis // Strahlensatz: ZA zu BA = ZA' zu BA', c zu a = x*c zu x*a sodass x*a = 5 double a = Math.sin(angle / 360 * Math.PI); double radius = constants.getInitialDistance() / 2 / a; int i = 0; for (Player ai : playingAIs) { // create new sphere for current player MutableSphere sphere = new MutableSphere(constants, ai); Position addPos = new Position( radius * Math.cos(i * 2 * Math.PI / playingAIs.size()), radius * Math.sin(i * 2 * Math.PI / playingAIs.size())); // place modulo the usual position if it would be out of bounds Position spherePos = initalPos.add(addPos); if (spherePos.getX() < 0 || spherePos.getX() > constants.getFieldWidth() || spherePos.getY() < 0 || spherePos.getY() > constants.getFieldHeight()) { spherePos = new Position( spherePos.getX() % constants.getFieldWidth(), spherePos.getY() % constants.getFieldHeight()); } sphere.setPosition(spherePos); aiSpheres.put(sphere, sphere.toImmutableSphere()); i++; } createDots(constants.getDotAmount()); spheresForAisNextTurn.addAll(aiSpheres.values()); return snapshot(); } private void createDots(int number) { for (int i = 0; i < number; i++) { MutableSphere sphere = new MutableSphere(constants); sphere.setPosition( new Position( random.nextInt(constants.getFieldWidth() + 1), random.nextInt(constants.getFieldHeight() + 1))); sphere.setSize(constants.getDotSize()); sphere.setColor(new Color(random.nextDouble(), random.nextDouble(), random.nextDouble(), 1)); dots.add(sphere); } } /** * This method applies the physics for one tick (frame). It is computed * in smaller parts in a loop, the last of these parts is then packed * into a tick and returned. * * @return the computed tick */ public Tick applyPhysics() { for (int i = 0; i < constants.getCalcsPerTick(); i++) { // 1. move all spheres moveSpheres(); // 2. merge dots into spheres mergeDots(); } // refill dots createDots(constants.getDotAmount() - dots.size()); // update ailist spheresForAisNextTurn.clear(); spheresForAisNextTurn.addAll(aiSpheres.values()); return snapshot(); } private Tick snapshot() { Builder<Sphere> sphereCopy = ImmutableList.builder(); for (MutableSphere sphere : aiSpheres.keySet()) { { sphereCopy.add(sphere.immutableCopy()); } } Builder<Sphere> dotsCopy = ImmutableList.builder(); for (MutableSphere dot : dots) { dotsCopy.add(dot.immutableCopy()); } return new Tick(sphereCopy.build(), dotsCopy.build()); } private void moveSpheres() { for (MutableSphere sphere : aiSpheres.keySet()) { double speed = (Math.log(constants.getInitialSphereSize()) / Math.log(sphere.getSize()) * (constants.getMaxSpeed() - constants.getMinSpeed()) + constants.getMinSpeed()) * partialTick; Position tmpPos = sphere.getPosition().add(sphere.getDirection().mult(speed)); double x = tmpPos.getX() > constants.getFieldWidth() ? constants.getFieldWidth() : (tmpPos.getX() < 0 ? 0 : tmpPos.getX()); double y = tmpPos.getY() > constants.getFieldHeight() ? constants.getFieldHeight() : (tmpPos.getY() < 0 ? 0 : tmpPos.getY()); sphere.setPosition(new Position(x, y)); } } private void mergeDots() { for (MutableSphere sphere : aiSpheres.keySet()) { Iterator<MutableSphere> dotsIt = dots.iterator(); while (dotsIt.hasNext()) { Sphere dot = dotsIt.next(); if (sphere.canBeMergedWidth(dot)) { sphere.merge(dot); dotsIt.remove(); } } } } /** * Returns a set of the map with all spheres per player. * * @return a set of all spheres owned by AIs */ public Set<Sphere> getAISpheres() { return spheresForAisNextTurn; } /** * Returns an unmodifiable view of the set of all dots on the playground * * @return the set of all dots on the playground */ public Set<Sphere> getDots() { return Collections.unmodifiableSet( dots.stream().map(s -> s.toImmutableSphere()).collect(Collectors.toSet())); } public void changeDirection(Sphere sphere, Position direction) { // perhaps a sphere was mined and therefore is no longer available if (aiSpheres.values().contains(sphere)) { aiSpheres.inverse().get(sphere).setDirection(direction.normalize()); } } /** * Splits a sphere into two smaller parts * @param sphere the sphere to split */ public void split(Sphere sphere) { // perhaps a sphere was mined and therefore is no longer available if (aiSpheres.values().contains(sphere)) { MutableSphere s = aiSpheres.inverse().get(sphere); MutableSphere newSphere = s.split(); if (newSphere != null) { aiSpheres.put(newSphere, newSphere.toImmutableSphere()); } } } /** * Merges two spheres if they are in the necessary range to do that. * @param big the sphere that should grow * @param small the sphere that should be merged into the other one */ public void merge(Sphere big, Sphere small) { // perhaps a sphere was mined and therefore is no longer available if (aiSpheres.values().contains(big) && aiSpheres.values().contains(small) && big.canBeMergedWidth(small)) { MutableSphere bigger = aiSpheres.inverse().get(big); MutableSphere smaller = aiSpheres.inverse().get(small); aiSpheres.remove(smaller); bigger.merge(smaller); } } public void mine(Sphere minerSphere, Sphere minedSphere) { // perhaps a sphere was mined and therefore is no longer available if (aiSpheres.values().contains(minerSphere) && aiSpheres.values().contains(minedSphere) && minerSphere.canBeMergedWidth(minedSphere)) { MutableSphere miner = aiSpheres.inverse().get(minerSphere); MutableSphere mined = aiSpheres.inverse().get(minedSphere); aiSpheres.remove(mined); miner.merge(mined); } } }