// This file is part of MicropolisJ.
// Copyright (C) 2013 Jason Long
// Portions Copyright (C) 1989-2007 Electronic Arts Inc.
//
// MicropolisJ is free software; you can redistribute it and/or modify
// it under the terms of the GNU GPLv3, with additional terms.
// See the README file, included in this distribution, for details.

package micropolisj.engine;

import java.io.*;
import java.util.*;

import static micropolisj.engine.TileConstants.*;

/**
 * The main simulation engine for Micropolis.
 * The front-end should call animate() periodically
 * to move the simulation forward in time.
 */
public class Micropolis
{
	static final Random DEFAULT_PRNG = new Random();

	Random PRNG;

	// full size arrays
	char [][] map;
	boolean [][] powerMap;

	// half-size arrays

	/**
	 * For each 2x2 section of the city, the land value of the city (0-250).
	 * 0 is lowest land value; 250 is maximum land value.
	 * Updated each cycle by ptlScan().
	 */
	public int [][] landValueMem;

	/**
	 * For each 2x2 section of the city, the pollution level of the city (0-255).
	 * 0 is no pollution; 255 is maximum pollution.
	 * Updated each cycle by ptlScan(); affects land value.
	 */
	public int [][] pollutionMem;

	/**
	 * For each 2x2 section of the city, the crime level of the city (0-250).
	 * 0 is no crime; 250 is maximum crime.
	 * Updated each cycle by crimeScan(); affects land value.
	 */
	public int [][] crimeMem;

	/**
	 * For each 2x2 section of the city, the population density (0-?).
	 * Used for map overlays and as a factor for crime rates.
	 */
	public int [][] popDensity;

	/**
	 * For each 2x2 section of the city, the traffic density (0-255).
	 * If less than 64, no cars are animated.
	 * If between 64 and 192, then the "light traffic" animation is used.
	 * If 192 or higher, then the "heavy traffic" animation is used.
	 */
	public int [][] trfDensity;

	// quarter-size arrays

	/**
	 * For each 4x4 section of the city, an integer representing the natural
	 * land features in the vicinity of this part of the city.
	 */
	int [][] terrainMem;

	// eighth-size arrays

	/**
	 * For each 8x8 section of the city, the rate of growth.
	 * Capped to a number between -200 and 200.
	 * Used for reporting purposes only; the number has no affect.
	 */
	public int [][] rateOGMem; //rate of growth?

	int [][] fireStMap;      //firestations- cleared and rebuilt each sim cycle
	public int [][] fireRate;       //firestations reach- used for overlay graphs
	int [][] policeMap;      //police stations- cleared and rebuilt each sim cycle
	public int [][] policeMapEffect;//police stations reach- used for overlay graphs

	/** For each 8x8 section of city, this is an integer between 0 and 64,
	 * with higher numbers being closer to the center of the city. */
	int [][] comRate;

	static final int DEFAULT_WIDTH = 120;
	static final int DEFAULT_HEIGHT = 100;

	public final CityBudget budget = new CityBudget(this);
	public boolean autoBulldoze = true;
	public boolean autoBudget = false;
	public Speed simSpeed = Speed.NORMAL;
	public boolean noDisasters = false;

	public int gameLevel;

	boolean autoGo;

	// census numbers, reset in phase 0 of each cycle, summed during map scan
	int poweredZoneCount;
	int unpoweredZoneCount;
	int roadTotal;
	int railTotal;
	int firePop;
	int resZoneCount;
	int comZoneCount;
	int indZoneCount;
	int resPop;
	int comPop;
	int indPop;
	int hospitalCount;
	int churchCount;
	int policeCount;
	int fireStationCount;
	int stadiumCount;
	int coalCount;
	int nuclearCount;
	int seaportCount;
	int airportCount;

	int totalPop;
	int lastCityPop;

	// used in generateBudget()
	int lastRoadTotal;
	int lastRailTotal;
	int lastTotalPop;
	int lastFireStationCount;
	int lastPoliceCount;

	int trafficMaxLocationX;
	int trafficMaxLocationY;
	int pollutionMaxLocationX;
	int pollutionMaxLocationY;
	int crimeMaxLocationX;
	int crimeMaxLocationY;
	public int centerMassX;
	public int centerMassY;
	CityLocation meltdownLocation;  //may be null
	CityLocation crashLocation;     //may be null

	int needHospital; // -1 too many already, 0 just right, 1 not enough
	int needChurch;   // -1 too many already, 0 just right, 1 not enough

	int crimeAverage;
	int pollutionAverage;
	int landValueAverage;
	int trafficAverage;

	int resValve;   // ranges between -2000 and 2000, updated by setValves
	int comValve;   // ranges between -1500 and 1500
	int indValve;   // ranges between -1500 and 1500

	boolean resCap;  // residents demand a stadium, caps resValve at 0
	boolean comCap;  // commerce demands airport,   caps comValve at 0
	boolean indCap;  // industry demands sea port,  caps indValve at 0
	int crimeRamp;
	int polluteRamp;

	//
	// budget stuff
	//
	public int cityTax = 7;
	public double roadPercent = 1.0;
	public double policePercent = 1.0;
	public double firePercent = 1.0;

	int taxEffect = 7;
	int roadEffect = 32;
	int policeEffect = 1000;
	int fireEffect = 1000;

	int cashFlow; //net change in totalFunds in previous year

	boolean newPower;

	int floodCnt; //number of turns the flood will last
	int floodX;
	int floodY;

	public int cityTime;  //counts "weeks" (actually, 1/48'ths years)
	int scycle; //same as cityTime, except mod 1024
	int fcycle; //counts simulation steps (mod 1024)
	int acycle; //animation cycle (mod 960)

	public CityEval evaluation;

	ArrayList<Sprite> sprites = new ArrayList<Sprite>();

	static final int VALVERATE = 2;
	public static final int CENSUSRATE = 4;
	static final int TAXFREQ = 48;

	public void spend(int amount)
	{
		budget.totalFunds -= amount;
		fireFundsChanged();
	}

	public Micropolis()
	{
		this(DEFAULT_WIDTH, DEFAULT_HEIGHT);
	}

	public Micropolis(int width, int height)
	{
		PRNG = DEFAULT_PRNG;
		evaluation = new CityEval(this);
		init(width, height);
	}

	protected void init(int width, int height)
	{
		map = new char[height][width];
		powerMap = new boolean[height][width];

		int hX = (width+1)/2;
		int hY = (height+1)/2;

		landValueMem = new int[hY][hX];
		pollutionMem = new int[hY][hX];
		crimeMem = new int[hY][hX];
		popDensity = new int[hY][hX];
		trfDensity = new int[hY][hX];

		int qX = (width+3)/4;
		int qY = (height+3)/4;

		terrainMem = new int[qY][qX];

		int smX = (width+7)/8;
		int smY = (height+7)/8;

		rateOGMem = new int[smY][smX];
		fireStMap = new int[smY][smX];
		policeMap = new int[smY][smX];
		policeMapEffect = new int[smY][smX];
		fireRate = new int[smY][smX];
		comRate = new int[smY][smX];

		centerMassX = hX;
		centerMassY = hY;
	}

	void fireCensusChanged()
	{
		for (Listener l : listeners) {
			l.censusChanged();
		}
	}

	void fireCityMessage(MicropolisMessage message, CityLocation loc, boolean isPic)
	{
		for (Listener l : listeners) {
			l.cityMessage(message, loc, isPic);
		}
	}

	void fireCitySound(Sound sound, CityLocation loc)
	{
		for (Listener l : listeners) {
			l.citySound(sound, loc);
		}
	}

	void fireDemandChanged()
	{
		for (Listener l : listeners) {
			l.demandChanged();
		}
	}

	void fireEarthquakeStarted()
	{
		for (EarthquakeListener l : earthquakeListeners) {
			l.earthquakeStarted();
		}
	}

	void fireEvaluationChanged()
	{
		for (Listener l : listeners) {
			l.evaluationChanged();
		}
	}

	void fireFundsChanged()
	{
		for (Listener l : listeners) {
			l.fundsChanged();
		}
	}

	void fireMapOverlayDataChanged(MapState overlayDataType)
	{
		for (MapListener l : mapListeners) {
			l.mapOverlayDataChanged(overlayDataType);
		}
	}

	void fireOptionsChanged()
	{
		for (Listener l : listeners)
		{
			l.optionsChanged();
		}
	}

	void fireSpriteMoved(Sprite sprite)
	{
		for (MapListener l : mapListeners)
		{
			l.spriteMoved(sprite);
		}
	}

