Blob Blame Raw
package com.icystar.findnumber;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.text.TextPaint;
import android.util.FloatMath;
import android.view.View;
import android.widget.TextView;

public class Game {
	public static Game[] games = new Game[] { new Game(), new Game() };

	public static class Number {
		public int number;
		public String text;
		public int digits;
		public int sizeIndex;
		public float size;
		public float scaleX;
		public int fontIndex;
		Typeface font;
		public Rect rect = new Rect();
		public Point position = new Point();
		
		public void save(FileOutputStream fos) throws IOException {
			Serializer.writeInt(fos, number);
			Serializer.writeFloat(fos, size);
			Serializer.writeFloat(fos, scaleX);
			Serializer.writeInt(fos, fontIndex);
			Serializer.writeInt(fos, rect.left);
			Serializer.writeInt(fos, rect.right);
			Serializer.writeInt(fos, rect.top);
			Serializer.writeInt(fos, rect.bottom);
			Serializer.writeInt(fos, position.x);
			Serializer.writeInt(fos, position.y);
		}

		public void load(FileInputStream fis) throws IOException {
			number 		= Serializer.readInt(fis);
			size 		= Serializer.readFloat(fis);
			scaleX 		= Serializer.readFloat(fis);
			fontIndex 	= Serializer.readInt(fis);
			rect.left 	= Serializer.readInt(fis);
			rect.right 	= Serializer.readInt(fis);
			rect.top 	= Serializer.readInt(fis);
			rect.bottom = Serializer.readInt(fis);
			position.x 	= Serializer.readInt(fis);
			position.y 	= Serializer.readInt(fis);
			
			text = Integer.toString(number);
			digits = text.length();
			sizeIndex = 0;
			font = fonts[fontIndex];
		}
	}
	
	public interface Listener {
		public void onLock(Game game);
		public void onWin(Game game, float time);
		public void onTouchNumber(Game game, int touchedNumber, int maxNumber, boolean win);
	}

