package rabbitescape.engine.util; import static rabbitescape.engine.CellularDirection.DOWN; import static rabbitescape.engine.CellularDirection.HERE; import static rabbitescape.engine.CellularDirection.LEFT; import static rabbitescape.engine.CellularDirection.RIGHT; import static rabbitescape.engine.CellularDirection.UP; import static rabbitescape.engine.util.MathUtil.constrain; import static rabbitescape.engine.util.MathUtil.max; import static rabbitescape.engine.util.MathUtil.min; import static rabbitescape.engine.util.MathUtil.sum; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import rabbitescape.engine.CellularDirection; import rabbitescape.engine.WaterRegion; public class WaterUtil { /** The maximum capacity of water that can be held in a quarter empty tile without it overflowing. */ public static final int QUARTER_CAPACITY = 256; /** The maximum capacity of water that can be held in a half empty tile without it overflowing. */ public static final int HALF_CAPACITY = QUARTER_CAPACITY * 2; /** The maximum capacity of water that can be held in an empty tile without it overflowing. */ public static final int MAX_CAPACITY = HALF_CAPACITY * 2; /** The default rate at which pipes produce water. */ public static final int SOURCE_RATE = 512; /** A cell can hold 1/COMPRESSION_FACTOR extra if the cell above is full (assuming both have same capacity). */ private static final int COMPRESSION_FACTOR = 4; /** A magic constant for encouraging water to flow upwards. */ private static final int MAGIC_UP_NUMERATOR = 11; private static final int MAGIC_UP_DENOMINATOR = 20; /** A fake water region, for referencing in situations where there is no water region in a given direction. */ private static final WaterRegion FAKE_REGION = new WaterRegion(0, 0, null, 0); /** Find all WaterRegions connected to the current region. */ public static Map<CellularDirection, WaterRegion> findNeighbourhood( WaterRegion region, LookupTable2D<WaterRegion> waterTable ) { Position position = region.getPosition(); Map<CellularDirection, WaterRegion> neighbourhood = new HashMap<>(); neighbourhood.put( CellularDirection.HERE, region ); Iterator<CellularDirection> connectionsIterator = region .getConnectionsIterator(); while ( connectionsIterator.hasNext() ) { CellularDirection connection = connectionsIterator.next(); Position otherPosition = connection.offset( position ); for ( WaterRegion otherRegion : waterTable .getItemsAt( otherPosition.x, otherPosition.y ) ) { if ( otherRegion .isConnected( CellularDirection.opposite( connection ) ) ) { if ( neighbourhood.containsKey( connection ) ) { throw new IllegalStateException( "There are two water regions connected on the same side of " + region ); } neighbourhood.put( connection, otherRegion ); } } } return neighbourhood; } /** Update the flow given some contents to split between some directions in ratio with the capacities. */ private static int updateFlow( Map<CellularDirection, Integer> flow, Map<CellularDirection, Integer> relevantContents, Map<CellularDirection, Integer> relevantCapacity ) { int totalCapacity = sum( relevantCapacity.values() ); int totalContents = sum( relevantContents.values() ); Map<CellularDirection, Integer> targetFlow = new HashMap<>(); Set<CellularDirection> connectedOrdinals = new HashSet<>(); for ( CellularDirection direction : relevantCapacity.keySet() ) { int target = relevantCapacity.get( direction ) * totalContents / totalCapacity; targetFlow.put( direction, max( target - relevantContents.get( direction ), 0 ) ); if ( direction != HERE ) { connectedOrdinals.add( direction ); } } int totalTargetOutFlow = 0; for ( CellularDirection direction : connectedOrdinals ) { totalTargetOutFlow += targetFlow.get( direction ); } if ( totalTargetOutFlow <= 0 ) { return 0; } int actualOutFlow = min( totalTargetOutFlow, relevantContents.get( HERE ) ); int totalFlowed = 0; for ( CellularDirection ordinal : connectedOrdinals ) { int amount = ( targetFlow.get( ordinal ) * actualOutFlow ) / totalTargetOutFlow; flow.put( ordinal, flow.get( ordinal ) + amount ); totalFlowed += amount; } return totalFlowed; } private static int updateFlowDown( Map<CellularDirection, Integer> flow, int contentsHere, Map<CellularDirection, WaterRegion> neighbourhood ) { WaterRegion down = neighbourhood.get( DOWN ); int flowDown = constrain( down.capacity - down.getContents(), 0, contentsHere ); flow.put( DOWN, flow.get( DOWN ) + flowDown ); return contentsHere - flowDown; } /** Update flow across and a bit down to simulate pressure at this level. */ private static int updateFlowAcross( Map<CellularDirection, Integer> flow, int contentsHere, Map<CellularDirection, WaterRegion> neighbourhood ) { Map<CellularDirection, Integer> relevantCapacity = new HashMap<>(); relevantCapacity.put( LEFT, neighbourhood.get( LEFT ).capacity ); relevantCapacity.put( HERE, neighbourhood.get( HERE ).capacity ); relevantCapacity.put( RIGHT, neighbourhood.get( RIGHT ).capacity ); relevantCapacity.put( DOWN, neighbourhood.get( DOWN ).capacity / COMPRESSION_FACTOR ); Map<CellularDirection, Integer> relevantContents = new HashMap<>(); relevantContents.put(LEFT, constrain(neighbourhood.get( LEFT ).getContents(), 0, neighbourhood.get( LEFT ).capacity)); relevantContents.put(HERE, constrain(contentsHere, 0, neighbourhood.get( HERE ).capacity)); relevantContents.put(RIGHT, constrain(neighbourhood.get( RIGHT ).getContents(), 0, neighbourhood.get( RIGHT ).capacity)); relevantContents.put(DOWN, max(neighbourhood.get( DOWN ).getContents() - neighbourhood.get( DOWN ).capacity, 0)); int totalFlowed = updateFlow(flow, relevantContents, relevantCapacity); return contentsHere - totalFlowed; } /** Create a 'flow' to the current cell. Any remaining can be pushed upwards. */ private static int updateFlowHere( Map<CellularDirection, Integer> flow, int contentsHere, Map<CellularDirection, WaterRegion> neighbourhood ) { int constrained = constrain( contentsHere, 0, neighbourhood.get( HERE ).capacity ); // The water will not actually leave the cell, so no need to add an explicit flow return contentsHere - constrained; } /** Update flow up and a bit across and down to simulate pressure at the level above. */ private static int updateFlowUp( Map<CellularDirection, Integer> flow, int contentsHere, Map<CellularDirection, WaterRegion> neighbourhood ) { Map<CellularDirection, Integer> relevantCapacity = new HashMap<>(); relevantCapacity.put( UP, ( neighbourhood.get( UP ).capacity * MAGIC_UP_NUMERATOR ) / MAGIC_UP_DENOMINATOR ); relevantCapacity.put( LEFT, neighbourhood.get( LEFT ).capacity / COMPRESSION_FACTOR ); relevantCapacity.put( HERE, neighbourhood.get( HERE ).capacity / COMPRESSION_FACTOR ); relevantCapacity.put( RIGHT, neighbourhood.get( RIGHT ).capacity / COMPRESSION_FACTOR ); relevantCapacity.put( DOWN, ( neighbourhood.get( DOWN ).capacity * ( COMPRESSION_FACTOR + 1 ) / ( COMPRESSION_FACTOR * COMPRESSION_FACTOR ) ) ); Map<CellularDirection, Integer> relevantContents = new HashMap<>(); relevantContents.put( UP, neighbourhood.get( UP ).getContents() ); relevantContents.put( LEFT, max( neighbourhood.get( LEFT ).getContents() - neighbourhood.get( LEFT ).capacity, 0 ) ); relevantContents.put( HERE, contentsHere ); relevantContents.put( RIGHT, max( neighbourhood.get( RIGHT ).getContents() - neighbourhood.get( RIGHT ).capacity, 0 ) ); relevantContents.put( DOWN, max(neighbourhood.get( DOWN ).getContents() - ( neighbourhood.get( DOWN ).capacity * ( COMPRESSION_FACTOR + 1 ) ) / COMPRESSION_FACTOR, 0) ); int totalFlowed = updateFlow(flow, relevantContents, relevantCapacity); return contentsHere - totalFlowed; } public static Map<CellularDirection, Integer> calculateFlow( Map<CellularDirection, WaterRegion> neighbourhood ) { Map<CellularDirection, Integer> flow = new HashMap<>(); for ( CellularDirection direction : CellularDirection.values() ) { flow.put( direction, 0 ); if ( !neighbourhood.keySet().contains( direction ) ) { neighbourhood.put( direction, FAKE_REGION ); } } int contentsHere = neighbourhood.get( HERE ).getContents(); contentsHere = updateFlowDown( flow, contentsHere, neighbourhood ); if ( contentsHere > 0 ) { contentsHere = updateFlowAcross( flow, contentsHere, neighbourhood ); contentsHere = updateFlowHere( flow, contentsHere, neighbourhood ); if ( contentsHere > 0 ) { contentsHere = updateFlowUp( flow, contentsHere, neighbourhood ); } } return flow; } }