	void fireTileChanged(int xpos, int ypos)
	{
		for (MapListener l : mapListeners)
		{
			l.tileChanged(xpos, ypos);
		}
	}

	void fireWholeMapChanged()
	{
		for (MapListener l : mapListeners)
		{
			l.wholeMapChanged();
		}
	}

	ArrayList<Listener> listeners = new ArrayList<Listener>();
	ArrayList<MapListener> mapListeners = new ArrayList<MapListener>();
	ArrayList<EarthquakeListener> earthquakeListeners = new ArrayList<EarthquakeListener>();

	public void addListener(Listener l)
	{
		this.listeners.add(l);
	}

	public void removeListener(Listener l)
	{
		this.listeners.remove(l);
	}

	public void addEarthquakeListener(EarthquakeListener l)
	{
		this.earthquakeListeners.add(l);
	}

	public void removeEarthquakeListener(EarthquakeListener l)
	{
		this.earthquakeListeners.remove(l);
	}

	public void addMapListener(MapListener l)
	{
		this.mapListeners.add(l);
	}

	public void removeMapListener(MapListener l)
	{
		this.mapListeners.remove(l);
	}

	/**
	 * The listener interface for receiving miscellaneous events that occur
	 * in the Micropolis city.
	 * Use the Micropolis class's addListener interface to register an object
	 * that implements this interface.
	 */
	public interface Listener
	{
		void cityMessage(MicropolisMessage message, CityLocation loc, boolean isPic);
		void citySound(Sound sound, CityLocation loc);

		/**
		 * Fired whenever the "census" is taken, and the various historical
		 * counters have been updated. (Once a month in game.)
		 */
		void censusChanged();

		/**
		 * Fired whenever resValve, comValve, or indValve changes.
		 * (Twice a month in game.) */
		void demandChanged();

		/**
		 * Fired whenever the city evaluation is recalculated.
		 * (Once a year.)
		 */
		void evaluationChanged();

		/**
		 * Fired whenever the mayor's money changes.
		 */
		void fundsChanged();

		/**
		 * Fired whenever autoBulldoze, autoBudget, noDisasters,
		 * or simSpeed change.
		 */
		void optionsChanged();
	}

	public int getWidth()
	{
		return map[0].length;
	}

	public int getHeight()
	{
		return map.length;
	}

	public char getTile(int xpos, int ypos)
	{
		return map[ypos][xpos];
	}

	public void setTile(int xpos, int ypos, char newTile)
	{
		if (map[ypos][xpos] != newTile)
		{
			map[ypos][xpos] = newTile;
			fireTileChanged(xpos, ypos);
		}
	}

	final public boolean testBounds(int xpos, int ypos)
	{
		return xpos >= 0 && xpos < getWidth() &&
			ypos >= 0 && ypos < getHeight();
	}

	final boolean hasPower(int x, int y)
	{
		return powerMap[y][x];
	}

	/**
	 * Checks whether the next call to animate() will collect taxes and
	 * process the budget.
	 */
	public boolean isBudgetTime()
	{
		return (
			cityTime != 0 &&
			(cityTime % TAXFREQ) == 0 &&
			((fcycle + 1) % 16) == 10 &&
			((acycle + 1) % 2) == 0
			);
	}

	void step()
	{
		fcycle = (fcycle + 1) % 1024;
		simulate(fcycle % 16);
	}

	void clearCensus()
	{
		poweredZoneCount = 0;
		unpoweredZoneCount = 0;
		firePop = 0;
		roadTotal = 0;
		railTotal = 0;
		resPop = 0;
		comPop = 0;
		indPop = 0;
		resZoneCount = 0;
		comZoneCount = 0;
		indZoneCount = 0;
		hospitalCount = 0;
		churchCount = 0;
		policeCount = 0;
		fireStationCount = 0;
		stadiumCount = 0;
		coalCount = 0;
		nuclearCount = 0;
		seaportCount = 0;
		airportCount = 0;
		powerPlants.clear();

		for (int y = 0; y < fireStMap.length; y++) {
			for (int x = 0; x < fireStMap[y].length; x++) {
				fireStMap[y][x] = 0;
				policeMap[y][x] = 0;
			}
		}
	}

	void simulate(int mod16)
	{
		final int band = getWidth() / 8;

		switch (mod16)
		{
		case 0:
			scycle = (scycle + 1) % 1024;
			cityTime++;
			if (scycle % 2 == 0) {
				setValves();
			}
			clearCensus();
			break;

		case 1:
			mapScan(0 * band, 1 * band);
			break;

		case 2:
			mapScan(1 * band, 2 * band);
			break;

		case 3:
			mapScan(2 * band, 3 * band);
			break;

		case 4:
			mapScan(3 * band, 4 * band);
			break;

		case 5:
			mapScan(4 * band, 5 * band);
			break;

		case 6:
			mapScan(5 * band, 6 * band);
			break;

		case 7:
			mapScan(6 * band, 7 * band);
			break;

		case 8:
			mapScan(7 * band, getWidth());
			break;

		case 9:
			if (cityTime % CENSUSRATE == 0) {
				takeCensus();

				if (cityTime % (CENSUSRATE*12) == 0) {
					takeCensus2();
				}

				fireCensusChanged();
			}

			collectTaxPartial();

			if (cityTime % TAXFREQ == 0) {
				collectTax();
				evaluation.cityEvaluation();
			}
			break;

		case 10:
			if (scycle % 5 == 0) {  // every ~10 weeks
				decROGMem();
			}
			decTrafficMem();
			fireMapOverlayDataChanged(MapState.TRAFFIC_OVERLAY); //TDMAP
			fireMapOverlayDataChanged(MapState.TRANSPORT);       //RDMAP
			fireMapOverlayDataChanged(MapState.ALL);             //ALMAP
			fireMapOverlayDataChanged(MapState.RESIDENTIAL);     //REMAP
			fireMapOverlayDataChanged(MapState.COMMERCIAL);      //COMAP
			fireMapOverlayDataChanged(MapState.INDUSTRIAL);      //INMAP
			doMessages();
			break;

		case 11:
			powerScan();
			fireMapOverlayDataChanged(MapState.POWER_OVERLAY);
			newPower = true;
			break;

		case 12:
			ptlScan();
			break;

		case 13:
			crimeScan();
			break;

		case 14:
			popDenScan();
			break;

		case 15:
			fireAnalysis();
			doDisasters();
			break;

		default:
			throw new Error("unreachable");
		}
	}

	private int computePopDen(int x, int y, char tile)
	{
		if (tile == FREEZ)
			return doFreePop(x, y);

		if (tile < COMBASE)
			return residentialZonePop(tile);

		if (tile < INDBASE)
			return commercialZonePop(tile) * 8;

		if (tile < PORTBASE)
			return industrialZonePop(tile) * 8;

		return 0;
	}

	private static int [][] doSmooth(int [][] tem)
	{
		final int h = tem.length;
		final int w = tem[0].length;
		int [][] tem2 = new int[h][w];

		for (int y = 0; y < h; y++)
		{
			for (int x = 0; x < w; x++)
			{
				int z = tem[y][x];
				if (x > 0)
					z += tem[y][x-1];
				if (x + 1 < w)
					z += tem[y][x+1];
				if (y > 0)
					z += tem[y-1][x];
				if (y + 1 < h)
					z += tem[y+1][x];
				z /= 4;
				if (z > 255)
					z = 255;
				tem2[y][x] = z;
			}
		}
	
		return tem2;
	}

	public void calculateCenterMass()
	{
		popDenScan();
	}

	private void popDenScan()
	{
		int xtot = 0;
		int ytot = 0;
		int zoneCount = 0;
		int width = getWidth();
		int height = getHeight();
		int [][] tem = new int[(height+1)/2][(width+1)/2];

		for (int x = 0; x < width; x++)
		{
			for (int y = 0; y < height; y++)
			{
				char tile = map[y][x];
				if (isZoneCenter(tile))
				{
					tile &= LOMASK;
					int den = computePopDen(x, y, (char)tile) * 8;
					if (den > 254)
						den = 254;
					tem[y/2][x/2] = den;
					xtot += x;
					ytot += y;
					zoneCount++;
				}
			}
		}

		tem = doSmooth(tem);
		tem = doSmooth(tem);
		tem = doSmooth(tem);

		for (int x = 0; x < (width+1)/2; x++)
		{
			for (int y = 0; y < (height+1)/2; y++)
			{
				popDensity[y][x] = 2 * tem[y][x];
			}
		}

		distIntMarket(); //set ComRate

		// find center of mass for city
		if (zoneCount != 0)
		{
			centerMassX = xtot / zoneCount;
			centerMassY = ytot / zoneCount;
		}
		else
		{
			centerMassX = (width+1)/2;
			centerMassY = (height+1)/2;
		}

		fireMapOverlayDataChanged(MapState.POPDEN_OVERLAY);     //PDMAP
		fireMapOverlayDataChanged(MapState.GROWTHRATE_OVERLAY); //RGMAP
	}