	private static final String filename = "game.sav";
	private static final int sizesCount = 100;
	private static final int generationTrials = 100;
	private static final int fontTrials = 100;
	private static final int positionTrials = 100;
	public static final Typeface[] fonts = new Typeface[] {
		Typeface.create(Typeface.SERIF, Typeface.NORMAL),
		Typeface.create(Typeface.SERIF, Typeface.BOLD),
		Typeface.create(Typeface.SERIF, Typeface.ITALIC),
		Typeface.create(Typeface.SERIF, Typeface.BOLD_ITALIC),
		Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL),
		Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD),
		Typeface.create(Typeface.SANS_SERIF, Typeface.ITALIC),
		Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD_ITALIC)
	};
	
	private static Object staticHandle = new Object();
	private static boolean stateLoaded = false;
	private static String[] playerNames;
	
	private int width = 0;
	private int height = 0;
	private float minSize; 
	private float maxSize; 
	private int initialMaxSizeIndex; 
	private int maxDigits; 
	private Rect[][][] sizes;

	private TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
	private TextView numberTextView;
	private TextView timeTextView;
	private TextView playerNameTextView;
	private NumbersView numbersView;
	private View lockView;
	private Listener listener;
	
	private Timer timer;
	private Object handle = new Object();
	private int waitToGenerate = 0;
	
	private List<Point> invalidTouches = new ArrayList<Point>();

	// serialization data
	private Number[] numbers = new Number[0];
	private int currentNumberIndex = -1;
	private int currentNumber = 0;
	private float time = 0;
	private float lockTime = 0;
	private boolean paused = true;
	private boolean playing = false;
	private boolean isWin = false;
	private boolean isLose = false;
	
	public void save(FileOutputStream fos) throws IOException {
		synchronized (handle) {
			Serializer.writeInt(fos, currentNumberIndex);
			Serializer.writeInt(fos, Math.round(time));
			Serializer.writeInt(fos, Math.round(lockTime));
			Serializer.writeInt(fos, paused ? 1 : 0);
			Serializer.writeInt(fos, playing ? 1 : 0);
			Serializer.writeInt(fos, numbers.length);
			for(Number number : numbers)
				number.save(fos);
		}
	}

	public void load(FileInputStream fis) throws IOException {
		synchronized (handle) {
			currentNumberIndex	= Serializer.readInt(fis);
			time				= Serializer.readInt(fis);
			lockTime			= Serializer.readInt(fis);
			paused				= Serializer.readInt(fis) != 0;
			playing				= Serializer.readInt(fis) != 0;
			numbers = new Number[Serializer.readInt(fis)];
			for(int i = 0; i < numbers.length; i++) {
				numbers[i] = new Number();
				numbers[i].load(fis);
			}
			currentNumber = currentNumberIndex >= 0 && currentNumberIndex < numbers.length
			              ? numbers[currentNumberIndex].number : 0; 
		}
	}
	

	Game() {
		timer = new Timer();
		timer.schedule(new TimerTask() {
			@Override
			public void run() { onTime(1); }
		}, 1000, 1000);
	}
	
	private float internalCalcSize(int index) {
		return minSize + (maxSize - minSize)*(float)index/(float)(sizesCount-1);
	}
	
	private void internalCalcSizes(int count) {
		maxSize = Math.min(width, height)/2f;
		minSize = count < 50 ? maxSize/5f : maxSize/10f;
		initialMaxSizeIndex = sizesCount/2;
		if (count > 25) initialMaxSizeIndex = sizesCount/4;
		if (count > 50) initialMaxSizeIndex = sizesCount/6;
		if (initialMaxSizeIndex < 1) initialMaxSizeIndex = 1;

		String template = "MMMMMMMMMMMMMMMMMMMMMMMMMMMMM";
		sizes = new Rect[fonts.length][sizesCount][maxDigits + 1];
		paint.setTextScaleX(1f);
		for(int fontIndex = 0; fontIndex < fonts.length; fontIndex++) {
			paint.setTypeface(fonts[fontIndex]);
			for(int sizeIndex = 0; sizeIndex < sizesCount; sizeIndex++) {
				paint.setTextSize(internalCalcSize(sizeIndex));
				for(int digits = 0; digits <= maxDigits; digits++) {
					sizes[fontIndex][sizeIndex][digits] = new Rect();
					paint.getTextBounds(template, 0, digits, sizes[fontIndex][sizeIndex][digits]);
				}
			}
		}
	}

	private boolean internalCheckRect(Rect rect, int count, int exclude) {
		if (rect.left < 0 || rect.top < 0 || rect.right > width || rect.bottom > height)
			return false;
		for(int i = 0; i < count; i++)
			if (Rect.intersects(rect, numbers[i].rect))
				return false;
		return true;
	}
	
	private void internalGenerate(int count) {
		waitToGenerate = 0;
		time = 0;
		lockTime = 0;
		playing = false;
		isWin = false;
		isLose = false;
		if (timer != null) timer.cancel();
		timer = null;
		
		maxDigits = count < 10 ? 1 : (count < 100 ? 2 : (count < 1000 ? 3 : 4));
		internalCalcSizes(count);
		
		numbers = new Number[count];
		Random random = new Random();
		
		// fill digits
		int[] remainingNumbers = new int[count];
		for(int i = 0; i < count; i++) remainingNumbers[i] = i+1;
		for(int i = 0; i < count; i++) {
			int j = random.nextInt(count - i);
			numbers[i] = new Number();
			numbers[i].number = remainingNumbers[j];
			numbers[i].text = Integer.toString(numbers[i].number);
			numbers[i].digits = numbers[i].text.length();
			for(int k = j+1; k < count-i; k++) remainingNumbers[k-1] = remainingNumbers[k];
		}

		// generate numbers
		boolean success = false;
		for(int generationTry = 0; !success && generationTry < generationTrials; generationTry++) {
			for(int i = 0; i < count; i++) {
				success = false;
				Number number = numbers[i];
				for(int fontTry = 0; !success && fontTry < fontTrials && fontTry < initialMaxSizeIndex; fontTry++) {
					number.fontIndex = random.nextInt(fonts.length);
					number.sizeIndex = random.nextInt(initialMaxSizeIndex - fontTry);
					number.font = fonts[number.fontIndex];
					number.size = internalCalcSize(number.sizeIndex);					
					number.scaleX = 1f; //random.nextFloat()*1.5f + 0.5f;
					Rect r = sizes[number.fontIndex][number.sizeIndex][number.digits];
					int w = Math.round((r.right - r.left)*number.scaleX);
					int h = r.bottom - r.top;
					if (w < width && h < height) {
						for(int positionTry = 0; !success && positionTry < positionTrials; positionTry++) {
							int x = random.nextInt(width - w);
							int y = random.nextInt(height - h);
							number.rect.set(x, y, x+w, y+h);
							number.position.set(number.rect.left - r.left, number.rect.top - r.top);
							if (internalCheckRect(number.rect, i, -1))
								success = true;
						}
					}
				}
				if (!success) break;
			}
		}
		
		if (success) {
			// enlarge numbers
			boolean enlarged = false;
			Rect[] rects = new Rect[] { new Rect(), new Rect(), new Rect(), new Rect() }; 
			while (enlarged) {
				enlarged = true;
				for(int i = 0; i < count; i++) {
					Number number = numbers[i];
					if (number.sizeIndex < sizesCount) {
						Rect r = sizes[number.fontIndex][number.sizeIndex+1][number.digits];
						int w = Math.round((r.right - r.left)*number.scaleX);
						int h = r.bottom - r.top;
						int validCount = 0;
						for(int j = 0; j < 3; j++) {
							int x = j % 2 == 0 ? number.rect.left : number.rect.right - w; 
							int y = j / 2 == 0 ? number.rect.top : number.rect.bottom - h;
							rects[validCount].set(x, y, x+w, y+h);
							if (internalCheckRect(rects[validCount], count, i)) validCount++;
						}
						if (validCount > 0) {
							number.sizeIndex++;
							number.size = internalCalcSize(number.sizeIndex);
							number.rect.set(rects[random.nextInt(validCount)]);
							number.position.set(number.rect.left - r.left, number.rect.top - r.top);
							enlarged = true;
						}
					}
				}
			}
		} else {
			numbers = new Number[0];
		}
		
		time = 0;
		lockTime = 0;
		playing = true;
		internalSetCurrentNumber(1);
		internalUpdateViews();
		timer = new Timer();
		timer.schedule(new TimerTask() {
			@Override
			public void run() { onTime(1); }
		}, 1000, 1000);
	}
	
	private static boolean checkDistance(int x0, int y0, int x1, int y1, int distSqr)
		{ return (x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) < distSqr; }
	
	private boolean internalCheckTouch(int x, int y) {
		Number number = null;
		try { number = numbers[currentNumberIndex]; } catch (Exception e) { }
		if (number != null) {
			int radius = Math.min(width, height)/20;
			int radiusSqr = radius*radius;
			Rect rect = number.rect;

			if (new Rect(rect.left - radius, rect.top, rect.right + 2*radius, rect.bottom ).contains(x, y)
			 || new Rect(rect.left, rect.top - radius, rect.right, rect.bottom + 2*radius ).contains(x, y)
			 || checkDistance(x, y, rect.left, rect.top, radiusSqr) 
			 || checkDistance(x, y, rect.right, rect.top, radiusSqr) 
			 || checkDistance(x, y, rect.left, rect.bottom, radiusSqr) 
			 || checkDistance(x, y, rect.right, rect.bottom, radiusSqr) 
			) {
				return true;
			}
		}
		return false;
	}
	
	private void internalSetCurrentNumber(int value) {
		currentNumberIndex = -1;
		currentNumber = 0;
		for(int i = 0; i < numbers.length; i++)
			if (numbers[i].number == value)
				{ currentNumberIndex = i; currentNumber = value; break; }
		internalUpdateNumberTextView();
	}

	private void internalUpdateNumberTextView() {
		if (numberTextView != null) {
			int numbersCount = numbers.length; 
			int numbersFound = isWin ? numbersCount : currentNumber - 1;
			int id = R.string.currentNumber;
			if (isLose) id = R.string.currentNumberLose;
			if (isWin) { id = R.string.currentNumberWin; numbersFound = numbersCount; } 
			
			final TextView textView = numberTextView;
			final String text = textView.getContext().getString(id, currentNumber, numbersFound, numbersCount);
			textView.post(new Runnable() { @Override public void run() { textView.setText(text); } });
		}
	}

	private void internalUpdateTimeTextView() {
		if (timeTextView != null) {
			int min = (int)FloatMath.floor(time/60);
			int sec = (int)FloatMath.floor(time - min*60);
			String minStr = Integer.toString(min);
			String secStr = Integer.toString(sec);
			while (minStr.length()  < 2) minStr = "0" + minStr;
			while (secStr.length()  < 2) secStr = "0" + secStr;

			final TextView textView = timeTextView;
			final String text = minStr + ":" + secStr;
			textView.post(new Runnable() { @Override public void run() { textView.setText(text); } });
		}
	}
	
	private void internalUpdatePlayerNameTextView() {
		if (playerNameTextView != null) {
			final TextView textView = playerNameTextView;
			final int visibility = playing ? View.VISIBLE : View.GONE;
			textView.post(new Runnable() { @Override public void run() { textView.setVisibility(visibility); } });
		}
	}
	
	private void internalUpdateNumbersView() {
		if (numbersView != null) {
			final NumbersView view = numbersView;
			view.post(new Runnable() { @Override public void run() { view.invalidate(); } });
		}
	}
	
	private void internalUpdateLockView() {
		if (lockView != null) {
			final View view = lockView;
			final int visibility = lockTime <= 0 ? View.GONE : View.VISIBLE;
			view.post(new Runnable() { @Override public void run() {
				if (view.getVisibility() != visibility) view.setVisibility(visibility);
				if (visibility == View.VISIBLE) view.invalidate();
			} });
		}
	}
	
	private void internalUpdateViews() {
		internalUpdateNumberTextView();
		internalUpdateTimeTextView();
		internalUpdatePlayerNameTextView();
		internalUpdateNumbersView();
		internalUpdateLockView();
	}

	
	
	public void start(int count) {
		synchronized (handle) {
			if (width > 0 && height > 0)
				internalGenerate(count);
			else
				waitToGenerate = count;
		}
	}
	
	public void stop() {
		synchronized (handle) {
			playing = false;
			isWin = false;
			isLose = false;
			numbers = new Number[0];
			internalUpdateNumberTextView();
			internalUpdatePlayerNameTextView();
		}
	}
	
	public void lose() {
		synchronized (handle) {
			playing = false;
			isWin = false;
			isLose = true;
			internalUpdateNumberTextView();
			internalUpdatePlayerNameTextView();
		}
	}
	
	public void win() {
		synchronized (handle) {
			playing = false;
			isWin = true;
			isLose = false;
			internalUpdateNumberTextView();
			internalUpdatePlayerNameTextView();
		}
	}

	public static void lock() {
		for(Game game : games) {
			synchronized (game.handle) {
				game.lockTime = 5;
				game.internalUpdateLockView();
			}
			if (game.listener != null) game.listener.onLock(game);
		}
	}
	
	public boolean getPlaying()
		{ synchronized (handle) { return playing; } }
	public boolean getPaused()
		{ synchronized (handle) { return paused; } }
	public void setPaused(boolean value)
		{ synchronized (handle) { paused = value; } }
	public int getCount()
		{ synchronized (handle) { return numbers.length; } }
	
	public int getCurrentNumber() {
		synchronized (handle) {
			try { return numbers[currentNumberIndex].number; } catch (Exception e) { }
			return 0;
		}
	}

	public void onLayout(int width, int height) {
		synchronized (handle) {
			this.width = width;
			this.height = height;
			if (width > 0 && height > 0 && waitToGenerate > 0)
				internalGenerate(waitToGenerate);
		}
	}
	
	public void onTouch(int x, int y) {
		boolean touch = false;
		int touchedNumber = 0;
		int maxNumber = 0;

		boolean win = false;
		float winTime = 0;
		
		synchronized (handle) {
			if (playing && !paused && lockTime <= 0) {
				if (internalCheckTouch(x, y)) {
					touch = true;
					touchedNumber = currentNumber;
					maxNumber = numbers.length;
					if (currentNumber < numbers.length) {
						internalSetCurrentNumber(currentNumber + 1);
					} else {
						this.win();
						win = true;
						winTime = time;
					}
					invalidTouches.clear();
				} else {
					invalidTouches.add(new Point(x, y));
					if (invalidTouches.size() > 1) {
						int maxDist = Math.min(width, height)/4;
						int places = 1;
						Point min = new Point(width, height);
						Point max = new Point(0, 0);
						for(Point p : invalidTouches) {
							min.x = Math.min(min.x, p.x);
							min.y = Math.min(min.y, p.y);
							max.x = Math.max(max.x, p.x);
							max.y = Math.max(max.y, p.y);
							if (max.x - min.x > maxDist || max.y - min.y > maxDist) {
								places++;
								min.set(p.x, p.y);
								max.set(p.x, p.y);
							}
						}
						if (places > 3) {
							lock();
							invalidTouches.clear();
						}
					}
				}
			}
		}
		
		if (touch && listener != null) listener.onTouchNumber(this, touchedNumber, maxNumber, win);
		if (win && listener != null) listener.onWin(this, winTime);
	}
	
	public void onTime(float time) {
		synchronized (handle) {
			if (playing && !paused) {
				this.time += time;
				lockTime -= time;
				if (lockTime <= 0) lockTime = 0;
				internalUpdateTimeTextView();
				internalUpdateLockView();
				if (Math.round(this.time) % 3 == 0 && invalidTouches.size() > 0)
					invalidTouches.remove(0);
			}
		}
	}

	public void draw(Canvas canvas) {
		synchronized (handle) {
			for(int i = 0; i < numbers.length; i++) {
				Number number = numbers[i];
				paint.setTypeface(number.font);
				paint.setTextSize(number.size);
				paint.setTextScaleX(number.scaleX);
				canvas.drawText(number.text, number.position.x, number.position.y, paint);
			}
		}
	}
	
	public void setListener(Listener value)
		{ synchronized (handle) { listener = value; } }
	public void setNumberTextView(TextView value)
		{ synchronized (handle) { numberTextView = value; internalUpdateNumberTextView(); } }
	public void setTimeTextView(TextView value)
		{ synchronized (handle) { timeTextView = value; internalUpdateTimeTextView(); } }
	public void setPlayerNameTextView(TextView value)
		{ synchronized (handle) { playerNameTextView = value; internalUpdatePlayerNameTextView(); } }
	public void setNumbersView(NumbersView value)
		{ synchronized (handle) { numbersView = value; internalUpdateNumbersView(); } }
	public void setLockView(View value)
		{ synchronized (handle) { lockView = value; internalUpdateLockView(); } }
	
	private static void save(Context context) {
		try {
			FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE);
			for(Game game : games) game.save(fos);
			Serializer.writeInt(fos, playerNames.length);
			if (playerNames == null) playerNames = new String[0];
			for(String name : playerNames)
				Serializer.writeString(fos, name);
			ScoreboardActivity.save(fos);
			Serializer.writeInt(fos, SoundPlayer.getEnabled() ? 1 : 0);
		} catch (Exception e) { }
	}
	
	private static void load(Context context) {
		for(Game game : games) game.stop();
		
		try {
			FileInputStream fis = context.openFileInput(filename);
			for(Game game : games) game.load(fis);
			playerNames = new String[Serializer.readInt(fis)];
			for(int i = 0; i < playerNames.length; i++)
				playerNames[i] = Serializer.readString(fis);
			ScoreboardActivity.load(fis);
			SoundPlayer.setEnabled(Serializer.readInt(fis) != 0);
		} catch (Exception e) {
			for(Game game : games) game.stop();
			//e.printStackTrace();
		}
		if (playerNames == null) playerNames = new String[0];
	}
	
	public static void saveState(Context context) {
		synchronized (staticHandle) {
			save(context);
		}
	}
	
	public static void loadState(Context context) {
		synchronized (staticHandle) {
			if (!stateLoaded) {
				stateLoaded = true;
				load(context);
			}
		}
	}
	
	public static int getMode() {
		boolean g0 = games[0].getCount() > 0;
		boolean g1 = games[1].getCount() > 0;
		if (g0 && g1) return 2;	
		if (g0 && !g1) return 1;
		return 0;
	}
	
	public static boolean getAnyPlaying() {
		switch (getMode()) {
		case 1:
			return games[0].getPlaying();
		case 2:
			return games[0].getPlaying() || games[1].getPlaying();
		}
		return false;
	}
	
	public static String getPlayerName(int index) {
		synchronized (staticHandle) {
			try { return playerNames[index] == null ? "" : playerNames[index]; } catch (Exception e) { }
			return "";
		}
	}

	public static void setPlayerName(int index, String name) {
		synchronized (staticHandle) {
			if (name != null && name.length() > 0) {
				try {
					playerNames = Serializer.enlargeArrayIfNeed(String.class, playerNames, index+1);
					playerNames[index] = name;
				} catch (Exception e) {	}
			}
		}
	}
}