	private void distIntMarket()
	{
		for (int y = 0; y < comRate.length; y++)
		{
			for (int x = 0; x < comRate[y].length; x++)
			{
				int z = getDisCC(x*4, y*4);
				z /= 4;
				z = 64 - z;
				comRate[y][x] = z;
			}
		}
	}

	//tends to empty RateOGMem[][]
	private void decROGMem()
	{
		for (int y = 0; y < rateOGMem.length; y++)
		{
			for (int x = 0; x < rateOGMem[y].length; x++)
			{
				int z = rateOGMem[y][x];
				if (z == 0)
					continue;

				if (z > 0)
				{
					rateOGMem[y][x]--;
					if (z > 200)
					{
						rateOGMem[y][x] = 200; //prevent overflow?
					}
					continue;
				}

				if (z < 0)
				{
					rateOGMem[y][x]++;
					if (z < -200)
					{
						rateOGMem[y][x] = -200;
					}
					continue;
				}
			}
		}
	}

	//tends to empty trfDensity
	private void decTrafficMem()
	{
		for (int y = 0; y < trfDensity.length; y++)
		{
			for (int x = 0; x < trfDensity[y].length; x++)
			{
				int z = trfDensity[y][x];
				if (z != 0)
				{
					if (z > 200)
						trfDensity[y][x] = z - 34;
					else if (z > 24)
						trfDensity[y][x] = z - 24;
					else
						trfDensity[y][x] = 0;
				}
			}
		}
	}

	void crimeScan()
	{
		policeMap = smoothFirePoliceMap(policeMap);
		policeMap = smoothFirePoliceMap(policeMap);
		policeMap = smoothFirePoliceMap(policeMap);

		for (int sy = 0; sy < policeMap.length; sy++) {
			for (int sx = 0; sx < policeMap[sy].length; sx++) {
				policeMapEffect[sy][sx] = policeMap[sy][sx];
			}
		}

		int count = 0;
		int sum = 0;
		int cmax = 0;
		for (int hy = 0; hy < landValueMem.length; hy++) {
			for (int hx = 0; hx < landValueMem[hy].length; hx++) {
				int val = landValueMem[hy][hx];
				if (val != 0) {
					count++;
					int z = 128 - val + popDensity[hy][hx];
					z = Math.min(300, z);
					z -= policeMap[hy/4][hx/4];
					z = Math.min(250, z);
					z = Math.max(0, z);
					crimeMem[hy][hx] = z;

					sum += z;
					if (z > cmax || (z == cmax && PRNG.nextInt(4) == 0)) {
						cmax = z;
						crimeMaxLocationX = hx*2;
						crimeMaxLocationY = hy*2;
					}
				}
				else {
					crimeMem[hy][hx] = 0;
				}
			}
		}

		if (count != 0)
			crimeAverage = sum / count;
		else
			crimeAverage = 0;

		fireMapOverlayDataChanged(MapState.POLICE_OVERLAY);
	}

	void doDisasters()
	{
		if (floodCnt > 0) {
			floodCnt--;
		}

		final int [] DisChance = { 480, 240, 60 };
		if (noDisasters)
			return;

		if (PRNG.nextInt(DisChance[gameLevel]+1) != 0)
			return;

		switch (PRNG.nextInt(9))
		{
		case 0:
		case 1:
			setFire();
			break;
		case 2:
		case 3:
			makeFlood();
			break;
		case 4:
			break;
		case 5:
			makeTornado();
			break;
		case 6:
			makeEarthquake();
			break;
		case 7:
		case 8:
			if (pollutionAverage > 60) {
				makeMonster();
			}
			break;
		}
	}

	private int[][] smoothFirePoliceMap(int[][] omap)
	{
		int smX = omap[0].length;
		int smY = omap.length;
		int[][] nmap = new int[smY][smX];
		for (int sy = 0; sy < smY; sy++) {
			for (int sx = 0; sx < smX; sx++) {
				int edge = 0;
				if (sx > 0) { edge += omap[sy][sx-1]; }
				if (sx + 1 < smX) { edge += omap[sy][sx+1]; }
				if (sy > 0) { edge += omap[sy-1][sx]; }
				if (sy + 1 < smY) { edge += omap[sy+1][sx]; }
				edge = edge / 4 + omap[sy][sx];
				nmap[sy][sx] = edge / 2;
			}
		}
		return nmap;
	}

	void fireAnalysis()
	{
		fireStMap = smoothFirePoliceMap(fireStMap);
		fireStMap = smoothFirePoliceMap(fireStMap);
		fireStMap = smoothFirePoliceMap(fireStMap);
		for (int sy = 0; sy < fireStMap.length; sy++) {
			for (int sx = 0; sx < fireStMap[sy].length; sx++) {
				fireRate[sy][sx] = fireStMap[sy][sx];
			}
		}

		fireMapOverlayDataChanged(MapState.FIRE_OVERLAY);
	}

	private boolean isConductive(int x, int y)
	{
		return (map[y][x] & CONDBIT) != 0;
	}

	private boolean testForCond(CityLocation loc, int dir)
	{
		int xsave = loc.x;
		int ysave = loc.y;

		boolean rv = false;
		if (movePowerLocation(loc,dir))
		{
			rv = (
				isConductive(loc.x, loc.y) &&
				map[loc.y][loc.x] != NUCLEAR &&
				map[loc.y][loc.x] != POWERPLANT &&
				!hasPower(loc.x, loc.y)
				);
		}

		loc.x = xsave;
		loc.y = ysave;
		return rv;
	}

	private boolean movePowerLocation(CityLocation loc, int dir)
	{
		switch(dir)
		{
		case 0:
			if (loc.y > 0)
			{
				loc.y--;
				return true;
			}
			else
				return false;
		case 1:
			if (loc.x + 1 < getWidth())
			{
				loc.x++;
				return true;
			}
			else
				return false;
		case 2:
			if (loc.y + 1 < getHeight())
			{
				loc.y++;
				return true;
			}
			else
				return false;
		case 3:
			if (loc.x > 0)
			{
				loc.x--;
				return true;
			}
			else
				return false;
		case 4:
			return true;
		}
		return false;
	}

	void powerScan()
	{
		// clear powerMap
		for (boolean [] bb : powerMap)
		{
			Arrays.fill(bb, false);
		}

		//
		// Note: brownouts are based on total number of power plants, not the number
		// of powerplants connected to your city.
		//

		int maxPower = coalCount * 700 + nuclearCount * 2000;
		int numPower = 0;

		// This is kind of odd algorithm, but I haven't the heart to rewrite it at
		// this time.

		while (!powerPlants.isEmpty())
		{
			CityLocation loc = powerPlants.pop();

			int aDir = 4;
			int conNum;
			do
			{
				if (++numPower > maxPower)
				{
					// trigger notification
					sendMessage(MicropolisMessage.BROWNOUTS_REPORT);
					return;
				}
				movePowerLocation(loc, aDir);
				powerMap[loc.y][loc.x] = true;

				conNum = 0;
				int dir = 0;
				while (dir < 4 && conNum < 2)
				{
					if (testForCond(loc, dir))
					{
						conNum++;
						aDir = dir;
					}
					else
					{
					}
					dir++;
				}
				if (conNum > 1)
				{
					powerPlants.add(new CityLocation(loc.x,loc.y));
				}
			}
			while (conNum != 0);
		}
	}

	static int getPollutionValue(int tile)
	{
		if (tile < POWERBASE)
		{
			if (tile >= HTRFBASE)
				return 75; //heavy traffic
			if (tile >= LTRFBASE)
				return 50; //light traffic

			if (tile < ROADBASE)
			{
				if (tile > FIREBASE)
					return 90;  //active fire

				if (tile >= RADTILE)
					return 255; //radioactivity
			}

			return 0;
		}

		if (tile <= LASTIND)
			return 0;

		if (tile < PORTBASE)
			return 50;

		if (tile <= LASTPOWERPLANT)
			return 100;

		return 0;
	}

	public int getTrafficDensity(int xpos, int ypos)
	{
		if (testBounds(xpos, ypos)) {
			return trfDensity[ypos/2][xpos/2];
		} else {
			return 0;
		}
	}

	//power, terrain, land value
	void ptlScan()
	{
		final int qX = (getWidth()+3)/4;
		final int qY = (getHeight()+3)/4;
		int [][] qtem = new int[qY][qX];

		int landValueTotal = 0;
		int landValueCount = 0;

		final int HWLDX = (getWidth()+1)/2;
		final int HWLDY = (getHeight()+1)/2;
		int [][] tem = new int[HWLDY][HWLDX];
		for (int x = 0; x < HWLDX; x++)
		{
			for (int y = 0; y < HWLDY; y++)
			{
				int plevel = 0;
				int lvflag = 0;
				int zx = 2*x;
				int zy = 2*y;

				for (int mx = zx; mx <= zx+1; mx++)
				{
					for (int my = zy; my <= zy+1; my++)
					{
						int tile = (map[my][mx] & LOMASK);
						if (tile != DIRT)
						{
							if (tile < RUBBLE) //natural land features
							{
								//inc terrainMem
								qtem[y/2][x/2] += 15;
								continue;
							}
							plevel += getPollutionValue(tile);
							if (tile >= ROADBASE)
								lvflag++;
						}
					}
				}

				if (plevel < 0)
					plevel = 250; //?

				if (plevel > 255)
					plevel = 255;

				tem[y][x] = plevel;

				if (lvflag != 0)
				{
					//land value equation


					int dis = 34 - getDisCC(x, y);
					dis *= 4;
					dis += terrainMem[y/2][x/2];
					dis -= pollutionMem[y][x];
					if (crimeMem[y][x] > 190) {
						dis -= 20;
					}
					if (dis > 250)
						dis = 250;
					if (dis < 1)
						dis = 1;
					landValueMem[y][x] = dis;
					landValueTotal += dis;
					landValueCount++;
				}
				else
				{
					landValueMem[y][x] = 0;
				}
			}
		}

		landValueAverage = landValueCount != 0 ? (landValueTotal/landValueCount) : 0;

		tem = doSmooth(tem);
		tem = doSmooth(tem);

		int pcount = 0;
		int ptotal = 0;
		int pmax = 0;
		for (int x = 0; x < HWLDX; x++)
		{
			for (int y = 0; y < HWLDY; y++)
			{
				int z = tem[y][x];
				pollutionMem[y][x] = z;

				if (z != 0)
				{
					pcount++;
					ptotal += z;

					if (z > pmax ||
						(z == pmax && PRNG.nextInt(4) == 0))
					{
						pmax = z;
						pollutionMaxLocationX = 2*x;
						pollutionMaxLocationY = 2*y;
					}
				}
			}
		}

		pollutionAverage = pcount != 0 ? (ptotal / pcount) : 0;

		terrainMem = smoothTerrain(qtem);

		fireMapOverlayDataChanged(MapState.POLLUTE_OVERLAY);   //PLMAP
		fireMapOverlayDataChanged(MapState.LANDVALUE_OVERLAY); //LVMAP
	}

	public CityLocation getLocationOfMaxPollution()
	{
		return new CityLocation(pollutionMaxLocationX, pollutionMaxLocationY);
	}

	static final int [] TaxTable = {
		200, 150, 120, 100, 80, 50, 30, 0, -10, -40, -100,
		-150, -200, -250, -300, -350, -400, -450, -500, -550, -600 };

	public static class History
	{
		public int cityTime;
		public int [] res = new int[240];
		public int [] com = new int[240];
		public int [] ind = new int[240];
		public int [] money = new int[240];
		public int [] pollution = new int[240];
		public int [] crime = new int[240];
		int resMax;
		int comMax;
		int indMax;
	}
	public History history = new History();

	void setValves()
	{
		double normResPop = (double)resPop / 8.0;
		totalPop = (int) (normResPop + comPop + indPop);

		double employment;
		if (normResPop != 0.0)
		{
			employment = (history.com[1] + history.ind[1]) / normResPop;
		}
		else
		{
			employment = 1;
		}

		double migration = normResPop * (employment - 1);
		final double BIRTH_RATE = 0.02;
		double births = (double)normResPop * BIRTH_RATE;
		double projectedResPop = normResPop + migration + births;

		double temp = (history.com[1] + history.ind[1]);
		double laborBase;
		if (temp != 0.0)
		{
			laborBase = history.res[1] / temp;
		}
		else
		{
			laborBase = 1;
		}

		// clamp laborBase to between 0.0 and 1.3
		laborBase = Math.max(0.0, Math.min(1.3, laborBase));

		double internalMarket = (double)(normResPop + comPop + indPop) / 3.7;
		double projectedComPop = internalMarket * laborBase;

		int z = gameLevel;
		temp = 1.0;
		switch (z)
		{
		case 0: temp = 1.2; break;
		case 1: temp = 1.1; break;
		case 2: temp = 0.98; break;
		}

		double projectedIndPop = indPop * laborBase * temp;
		if (projectedIndPop < 5.0)
			projectedIndPop = 5.0;

		double resRatio;
		if (normResPop != 0)
		{
			resRatio = (double)projectedResPop / (double)normResPop;
		}
		else
		{
			resRatio = 1.3;
		}

		double comRatio;
		if (comPop != 0)
			comRatio = (double)projectedComPop / (double)comPop;
		else
			comRatio = projectedComPop;

		double indRatio;
		if (indPop != 0)
			indRatio = (double)projectedIndPop / (double)indPop;
		else
			indRatio = projectedIndPop;

		if (resRatio > 2.0)
			resRatio = 2.0;

		if (comRatio > 2.0)
			comRatio = 2.0;

		if (indRatio > 2.0)
			indRatio = 2.0;

		int z2 = taxEffect + gameLevel;
		if (z2 > 20)
			z2 = 20;

		resRatio = (resRatio - 1) * 600 + TaxTable[z];
		comRatio = (comRatio - 1) * 600 + TaxTable[z];
		indRatio = (indRatio - 1) * 600 + TaxTable[z];

		// ratios are velocity changes to valves
		resValve += (int) resRatio;
		comValve += (int) comRatio;
		indValve += (int) indRatio;

		if (resValve > 2000)
			resValve = 2000;
		else if (resValve < -2000)
			resValve = -2000;

		if (comValve > 1500)
			comValve = 1500;
		else if (comValve < -1500)
			comValve = -1500;

		if (indValve > 1500)
			indValve = 1500;
		else if (indValve < -1500)
			indValve = -1500;


		if (resCap && resValve > 0) {
			// residents demand stadium
			resValve = 0;
		}

		if (comCap && comValve > 0) {
			// commerce demands airport
			comValve = 0;
		}

		if (indCap && indValve > 0) {
			// industry demands sea port
			indValve = 0;
		}

		fireDemandChanged();
	}

	int [][] smoothTerrain(int [][] qtem)
	{
		final int QWX = qtem[0].length;
		final int QWY = qtem.length;

		int [][] mem = new int[QWY][QWX];
		for (int y = 0; y < QWY; y++)
		{
			for (int x = 0; x < QWX; x++)
			{
				int z = 0;
				if (x > 0)
					z += qtem[y][x-1];
				if (x+1 < QWX)
					z += qtem[y][x+1];
				if (y > 0)
					z += qtem[y-1][x];
				if (y+1 < QWY)
					z += qtem[y+1][x];
				mem[y][x] = z / 4 + qtem[y][x] / 2;
			}
		}
		return mem;
	}

	// calculate manhatten distance (in 2-units) from center of city
	// capped at 32
	int getDisCC(int x, int y)
	{
		assert x >= 0 && x <= getWidth()/2;
		assert y >= 0 && y <= getHeight()/2;

		int xdis = Math.abs(x - centerMassX/2);
		int ydis = Math.abs(y - centerMassY/2);

		int z = (xdis + ydis);
		if (z > 32)
			return 32;
		else
			return z;
	}

	void mapScan(int x0, int x1)
	{
		MapScanner scanner = new MapScanner(this);

		for (int x = x0; x < x1; x++)
		{
			scanner.xpos = x;

			for (int y = 0; y < getHeight(); y++)
			{
				scanner.ypos = y;
				scanner.cchr = map[y][x];
				scanner.scanTile();
			}
		}
	}

	void generateShip()
	{
		int edge = PRNG.nextInt(4);

		if (edge == 0) {
			for (int x = 4; x < getWidth() - 2; x++) {
				if (getTile(x,0) == CHANNEL) {
					makeShipAt(x, 0, ShipSprite.NORTH_EDGE);
					return;
				}
			}
		}
		else if (edge == 1) {
			for (int y = 1; y < getHeight() - 2; y++) {
				if (getTile(0,y) == CHANNEL) {
					makeShipAt(0, y, ShipSprite.EAST_EDGE);
					return;
				}
			}
		}
		else if (edge == 2) {
			for (int x = 4; x < getWidth() - 2; x++) {
				if (getTile(x, getHeight()-1) == CHANNEL) {
					makeShipAt(x, getHeight()-1, ShipSprite.SOUTH_EDGE);
					return;
				}
			}
		}
		else {
			for (int y = 1; y < getHeight() - 2; y++) {
				if (getTile(getWidth()-1, y) == CHANNEL) {
					makeShipAt(getWidth()-1, y, ShipSprite.EAST_EDGE);
					return;
				}
			}
		}
	}

	Sprite getSprite(SpriteKind kind)
	{
		for (Sprite s : sprites) {
			if (s.kind == kind)
				return s;
		}
		return null;
	}

	boolean hasSprite(SpriteKind kind)
	{
		return getSprite(kind) != null;
	}

	void makeShipAt(int xpos, int ypos, int edge)
	{
		assert !hasSprite(SpriteKind.SHI);

		sprites.add(new ShipSprite(this, xpos, ypos, edge));
	}

	void generateCopter(int xpos, int ypos)
	{
		if (!hasSprite(SpriteKind.COP)) {
			sprites.add(new HelicopterSprite(this, xpos, ypos));
		}
	}

	void generatePlane(int xpos, int ypos)
	{
		if (!hasSprite(SpriteKind.AIR)) {
			sprites.add(new AirplaneSprite(this, xpos, ypos));
		}
	}

	void generateTrain(int xpos, int ypos)
	{
		if (totalPop > 20 &&
			!hasSprite(SpriteKind.TRA) &&
			PRNG.nextInt(26) == 0)
		{
			sprites.add(new TrainSprite(this, xpos, ypos));
		}
	}

	Stack<CityLocation> powerPlants = new Stack<CityLocation>();

	// counts the population in a certain type of residential zone
	int doFreePop(int xpos, int ypos)
	{
		int count = 0;

		for (int x = xpos - 1; x <= xpos + 1; x++)
		{
			for (int y = ypos - 1; y <= ypos + 1; y++)
			{
				if (testBounds(x,y))
				{
					char loc = (char) (map[y][x] & LOMASK);
					if (loc >= LHTHR && loc <= HHTHR)
						count++;
				}
			}
		}

		return count;
	}

	// called every several cycles; this takes the census data collected in this
	// cycle and records it to the history
	//
	void takeCensus()
	{
		int resMax = 0;
		int comMax = 0;
		int indMax = 0;

		for (int i = 118; i >= 0; i--)
		{
			if (history.res[i] > resMax)
				resMax = history.res[i];
			if (history.com[i] > comMax)
				comMax = history.res[i];
			if (history.ind[i] > indMax)
				indMax = history.ind[i];

			history.res[i + 1] = history.res[i];
			history.com[i + 1] = history.com[i];
			history.ind[i + 1] = history.ind[i];
			history.crime[i + 1] = history.crime[i];
			history.pollution[i + 1] = history.pollution[i];
			history.money[i + 1] = history.money[i];
		}

		history.resMax = resMax;
		history.comMax = comMax;
		history.indMax = indMax;

		//graph10max = Math.max(resMax, Math.max(comMax, indMax));

		history.res[0] = resPop / 8;
		history.com[0] = comPop;
		history.ind[0] = indPop;

		crimeRamp += (crimeAverage - crimeRamp) / 4;
		history.crime[0] = Math.min(255, crimeRamp);

		polluteRamp += (pollutionAverage - polluteRamp) / 4;
		history.pollution[0] = Math.min(255, polluteRamp);

		int moneyScaled = cashFlow / 20 + 128;
		if (moneyScaled < 0)
			moneyScaled = 0;
		if (moneyScaled > 255)
			moneyScaled = 255;
		history.money[0] = moneyScaled;

		history.cityTime = cityTime;

		if (hospitalCount < resPop / 256)
		{
			needHospital = 1;
		}
		else if (hospitalCount > resPop / 256)
		{
			needHospital = -1;
		}
		else
		{
			needHospital = 0;
		}

		if (churchCount < resPop / 256)
		{
			needChurch = 1;
		}
		else if (churchCount > resPop / 256)
		{
			needChurch = -1;
		}
		else
		{
			needChurch = 0;
		}
	}

	void takeCensus2()
	{
		// update long term graphs
		int resMax = 0;
		int comMax = 0;
		int indMax = 0;

		for (int i = 238; i >= 120; i--)
		{
			if (history.res[i] > resMax)
				resMax = history.res[i];
			if (history.com[i] > comMax)
				comMax = history.res[i];
			if (history.ind[i] > indMax)
				indMax = history.ind[i];

			history.res[i + 1] = history.res[i];
			history.com[i + 1] = history.com[i];
			history.ind[i + 1] = history.ind[i];
			history.crime[i + 1] = history.crime[i];
			history.pollution[i + 1] = history.pollution[i];
			history.money[i + 1] = history.money[i];
		}

		history.res[120] = resPop / 8;
		history.com[120] = comPop;
		history.ind[120] = indPop;
		history.crime[120] = history.crime[0];
		history.pollution[120] = history.pollution[0];
		history.money[120] = history.money[0];
	}

	/** Road/rail maintenance cost multiplier, for various difficulty settings.
	 */
	static final double [] RLevels = { 0.7, 0.9, 1.2 };

	/** Tax income multiplier, for various difficulty settings.
	 */
	static final double [] FLevels = { 1.4, 1.2, 0.8 };

	void collectTaxPartial()
	{
		lastRoadTotal = roadTotal;
		lastRailTotal = railTotal;
		lastTotalPop = totalPop;
		lastFireStationCount = fireStationCount;
		lastPoliceCount = policeCount;

		BudgetNumbers b = generateBudget();

		budget.taxFund += b.taxIncome;
		budget.roadFundEscrow -= b.roadFunded;
		budget.fireFundEscrow -= b.fireFunded;
		budget.policeFundEscrow -= b.policeFunded;

		taxEffect = b.taxRate;
		roadEffect = b.roadRequest != 0 ?
			(int)Math.floor(32.0 * (double)b.roadFunded / (double)b.roadRequest) :
			32;
		policeEffect = b.policeRequest != 0 ?
			(int)Math.floor(1000.0 * (double)b.policeFunded / (double)b.policeRequest) :
			1000;
		fireEffect = b.fireRequest != 0 ?
			(int)Math.floor(1000.0 * (double)b.fireFunded / (double)b.fireRequest) :
			1000;
	}

	public static class FinancialHistory
	{
		public int cityTime;
		public int totalFunds;
		public int taxIncome;
		public int operatingExpenses;
	}
	public ArrayList<FinancialHistory> financialHistory = new ArrayList<FinancialHistory>();

	void collectTax()
	{
		int revenue = budget.taxFund / TAXFREQ;
		int expenses = -(budget.roadFundEscrow + budget.fireFundEscrow + budget.policeFundEscrow) / TAXFREQ;

		FinancialHistory hist = new FinancialHistory();
		hist.cityTime = cityTime;
		hist.taxIncome = revenue;
		hist.operatingExpenses = expenses;

		cashFlow = revenue - expenses;
		spend(-cashFlow);

		hist.totalFunds = budget.totalFunds;
		financialHistory.add(0,hist);

		budget.taxFund = 0;
		budget.roadFundEscrow = 0;
		budget.fireFundEscrow = 0;
		budget.policeFundEscrow = 0;
	}

	/** Annual maintenance cost of each police station. */
	static final int POLICE_STATION_MAINTENANCE = 100;

	/** Annual maintenance cost of each fire station. */
	static final int FIRE_STATION_MAINTENANCE = 100;

	/**
	 * Calculate the current budget numbers.
	 */
	public BudgetNumbers generateBudget()
	{
		BudgetNumbers b = new BudgetNumbers();
		b.taxRate = Math.max(0, cityTax);
		b.roadPercent = Math.max(0.0, roadPercent);
		b.firePercent = Math.max(0.0, firePercent);
		b.policePercent = Math.max(0.0, policePercent);

		b.previousBalance = budget.totalFunds;
		b.taxIncome = (int)Math.round(lastTotalPop * landValueAverage / 120 * b.taxRate * FLevels[gameLevel]);
		assert b.taxIncome >= 0;

		b.roadRequest = (int)Math.round((lastRoadTotal + lastRailTotal * 2) * RLevels[gameLevel]);
		b.fireRequest = FIRE_STATION_MAINTENANCE * lastFireStationCount;
		b.policeRequest = POLICE_STATION_MAINTENANCE * lastPoliceCount;

		b.roadFunded = (int)Math.round(b.roadRequest * b.roadPercent);
		b.fireFunded = (int)Math.round(b.fireRequest * b.firePercent);
		b.policeFunded = (int)Math.round(b.policeRequest * b.policePercent);

		int yumDuckets = budget.totalFunds + b.taxIncome;
		assert yumDuckets >= 0;

		if (yumDuckets >= b.roadFunded)
		{
			yumDuckets -= b.roadFunded;
			if (yumDuckets >= b.fireFunded)
			{
				yumDuckets -= b.fireFunded;
				if (yumDuckets >= b.policeFunded)
				{
					yumDuckets -= b.policeFunded;
				}
				else
				{
					assert b.policeRequest != 0;

					b.policeFunded = yumDuckets;
					b.policePercent = (double)b.policeFunded / (double)b.policeRequest;
					yumDuckets = 0;
				}
			}
			else
			{
				assert b.fireRequest != 0;

				b.fireFunded = yumDuckets;
				b.firePercent = (double)b.fireFunded / (double)b.fireRequest;
				b.policeFunded = 0;
				b.policePercent = 0.0;
				yumDuckets = 0;
			}
		}
		else
		{
			assert b.roadRequest != 0;

			b.roadFunded = yumDuckets;
			b.roadPercent = (double)b.roadFunded / (double)b.roadRequest;
			b.fireFunded = 0;
			b.firePercent = 0.0;
			b.policeFunded = 0;
			b.policePercent = 0.0;
		}

		b.operatingExpenses = b.roadFunded + b.fireFunded + b.policeFunded;
		b.newBalance = b.previousBalance + b.taxIncome - b.operatingExpenses;

		return b;
	}

	/**
	 * The three main types of zones found in Micropolis.
	 */
	static enum ZoneType
	{
		RESIDENTIAL, COMMERCIAL, INDUSTRIAL;
	}

	TrafficGen traffic = new TrafficGen(this);

	/**
	 * @return 1 if traffic "passed", 0 if traffic "failed", -1 if no roads found
	 */
	int makeTraffic(int xpos, int ypos, ZoneType zoneType)
	{
		traffic.mapX = xpos;
		traffic.mapY = ypos;
		traffic.sourceZone = zoneType;
		return traffic.makeTraffic();
	}

	int getPopulationDensity(int xpos, int ypos)
	{
		return popDensity[ypos/2][xpos/2];
	}

	void doMeltdown(int xpos, int ypos)
	{
		meltdownLocation = new CityLocation(xpos, ypos);

		makeExplosion(xpos - 1, ypos - 1);
		makeExplosion(xpos - 1, ypos + 2);
		makeExplosion(xpos + 2, ypos - 1);
		makeExplosion(xpos + 2, ypos + 2);

		for (int x = xpos - 1; x < xpos + 3; x++) {
			for (int y = ypos - 1; y < ypos + 3; y++) {
				setTile(x, y, (char)(FIRE + PRNG.nextInt(4) + ANIMBIT));
			}
		}

		for (int z = 0; z < 200; z++) {
			int x = xpos - 20 + PRNG.nextInt(41);
			int y = ypos - 15 + PRNG.nextInt(31);
			if (!testBounds(x,y))
				continue;

			int t = map[y][x];
			if (isZoneCenter(t)) {
				continue;
			}
			if ((t & BURNBIT) != 0 || t == DIRT) {
				setTile(x, y, RADTILE);
			}
		}

		clearMes();
		sendMessageAtPic(MicropolisMessage.MELTDOWN_REPORT, xpos, ypos);
	}

	static final int [] MltdwnTab = { 30000, 20000, 10000 };

	void loadHistoryArray(int [] array, DataInputStream dis)
		throws IOException
	{
		for (int i = 0; i < 240; i++)
		{
			array[i] = dis.readShort();
		}
	}

	void writeHistoryArray(int [] array, DataOutputStream out)
		throws IOException
	{
		for (int i = 0; i < 240; i++)
		{
			out.writeShort(array[i]);
		}
	}

	void loadMisc(DataInputStream dis)
		throws IOException
	{
		dis.readShort(); //[0]... ignored?
		dis.readShort(); //[1] externalMarket, ignored
		resPop = dis.readShort();  //[2-4] populations
		comPop = dis.readShort();
		indPop = dis.readShort();
		resValve = dis.readShort(); //[5-7] valves
		comValve = dis.readShort();
		indValve = dis.readShort();
		cityTime = dis.readInt();   //[8-9] city time
		crimeRamp = dis.readShort(); //[10]
		polluteRamp = dis.readShort();
		landValueAverage = dis.readShort(); //[12]
		crimeAverage = dis.readShort();
		pollutionAverage = dis.readShort(); //[14]
		gameLevel = dis.readShort();
		evaluation.cityClass = dis.readShort();  //[16]
		evaluation.cityScore = dis.readShort();

		for (int i = 18; i < 50; i++)
		{
			dis.readShort();
		}

		budget.totalFunds = dis.readInt();   //[50-51] total funds
		autoBulldoze = dis.readShort() != 0;    //52
		autoBudget = dis.readShort() != 0;
		autoGo = dis.readShort() != 0;          //54
		dis.readShort();  // userSoundOn (this setting not saved to game file
				// in this edition of the game)
		cityTax = dis.readShort();              //56
		taxEffect = cityTax;
		int simSpeedAsInt = dis.readShort();
		if (simSpeedAsInt >= 0 && simSpeedAsInt <= 4)
			simSpeed = Speed.values()[simSpeedAsInt];
		else
			simSpeed = Speed.NORMAL;

		// read budget numbers, convert them to percentages
		//
		long n = dis.readInt();	               //58,59... police percent
		policePercent = (double)n / 65536.0;
		n = dis.readInt();                     //60,61... fire percent
		firePercent = (double)n / 65536.0;
		n = dis.readInt();                     //62,63... road percent
		roadPercent = (double)n / 65536.0;

		for (int i = 64; i < 120; i++)
		{
			dis.readShort();
		}

		if (cityTime < 0) { cityTime = 0; }
		if (cityTax < 0 || cityTax > 20) { cityTax = 7; }
		if (gameLevel < 0 || gameLevel > 2) { gameLevel = 0; }
		if (evaluation.cityClass < 0 || evaluation.cityClass > 5) { evaluation.cityClass = 0; }
		if (evaluation.cityScore < 1 || evaluation.cityScore > 999) { evaluation.cityScore = 500; }

		resCap = false;
		comCap = false;
		indCap = false;
	}

	void writeMisc(DataOutputStream out)
		throws IOException
	{
		out.writeShort(0);
		out.writeShort(0);
		out.writeShort(resPop);
		out.writeShort(comPop);
		out.writeShort(indPop);
		out.writeShort(resValve);
		out.writeShort(comValve);
		out.writeShort(indValve);
		//8
		out.writeInt(cityTime);
		out.writeShort(crimeRamp);
		out.writeShort(polluteRamp);
		//12
		out.writeShort(landValueAverage);
		out.writeShort(crimeAverage);
		out.writeShort(pollutionAverage);
		out.writeShort(gameLevel);
		//16
		out.writeShort(evaluation.cityClass);
		out.writeShort(evaluation.cityScore);
		//18
		for (int i = 18; i < 50; i++) {
			out.writeShort(0);
		}
		//50
		out.writeInt(budget.totalFunds);
		out.writeShort(autoBulldoze ? 1 : 0);
		out.writeShort(autoBudget ? 1 : 0);
		//54
		out.writeShort(autoGo ? 1 : 0);
		out.writeShort(1); //userSoundOn
		out.writeShort(cityTax);
		out.writeShort(simSpeed.ordinal());

		//58
		out.writeInt((int)(policePercent * 65536));
		out.writeInt((int)(firePercent * 65536));
		out.writeInt((int)(roadPercent * 65536));

		//64
		for (int i = 64; i < 120; i++) {
			out.writeShort(0);
		}
	}

	void loadMap(DataInputStream dis)
		throws IOException
	{
		for (int x = 0; x < DEFAULT_WIDTH; x++)
		{
			for (int y = 0; y < DEFAULT_HEIGHT; y++)
			{
				map[y][x] = (char) dis.readShort();
			}
		}
	}

	void writeMap(DataOutputStream out)
		throws IOException
	{
		for (int x = 0; x < DEFAULT_WIDTH; x++)
		{
			for (int y = 0; y < DEFAULT_HEIGHT; y++)
			{
				out.writeShort(map[y][x]);
			}
		}
	}

	public void load(File filename)
		throws IOException
	{
		FileInputStream fis = new FileInputStream(filename);
		if (fis.getChannel().size() > 27120) {
			// some editions of the classic Simcity game
			// start the file off with a 128-byte header,
			// but otherwise use the same format as us,
			// so read in that 128-byte header and continue
			// as before.
			byte [] bbHeader = new byte[128];
			fis.read(bbHeader);
		}
		load(fis);
	}

	void checkPowerMap()
	{
		coalCount = 0;
		nuclearCount = 0;

		powerPlants.clear();
		for (int y = 0; y < map.length; y++) {
			for (int x = 0; x < map[y].length; x++) {
				int tile = getTile(x,y);
				if ((tile & LOMASK) == NUCLEAR) {
					nuclearCount++;
					powerPlants.add(new CityLocation(x,y));
				}
				else if ((tile & LOMASK) == POWERPLANT) {
					coalCount++;
					powerPlants.add(new CityLocation(x,y));
				}
			}
		}

		powerScan();
		newPower = true;

		assert powerPlants.isEmpty();
	}

	public void load(InputStream inStream)
		throws IOException
	{
		DataInputStream dis = new DataInputStream(inStream);
		loadHistoryArray(history.res, dis);
		loadHistoryArray(history.com, dis);
		loadHistoryArray(history.ind, dis);
		loadHistoryArray(history.crime, dis);
		loadHistoryArray(history.pollution, dis);
		loadHistoryArray(history.money, dis);
		loadMisc(dis);
		loadMap(dis);
		dis.close();

		checkPowerMap();

		fireWholeMapChanged();
		fireDemandChanged();
		fireFundsChanged();
	}

	public void save(File filename)
		throws IOException
	{
		save(new FileOutputStream(filename));
	}

	public void save(OutputStream outStream)
		throws IOException
	{
		DataOutputStream out = new DataOutputStream(outStream);
		writeHistoryArray(history.res, out);
		writeHistoryArray(history.com, out);
		writeHistoryArray(history.ind, out);
		writeHistoryArray(history.crime, out);
		writeHistoryArray(history.pollution, out);
		writeHistoryArray(history.money, out);
		writeMisc(out);
		writeMap(out);
		out.close();
	}

	public void toggleAutoBudget()
	{
		autoBudget = !autoBudget;
		fireOptionsChanged();
	}

	public void toggleAutoBulldoze()
	{
		autoBulldoze = !autoBulldoze;
		fireOptionsChanged();
	}

	public void toggleDisasters()
	{
		noDisasters = !noDisasters;
		fireOptionsChanged();
	}

	public void setSpeed(Speed newSpeed)
	{
		simSpeed = newSpeed;
		fireOptionsChanged();
	}

	public void animate()
	{
		this.acycle = (this.acycle+1) % 960;
		if (this.acycle % 2 == 0) {
			step();
		}
		moveObjects();
		animateTiles();
	}

	public Sprite [] allSprites()
	{
		return sprites.toArray(new Sprite[0]);
	}

	void moveObjects()
	{
		for (Sprite sprite : allSprites())
		{
			sprite.move();

			if (sprite.frame == 0) {
				sprites.remove(sprite);
			}
		}
	}

	void animateTiles()
	{
		for (int y = 0; y < map.length; y++)
		{
			for (int x = 0; x < map[y].length; x++)
			{
				char tilevalue = map[y][x];
				if ((tilevalue & ANIMBIT) != 0)
				{
					int flags = tilevalue & ALLBITS;
					setTile(x, y, (char)
						(Animate.aniTile[tilevalue & LOMASK] | flags)
						);
				}
			}
		}
	}

	public int getCityPopulation()
	{
		return lastCityPop;
	}

	void makeSound(int x, int y, Sound sound)
	{
		fireCitySound(sound, new CityLocation(x,y));
	}

	public void makeEarthquake()
	{
		makeSound(centerMassX, centerMassY, Sound.EXPLOSION_LOW);
		fireEarthquakeStarted();

		sendMessageAtPic(MicropolisMessage.EARTHQUAKE_REPORT, centerMassX, centerMassY);
		int time = PRNG.nextInt(701) + 300;
		for (int z = 0; z < time; z++) {
			int x = PRNG.nextInt(getWidth());
			int y = PRNG.nextInt(getHeight());
			assert testBounds(x, y);

			if (TileConstants.isVulnerable(getTile(x, y))) {
				if (PRNG.nextInt(4) != 0) {
					setTile(x, y, (char)(RUBBLE + BULLBIT + PRNG.nextInt(4)));
				} else {
					setTile(x, y, (char)(FIRE + ANIMBIT + PRNG.nextInt(8)));
				}
			}
		}
	}

	void setFire()
	{
		int x = PRNG.nextInt(getWidth());
		int y = PRNG.nextInt(getHeight());
		int t = getTile(x, y);

		if (TileConstants.isArsonable(t)) {
			setTile(x, y, (char)(FIRE + ANIMBIT + PRNG.nextInt(8)));
			crashLocation = new CityLocation(x, y);
			sendMessageAtPic(MicropolisMessage.FIRE_REPORT, x, y);
		}
	}

	public void makeFire()
	{
		// forty attempts at finding place to start fire
		for (int t = 0; t < 40; t++)
		{
			int x = PRNG.nextInt(getWidth());
			int y = PRNG.nextInt(getHeight());
			int tile = map[y][x];
			if (!isZoneCenter(tile) && (tile & BURNBIT) != 0)
			{
				tile &= LOMASK;
				if (tile > 21 && tile < LASTZONE) {
					setTile(x, y, (char)(FIRE + ANIMBIT + PRNG.nextInt(8)));
					sendMessageAt(MicropolisMessage.FIRE_REPORT, x, y);
					return;
				}
			}
		}
	}

	/**
	 * Force a meltdown to occur.
	 * @return true if a metldown was initiated.
	 */
	public boolean makeMeltdown()
	{
		ArrayList<CityLocation> candidates = new ArrayList<CityLocation>();
		for (int y = 0; y < map.length; y++) {
			for (int x = 0; x < map[y].length; x++) {
				if ((map[y][x] & LOMASK) == NUCLEAR) {
					candidates.add(new CityLocation(x,y));
				}
			}
		}

		if (candidates.isEmpty()) {
			// tell caller that no nuclear plants were found
			return false;
		}

		int i = PRNG.nextInt(candidates.size());
		CityLocation p = candidates.get(i);
		doMeltdown(p.x, p.y);
		return true;
	}

	public void makeMonster()
	{
		MonsterSprite monster = (MonsterSprite) getSprite(SpriteKind.GOD);
		if (monster != null) {
			// already have a monster in town
			monster.soundCount = 1;
			monster.count = 1000;
			monster.flag = false;
			monster.destX = pollutionMaxLocationX;
			monster.destY = pollutionMaxLocationY;
			return;
		}

		// try to find a suitable starting spot for monster

		for (int i = 0; i < 300; i++) {
			int x = PRNG.nextInt(getWidth() - 19) + 10;
			int y = PRNG.nextInt(getHeight() - 9) + 5;
			int t = getTile(x, y);
			if ((t & LOMASK) == RIVER) {
				makeMonsterAt(x, y);
				return;
			}
		}

		// no "nice" location found, just start in center of map then
		makeMonsterAt(getWidth()/2, getHeight()/2);
	}

	void makeMonsterAt(int xpos, int ypos)
	{
		assert !hasSprite(SpriteKind.GOD);
		sprites.add(new MonsterSprite(this, xpos, ypos));
	}

	public void makeTornado()
	{
		TornadoSprite tornado = (TornadoSprite) getSprite(SpriteKind.TOR);
		if (tornado != null) {
			// already have a tornado, so extend the length of the
			// existing tornado
			tornado.count = 200;
			return;
		}

		//FIXME- this is not exactly like the original code
		int xpos = PRNG.nextInt(getWidth() - 19) + 10;
		int ypos = PRNG.nextInt(getHeight() - 19) + 10;
		sprites.add(new TornadoSprite(this, xpos, ypos));
		sendMessageAtPic(MicropolisMessage.TORNADO_REPORT, xpos, ypos);
	}

	public void makeFlood()
	{
		final int [] DX = { 0, 1, 0, -1 };
		final int [] DY = { -1, 0, 1, 0 };

		for (int z = 0; z < 300; z++) {
			int x = PRNG.nextInt(getWidth());
			int y = PRNG.nextInt(getHeight());
			int tile = map[y][x] & LOMASK;
			if (isRiverEdge(tile))
			{
				for (int t = 0; t < 4; t++) {
					int xx = x + DX[t];
					int yy = y + DY[t];
					if (testBounds(xx,yy)) {
						int c = map[yy][xx];
						if (isFloodable(c)) {
							setTile(xx, yy, FLOOD);
							floodCnt = 30;
							sendMessageAtPic(MicropolisMessage.FLOOD_REPORT, xx, yy);
							floodX = xx;
							floodY = yy;
							return;
						}
					}
				}
			}
		}
	}

	/**
	 * Makes all component tiles of a zone bulldozable.
	 * Should be called whenever the key zone tile of a zone is destroyed,
	 * since otherwise the user would no longer have a way of destroying
	 * the zone.
	 */
	void killZone(int xpos, int ypos, int zoneTile)
	{
		rateOGMem[ypos/8][xpos/8] -= 20;

		int sz = TileConstants.getZoneSizeFor(zoneTile);
		int zoneBase = (zoneTile&LOMASK)-1-sz;

		for (int y = 0; y < sz; y++) {
			for (int x = 0; x < sz; x++, zoneBase++) {
				int xtem = xpos - 1 + x;
				int ytem = ypos - 1 + y;
				if (!testBounds(xtem, ytem))
					continue;

				int t = getTile(xtem, ytem);
				if ((zoneTile & LOMASK) == POWERPLANT &&
					(t & LOMASK) >= COALSMOKE1 &&
					(t & LOMASK) < COALSMOKE4+4) {
					// animated coal smoke
					setTile(xtem, ytem, (char)(zoneBase | CONDBIT | BULLBIT));
					continue;
				}

				if ((t & LOMASK) >= ROADBASE) {
					setTile(xtem, ytem, (char)(t | BULLBIT));
				}
			}
		}
	}

	void makeExplosion(int xpos, int ypos)
	{
		makeExplosionAt(xpos*16+8, ypos*16+8);
	}

	/**
	 * Uses x,y coordinates as 1/16th-length tiles.
	 */
	void makeExplosionAt(int x, int y)
	{
		sprites.add(new ExplosionSprite(this, x, y));
	}

	void checkGrowth()
	{
		if (cityTime % 4 == 0) {
			int newPop = (resPop + comPop * 8 + indPop * 8) * 20;
			if (lastCityPop != 0) {
				MicropolisMessage z = null;
				if (lastCityPop < 500000 && newPop >= 500000) {
					z = MicropolisMessage.POP_500K_REACHED;
				} else if (lastCityPop < 100000 && newPop >= 100000) {
					z = MicropolisMessage.POP_100K_REACHED;
				} else if (lastCityPop < 50000 && newPop >= 50000) {
					z = MicropolisMessage.POP_50K_REACHED;
				} else if (lastCityPop < 10000 && newPop >= 10000) {
					z = MicropolisMessage.POP_10K_REACHED;
				} else if (lastCityPop < 2000 && newPop >= 2000) {
					z = MicropolisMessage.POP_2K_REACHED;
				}
				if (z != null) {
					sendMessage(z, true);
				}
			}
			lastCityPop = newPop;
		}
	}

	void doMessages()
	{
		//MORE (scenario stuff)

		checkGrowth();

		int totalZoneCount = resZoneCount + comZoneCount + indZoneCount;
		int powerCount = nuclearCount + coalCount;

		int z = cityTime % 64;
		switch (z) {
		case 1:
			if (totalZoneCount / 4 >= resZoneCount) {
				sendMessage(MicropolisMessage.NEED_RES);
			}
			break;
		case 5:
			if (totalZoneCount / 8 >= comZoneCount) {
				sendMessage(MicropolisMessage.NEED_COM);
			}
			break;
		case 10:
			if (totalZoneCount / 8 >= indZoneCount) {
				sendMessage(MicropolisMessage.NEED_IND);
			}
			break;
		case 14:
			if (totalZoneCount > 10 && totalZoneCount * 2 > roadTotal) {
				sendMessage(MicropolisMessage.NEED_ROADS);
			}
			break;
		case 18:
			if (totalZoneCount > 50 && totalZoneCount > railTotal) {
				sendMessage(MicropolisMessage.NEED_RAILS);
			}
			break;
		case 22:
			if (totalZoneCount > 10 && powerCount == 0) {
				sendMessage(MicropolisMessage.NEED_POWER);
			}
			break;
		case 26:
			resCap = (resPop > 500 && stadiumCount == 0);
			if (resCap) {
				sendMessage(MicropolisMessage.NEED_STADIUM);
			}
			break;
		case 28:
			indCap = (indPop > 70 && seaportCount == 0);
			if (indCap) {
				sendMessage(MicropolisMessage.NEED_SEAPORT);
			}
			break;
		case 30:
			comCap = (comPop > 100 && airportCount == 0);
			if (comCap) {
				sendMessage(MicropolisMessage.NEED_AIRPORT);
			}
			break;
		case 32:
			int TM = unpoweredZoneCount + poweredZoneCount;
			if (TM != 0) {
				if ((double)poweredZoneCount / (double)TM < 0.7) {
					sendMessage(MicropolisMessage.BLACKOUTS);
				}
			}
			break;
		case 35:
			if (pollutionAverage > 60) { // FIXME, consider changing threshold to 80
				sendMessage(MicropolisMessage.HIGH_POLLUTION, true);
			}
			break;
		case 42:
			if (crimeAverage > 100) {
				sendMessage(MicropolisMessage.HIGH_CRIME, true);
			}
			break;
		case 45:
			if (totalPop > 60 && fireStationCount == 0) {
				sendMessage(MicropolisMessage.NEED_FIRESTATION);
			}
			break;
		case 48:
			if (totalPop > 60 && policeCount == 0) {
				sendMessage(MicropolisMessage.NEED_POLICE);
			}
			break;
		case 51:
			if (cityTax > 12) {
				sendMessage(MicropolisMessage.HIGH_TAXES);
			}
			break;
		case 54:
			if (roadEffect < 20 && roadTotal > 30) {
				sendMessage(MicropolisMessage.ROADS_NEED_FUNDING);
			}
			break;
		case 57:
			if (fireEffect < 700 && totalPop > 20) {
				sendMessage(MicropolisMessage.FIRE_NEED_FUNDING);
			}
			break;
		case 60:
			if (policeEffect < 700 && totalPop > 20) {
				sendMessage(MicropolisMessage.POLICE_NEED_FUNDING);
			}
			break;
		case 63:
			if (trafficAverage > 60) {
				sendMessage(MicropolisMessage.HIGH_TRAFFIC);
			}
			break;
		default:
			//nothing
		}
	}

	void clearMes()
	{
		//TODO.
		// What this does in the original code is clears the 'last message'
		// properties, ensuring that the next message will be delivered even
		// if it is a repeat.
	}

	void sendMessage(MicropolisMessage message)
	{
		fireCityMessage(message, null, false);
	}

	void sendMessage(MicropolisMessage message, boolean isPic)
	{
		fireCityMessage(message, null, true);
	}

	void sendMessageAt(MicropolisMessage message, int x, int y)
	{
		fireCityMessage(message, new CityLocation(x,y), false);
	}

	void sendMessageAtPic(MicropolisMessage message, int x, int y)
	{
		fireCityMessage(message, new CityLocation(x,y), true);
	}

	public ZoneStatus queryZoneStatus(int xpos, int ypos)
	{
		ZoneStatus zs = new ZoneStatus();
		zs.building = getBuildingId(getTile(xpos, ypos));

		int z;
		z = (popDensity[ypos/2][xpos/2] / 64) % 4;
		zs.popDensity = z + 1;

		z = landValueMem[ypos/2][xpos/2];
		z = z < 30 ? 4 : z < 80 ? 5 : z < 150 ? 6 : 7;
		zs.landValue = z + 1;

		z = ((crimeMem[ypos/2][xpos/2] / 64) % 4) + 8;
		zs.crimeLevel = z + 1;

		z = Math.max(13,((pollutionMem[ypos/2][xpos/2] / 64) % 4) + 12);
		zs.pollution = z + 1;

		z = rateOGMem[ypos/8][xpos/8];
		z = z < 0 ? 16 : z == 0 ? 17 : z <= 100 ? 18 : 19;
		zs.growthRate = z + 1;

		return zs;
	}

	public int getResValve()
	{
		return resValve;
	}

	public int getComValve()
	{
		return comValve;
	}

	public int getIndValve()
	{
		return indValve;
	}

	public void setGameLevel(int newLevel)
	{
		assert GameLevel.isValid(newLevel);

		gameLevel = newLevel;
		fireOptionsChanged();
	}

	public void setFunds(int totalFunds)
	{
		budget.totalFunds = totalFunds;
	}
}
