com.shurik.droidzebra.ZebraEngine.java Source code

Java tutorial

Introduction

Here is the source code for com.shurik.droidzebra.ZebraEngine.java

Source

/* Copyright (C) 2010 by Alex Kompel  */
/* This file is part of DroidZebra.
    
   DroidZebra is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.
    
   DroidZebra is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.
    
   You should have received a copy of the GNU General Public License
   along with DroidZebra.  If not, see <http://www.gnu.org/licenses/>
*/

package com.shurik.droidzebra;

import android.content.Context;
import android.content.res.AssetManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
//import android.util.Log;

// DroidZebra -> ZebraEngine:public -async-> ZebraEngine thread(jni) -> Callback() -async-> DroidZebra:Handler
public class ZebraEngine extends Thread {

    static public final int BOARD_SIZE = 8;
    // board colors
    static public final byte PLAYER_BLACK = 0;
    static public final byte PLAYER_EMPTY = 1; // for board color
    static public final byte PLAYER_WHITE = 2;
    static public final int PLAYER_ZEBRA = 1; // for zebra skill in PlayerInfo
    // default parameters
    static public final int INFINIT_TIME = 10000000;
    // messages
    static public final int MSG_ERROR = 0, MSG_BOARD = 1, MSG_CANDIDATE_MOVES = 2, MSG_GET_USER_INPUT = 3,
            MSG_PASS = 4, MSG_OPENING_NAME = 5, MSG_LAST_MOVE = 6, MSG_GAME_START = 7, MSG_GAME_OVER = 8,
            MSG_MOVE_START = 9, MSG_MOVE_END = 10, MSG_EVAL_TEXT = 11, MSG_PV = 12, MSG_CANDIDATE_EVALS = 13,
            MSG_DEBUG = 65535;
    // engine state
    static public final int ES_INITIAL = 0, ES_READY2PLAY = 1, ES_PLAY = 2, ES_PLAYINPROGRESS = 3,
            ES_USER_INPUT_WAIT = 4;
    static public final int UI_EVENT_EXIT = 0, UI_EVENT_MOVE = 1, UI_EVENT_UNDO = 2, UI_EVENT_SETTINGS_CHANGE = 3,
            UI_EVENT_REDO = 4;
    private static final String[] coeffAssets = { "coeffs2.bin" };
    private static final String[] bookCompressedAssets = { "book.cmp.z", };
    // synchronization
    static private final Object mJNILock = new Object();
    static public String PATTERNS_FILE = "coeffs2.bin";
    static public String BOOK_FILE = "book.bin";
    static public String BOOK_FILE_COMPRESSED = "book.cmp.z";
    static private int SELFPLAY_MOVE_DELAY = 500; // ms

    static {
        System.loadLibrary("droidzebra");
    }

    private int mMoveDelay = 0;
    private long mMoveStartTime = 0; //ms

    ;
    private int mMovesWithoutInput = 0;

    ;
    private GameState mInitialGameState;

    ;
    private GameState mCurrentGameState;
    // current move
    private JSONObject mPendingEvent = null;

    ;
    private int mValidMoves[] = null;
    // player info
    private PlayerInfo[] mPlayerInfo = { new PlayerInfo(PLAYER_BLACK, 0, 0, 0, INFINIT_TIME, 0),
            new PlayerInfo(PLAYER_ZEBRA, 4, 12, 12, INFINIT_TIME, 0),
            new PlayerInfo(PLAYER_WHITE, 0, 0, 0, INFINIT_TIME, 0) };
    private boolean mPlayerInfoChanged = false;
    private int mSideToMove = PLAYER_ZEBRA;
    // context
    private Context mContext;
    // message sink
    private Handler mHandler;
    // files folder
    private File mFilesDir;
    private Object mEngineStateEvent = new Object();
    private int mEngineState = ES_INITIAL;
    private boolean mRun = false;
    private boolean bInCallback = false;

    public ZebraEngine(Context context, Handler handler) {
        mContext = context;
        mHandler = handler;
    }

    public boolean initFiles() {
        mFilesDir = null;

        // first check if files exist on internal device
        File pattern = new File(mContext.getFilesDir(), PATTERNS_FILE);
        File book = new File(mContext.getFilesDir(), BOOK_FILE);
        if (pattern.exists() && book.exists()) {
            mFilesDir = mContext.getFilesDir();
            return true;
        }

        // if not - try external folder
        try {
            if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)) {
                File extDir = new File(android.os.Environment.getExternalStorageDirectory(),
                        "/ZebraActivity/files/");
                _prepareZebraFolder(extDir); //may throw
                mFilesDir = extDir;
            }
        } catch (IOException e) {
            mFilesDir = null;
        }

        // if external did not work out - try internal
        if (mFilesDir == null) {
            try {
                _prepareZebraFolder(mContext.getFilesDir());
                mFilesDir = mContext.getFilesDir();
            } catch (IOException e) {
                fatalError(e.getMessage());
                return false;
            }
        }
        return true;
    }

    public void waitForEngineState(int state, int milliseconds) {
        synchronized (mEngineStateEvent) {
            if (mEngineState != state)
                try {
                    mEngineStateEvent.wait(milliseconds);
                } catch (InterruptedException e) {
                }
        }
    }

    public void waitForEngineState(int state) {
        synchronized (mEngineStateEvent) {
            while (mEngineState != state && mRun)
                try {
                    mEngineStateEvent.wait();
                } catch (InterruptedException e) {
                }
        }
    }

    public int getEngineState() {
        return mEngineState;
    }

    public void setEngineState(int state) {
        synchronized (mEngineStateEvent) {
            mEngineState = state;
            mEngineStateEvent.notifyAll();
        }
    }

    public boolean gameInProgress() {
        return zeGameInProgress();
    }

    public void setRunning(boolean b) {
        boolean oldRun = mRun;
        mRun = b;
        if (oldRun && !mRun)
            stopGame();
    }

    // tell zebra to stop thinking
    public void stopMove() {
        zeForceReturn();
    }

    // tell zebra to end current game
    public void stopGame() {
        zeForceExit();
        // if waiting for move - get back into the engine
        // every other state should work itself out
        if (getEngineState() == ES_USER_INPUT_WAIT) {
            mPendingEvent = new JSONObject();
            try {
                mPendingEvent.put("type", UI_EVENT_EXIT);
            } catch (JSONException e) {
                // Log.getStackTraceString(e);
            }
            setEngineState(ES_PLAY);
        }
    }

    public void makeMove(Move move) throws InvalidMove, EngineError {
        if (!isValidMove(move))
            throw new InvalidMove();

        // if thinking on human time - stop
        if (mPlayerInfo[mSideToMove].skill == 0 && getEngineState() == ZebraEngine.ES_PLAYINPROGRESS) {
            stopMove();
            waitForEngineState(ES_USER_INPUT_WAIT, 1000);
        }

        if (getEngineState() != ES_USER_INPUT_WAIT) {
            // Log.d("ZebraEngine", "Invalid Engine State");
            return;
        }

        // add move the the pending event and tell zebra to pick it up
        mPendingEvent = new JSONObject();
        try {
            mPendingEvent.put("type", UI_EVENT_MOVE);
            mPendingEvent.put("move", move.mMove);
        } catch (JSONException e) {
            // Log.getStackTraceString(e);
        }
        setEngineState(ES_PLAY);
    }

    public void undoMove() throws EngineError {
        // if thinking on human time - stop
        if (mPlayerInfo[mSideToMove].skill == 0 && getEngineState() == ZebraEngine.ES_PLAYINPROGRESS) {
            stopMove();
            waitForEngineState(ES_USER_INPUT_WAIT, 1000);
        }

        if (getEngineState() != ES_USER_INPUT_WAIT) {
            // Log.d("ZebraEngine", "Invalid Engine State");
            return;
        }

        // create pending event and tell zebra to pick it up
        mPendingEvent = new JSONObject();
        try {
            mPendingEvent.put("type", UI_EVENT_UNDO);
        } catch (JSONException e) {
            // Log.getStackTraceString(e);
        }
        setEngineState(ES_PLAY);
    }

    public void redoMove() throws EngineError {
        // if thinking on human time - stop
        if (mPlayerInfo[mSideToMove].skill == 0 && getEngineState() == ZebraEngine.ES_PLAYINPROGRESS) {
            stopMove();
            waitForEngineState(ES_USER_INPUT_WAIT, 1000);
        }

        if (getEngineState() != ES_USER_INPUT_WAIT) {
            // Log.d("ZebraEngine", "Invalid Engine State");
            return;
        }

        // create pending event and tell zebra to pick it up
        mPendingEvent = new JSONObject();
        try {
            mPendingEvent.put("type", UI_EVENT_REDO);
        } catch (JSONException e) {
            // Log.getStackTraceString(e);
        }
        setEngineState(ES_PLAY);
    }

    // notifications that some settings have changes - see if we care
    public void sendSettingsChanged() {
        // if we are waiting for input - restart the move (e.g. if sides switched)
        if (getEngineState() == ES_USER_INPUT_WAIT) {
            mPendingEvent = new JSONObject();
            try {
                mPendingEvent.put("type", UI_EVENT_SETTINGS_CHANGE);
            } catch (JSONException e) {
                // Log.getStackTraceString(e);
            }
            setEngineState(ES_PLAY);
        }
    }

    // settings helpers
    public void setAutoMakeMoves(boolean _settingAutoMakeForcedMoves) {
        if (_settingAutoMakeForcedMoves)
            zeSetAutoMakeMoves(1);
        else
            zeSetAutoMakeMoves(0);
    }

    public void setSlack(float _slack) {
        zeSetSlack(_slack);
    }

    public void setPerturbation(float _perturbation) {
        zeSetPerturbation(_perturbation);
    }

    public void setForcedOpening(String _openingName) {
        zeSetForcedOpening(_openingName);
    }

    public void setHumanOpenings(boolean _enable) {
        if (_enable)
            zeSetHumanOpenings(1);
        else
            zeSetHumanOpenings(0);
    }

    public void setPracticeMode(boolean _enable) {
        if (_enable)
            zeSetPracticeMode(1);
        else
            zeSetPracticeMode(0);
    }

    public void setUseBook(boolean _enable) {
        if (_enable)
            zeSetUseBook(1);
        else
            zeSetUseBook(0);
    }

    public void setPlayerInfo(PlayerInfo playerInfo) throws EngineError {
        if (playerInfo.playerColor != PLAYER_BLACK && playerInfo.playerColor != PLAYER_WHITE
                && playerInfo.playerColor != PLAYER_ZEBRA)
            throw new EngineError(String.format("Invalid player type %d", playerInfo.playerColor));

        mPlayerInfo[playerInfo.playerColor] = playerInfo;

        mPlayerInfoChanged = true;
    }

    public void setMoveDelay(int delay) {
        mMoveDelay = delay;
    }

    // gamestate manipulators
    public void setInitialGameState(int moveCount, byte[] moves) {
        mInitialGameState = new GameState();
        mInitialGameState.mDisksPlayed = moveCount;
        mInitialGameState.mMoveSequence = new byte[moveCount];
        for (int i = 0; i < moveCount; i++) {
            mInitialGameState.mMoveSequence[i] = moves[i];
        }
    }

    public GameState getGameState() {
        return mCurrentGameState;
    }

    // zebra thread
    @Override
    public void run() {
        setRunning(true);

        setEngineState(ES_INITIAL);

        // init data files
        if (!initFiles())
            return;

        synchronized (mJNILock) {
            zeGlobalInit(mFilesDir.getAbsolutePath());
            zeSetPlayerInfo(PLAYER_BLACK, 0, 0, 0, INFINIT_TIME, 0);
            zeSetPlayerInfo(PLAYER_WHITE, 0, 0, 0, INFINIT_TIME, 0);
        }

        setEngineState(ES_READY2PLAY);

        while (mRun) {
            waitForEngineState(ES_PLAY);

            if (!mRun)
                break; // something may have happened while we were waiting

            setEngineState(ES_PLAYINPROGRESS);

            synchronized (mJNILock) {
                zeSetPlayerInfo(PLAYER_BLACK, mPlayerInfo[PLAYER_BLACK].skill,
                        mPlayerInfo[PLAYER_BLACK].exactSolvingSkill, mPlayerInfo[PLAYER_BLACK].wldSolvingSkill,
                        mPlayerInfo[PLAYER_BLACK].playerTime, mPlayerInfo[PLAYER_BLACK].playerTimeIncrement);
                zeSetPlayerInfo(PLAYER_WHITE, mPlayerInfo[PLAYER_WHITE].skill,
                        mPlayerInfo[PLAYER_WHITE].exactSolvingSkill, mPlayerInfo[PLAYER_WHITE].wldSolvingSkill,
                        mPlayerInfo[PLAYER_WHITE].playerTime, mPlayerInfo[PLAYER_WHITE].playerTimeIncrement);
                zeSetPlayerInfo(PLAYER_ZEBRA, mPlayerInfo[PLAYER_ZEBRA].skill,
                        mPlayerInfo[PLAYER_ZEBRA].exactSolvingSkill, mPlayerInfo[PLAYER_ZEBRA].wldSolvingSkill,
                        mPlayerInfo[PLAYER_ZEBRA].playerTime, mPlayerInfo[PLAYER_ZEBRA].playerTimeIncrement);

                mCurrentGameState = new GameState();
                mCurrentGameState.mDisksPlayed = 0;
                mCurrentGameState.mMoveSequence = new byte[2 * BOARD_SIZE * BOARD_SIZE];

                if (mInitialGameState != null)
                    zePlay(mInitialGameState.mDisksPlayed, mInitialGameState.mMoveSequence);
                else
                    zePlay(0, null);

                mInitialGameState = null;
            }

            setEngineState(ES_READY2PLAY);
            //setEngineState(ES_PLAY);  // test
        }

        synchronized (mJNILock) {
            zeGlobalTerminate();
        }
    }

    // called by native code - see droidzebra-msg.c
    private JSONObject Callback(int msgcode, JSONObject data) {
        JSONObject retval = null;
        Message msg = mHandler.obtainMessage(msgcode);
        Bundle b = new Bundle();
        msg.setData(b);
        // Log.d("ZebraEngine", String.format("Callback(%d,%s)", msgcode, data.toString()));
        if (bInCallback)
            fatalError("Recursive vallback call");
        try {
            bInCallback = true;
            switch (msgcode) {
            case MSG_ERROR: {
                b.putString("error", data.getString("error"));
                if (getEngineState() == ES_INITIAL) {
                    // delete .bin files if initialization failed
                    // will be recreated from resources
                    new File(mFilesDir, PATTERNS_FILE).delete();
                    new File(mFilesDir, BOOK_FILE).delete();
                    new File(mFilesDir, BOOK_FILE_COMPRESSED).delete();
                }
                mHandler.sendMessage(msg);
            }
                break;

            case MSG_DEBUG: {
                b.putString("message", data.getString("message"));
                mHandler.sendMessage(msg);
            }
                break;

            case MSG_BOARD: {
                int len;
                JSONObject info;
                JSONArray zeArray;
                byte[] moves;

                JSONArray zeboard = data.getJSONArray("board");
                byte newBoard[] = new byte[BOARD_SIZE * BOARD_SIZE];
                for (int i = 0; i < zeboard.length(); i++) {
                    JSONArray row = zeboard.getJSONArray(i);
                    for (int j = 0; j < row.length(); j++) {
                        newBoard[i * BOARD_SIZE + j] = (byte) row.getInt(j);
                    }
                }
                b.putByteArray("board", newBoard);
                b.putInt("side_to_move", data.getInt("side_to_move"));
                mCurrentGameState.mDisksPlayed = data.getInt("disks_played");

                // black info
                {
                    Bundle black = new Bundle();
                    info = data.getJSONObject("black");
                    black.putString("time", info.getString("time"));
                    black.putFloat("eval", (float) info.getDouble("eval"));
                    black.putInt("disc_count", info.getInt("disc_count"));
                    black.putString("time", info.getString("time"));

                    zeArray = info.getJSONArray("moves");
                    len = zeArray.length();
                    moves = new byte[len];
                    assert (2 * len <= mCurrentGameState.mMoveSequence.length);
                    for (int i = 0; i < len; i++) {
                        moves[i] = (byte) zeArray.getInt(i);
                        mCurrentGameState.mMoveSequence[2 * i] = moves[i];
                    }
                    black.putByteArray("moves", moves);
                    b.putBundle("black", black);
                }

                // white info
                {
                    Bundle white = new Bundle();
                    info = data.getJSONObject("white");
                    white.putString("time", info.getString("time"));
                    white.putFloat("eval", (float) info.getDouble("eval"));
                    white.putInt("disc_count", info.getInt("disc_count"));
                    white.putString("time", info.getString("time"));

                    zeArray = info.getJSONArray("moves");
                    len = zeArray.length();
                    moves = new byte[len];
                    assert ((2 * len + 1) <= mCurrentGameState.mMoveSequence.length);
                    for (int i = 0; i < len; i++) {
                        moves[i] = (byte) zeArray.getInt(i);
                        mCurrentGameState.mMoveSequence[2 * i + 1] = moves[i];
                    }
                    white.putByteArray("moves", moves);
                    b.putBundle("white", white);
                }

                mHandler.sendMessage(msg);
            }
                break;

            case MSG_CANDIDATE_MOVES: {
                JSONArray jscmoves = data.getJSONArray("moves");
                CandidateMove cmoves[] = new CandidateMove[jscmoves.length()];
                mValidMoves = new int[jscmoves.length()];
                for (int i = 0; i < jscmoves.length(); i++) {
                    JSONObject jscmove = jscmoves.getJSONObject(i);
                    mValidMoves[i] = jscmoves.getJSONObject(i).getInt("move");
                    cmoves[i] = new CandidateMove(new Move(jscmove.getInt("move")));
                }
                msg.obj = cmoves;
                mHandler.sendMessage(msg);
            }
                break;

            case MSG_GET_USER_INPUT: {
                mMovesWithoutInput = 0;

                setEngineState(ES_USER_INPUT_WAIT);

                waitForEngineState(ES_PLAY);

                while (mPendingEvent == null) {
                    setEngineState(ES_USER_INPUT_WAIT);
                    waitForEngineState(ES_PLAY);
                }

                retval = mPendingEvent;

                setEngineState(ES_PLAYINPROGRESS);

                mValidMoves = null;
                mPendingEvent = null;
            }
                break;

            case MSG_PASS: {
                setEngineState(ES_USER_INPUT_WAIT);
                mHandler.sendMessage(msg);
                waitForEngineState(ES_PLAY);
                setEngineState(ES_PLAYINPROGRESS);
            }
                break;

            case MSG_OPENING_NAME: {
                b.putString("opening", data.getString("opening"));
                mHandler.sendMessage(msg);
            }
                break;

            case MSG_LAST_MOVE: {
                b.putInt("move", data.getInt("move"));
                mHandler.sendMessage(msg);
            }
                break;

            case MSG_GAME_START: {
                mHandler.sendMessage(msg);
            }
                break;

            case MSG_GAME_OVER: {
                mHandler.sendMessage(msg);
            }
                break;

            case MSG_MOVE_START: {
                mMoveStartTime = android.os.SystemClock.uptimeMillis();

                mSideToMove = data.getInt("side_to_move");

                // can change player info here
                if (mPlayerInfoChanged) {
                    zeSetPlayerInfo(PLAYER_BLACK, mPlayerInfo[PLAYER_BLACK].skill,
                            mPlayerInfo[PLAYER_BLACK].exactSolvingSkill, mPlayerInfo[PLAYER_BLACK].wldSolvingSkill,
                            mPlayerInfo[PLAYER_BLACK].playerTime, mPlayerInfo[PLAYER_BLACK].playerTimeIncrement);
                    zeSetPlayerInfo(PLAYER_WHITE, mPlayerInfo[PLAYER_WHITE].skill,
                            mPlayerInfo[PLAYER_WHITE].exactSolvingSkill, mPlayerInfo[PLAYER_WHITE].wldSolvingSkill,
                            mPlayerInfo[PLAYER_WHITE].playerTime, mPlayerInfo[PLAYER_WHITE].playerTimeIncrement);
                    zeSetPlayerInfo(PLAYER_ZEBRA, mPlayerInfo[PLAYER_ZEBRA].skill,
                            mPlayerInfo[PLAYER_ZEBRA].exactSolvingSkill, mPlayerInfo[PLAYER_ZEBRA].wldSolvingSkill,
                            mPlayerInfo[PLAYER_ZEBRA].playerTime, mPlayerInfo[PLAYER_ZEBRA].playerTimeIncrement);
                }
                mHandler.sendMessage(msg);
            }
                break;

            case MSG_MOVE_END: {
                // introduce delay between moves made by the computer without user input
                // so we can actually to see that the game is being played :)
                if (mMoveDelay > 0 || (mMovesWithoutInput > 1 && mPlayerInfo[mSideToMove].skill > 0)) {
                    long moveEnd = android.os.SystemClock.uptimeMillis();
                    int delay = mMoveDelay > 0 ? mMoveDelay : SELFPLAY_MOVE_DELAY;
                    if ((moveEnd - mMoveStartTime) < delay) {
                        android.os.SystemClock.sleep(delay - (moveEnd - mMoveStartTime));
                    }
                }

                // this counter is reset by user input
                mMovesWithoutInput += 1;

                mHandler.sendMessage(msg);
            }
                break;

            case MSG_EVAL_TEXT: {
                b.putString("eval", data.getString("eval"));
                mHandler.sendMessage(msg);
            }
                break;

            case MSG_PV: {
                JSONArray zeArray = data.getJSONArray("pv");
                int len = zeArray.length();
                byte[] moves = new byte[len];
                for (int i = 0; i < len; i++)
                    moves[i] = (byte) zeArray.getInt(i);
                b.putByteArray("pv", moves);
                mHandler.sendMessage(msg);
            }
                break;

            case MSG_CANDIDATE_EVALS: {
                JSONArray jscevals = data.getJSONArray("evals");
                CandidateMove cmoves[] = new CandidateMove[jscevals.length()];
                for (int i = 0; i < jscevals.length(); i++) {
                    JSONObject jsceval = jscevals.getJSONObject(i);
                    cmoves[i] = new CandidateMove(new Move(jsceval.getInt("move")), jsceval.getString("eval_s"),
                            jsceval.getString("eval_l"), (jsceval.getInt("best") != 0));
                }
                msg.obj = cmoves;
                mHandler.sendMessage(msg);

            }
                break;

            default: {
                b.putString("error", String.format("Unkown message ID %d", msgcode));
                msg.setData(b);
                mHandler.sendMessage(msg);
            }
                break;
            }
        } catch (JSONException e) {
            msg.what = MSG_ERROR;
            b.putString("error", "JSONException:" + e.getMessage());
            msg.setData(b);
            mHandler.sendMessage(msg);
        } finally {
            bInCallback = false;
        }
        return retval;
    }

    public boolean isThinking() {
        return getEngineState() == ZebraEngine.ES_PLAYINPROGRESS;
    }

    public boolean isValidMove(Move move) {
        if (mValidMoves == null)
            return false;

        boolean valid = false;
        for (int m : mValidMoves)
            if (m == move.mMove) {
                valid = true;
                break;
            }
        return valid;
    }

    public boolean isHumanToMove() {
        return mPlayerInfo[mSideToMove].skill == 0;
    }

    // called by native code
    //public void Error(String msg) throws EngineError
    //{
    //   throw new EngineError(msg);
    //}

    private void _prepareZebraFolder(File dir) throws IOException {
        File pattern = new File(dir, PATTERNS_FILE);
        File book = new File(dir, BOOK_FILE);
        File bookCompressed = new File(dir, BOOK_FILE_COMPRESSED);

        if (pattern.exists() && book.exists())
            return;

        if (!dir.exists() && !dir.mkdirs())
            throw new IOException(String.format("Unable to create %s", dir));

        try {
            _asset2File(coeffAssets, pattern);
            _asset2File(bookCompressedAssets, bookCompressed);
        } catch (IOException e) {
            pattern.delete();
            book.delete();
            bookCompressed.delete();
            throw e;
        }
    }

    private void _asset2File(String[] assets, File file) throws IOException {
        if (!file.exists() || file.length() == 0) {
            // copy files
            AssetManager assetManager = mContext.getAssets();
            InputStream source = null;
            OutputStream destination = null;
            try {
                destination = new FileOutputStream(file);
                for (String sourceAsset : assets) {
                    source = assetManager.open(sourceAsset);
                    byte[] buffer = new byte[1024];
                    int len = source.read(buffer);
                    while (len != -1) {
                        destination.write(buffer, 0, len);
                        len = source.read(buffer);
                    }
                }
            } finally {
                if (destination != null) {
                    try {
                        destination.close();
                    } catch (IOException e) {
                        fatalError(e.getMessage());
                    }
                }
            }
        }
    }

    private void fatalError(String message) {
        try {
            JSONObject json = new JSONObject();
            json.put("error", message);
            Callback(MSG_ERROR, json);
        } catch (JSONException e) {
            e.printStackTrace();
        }

    }

    // JNI
    private native void zeGlobalInit(String filesDir);

    private native void zeGlobalTerminate();

    private native void zeForceReturn();

    private native void zeForceExit();

    private native void zeSetPlayerInfo(int player, int skill, int exactSkill, int wldSkill, int time,
            int timeIncrement);

    private native void zePlay(int providedMoveCount, byte[] providedMoves);

    private native void zeSetAutoMakeMoves(int auto_make_moves);

    private native void zeSetSlack(float slack);

    private native void zeSetPerturbation(float perturbation);

    private native void zeSetForcedOpening(String opening_name);

    private native void zeSetHumanOpenings(int enable);

    private native void zeSetPracticeMode(int enable);

    private native void zeSetUseBook(int enable);

    private native boolean zeGameInProgress();

    public native void zeJsonTest(JSONObject json);

    // player info
    public static class PlayerInfo {
        public int playerColor;
        public int skill;
        public int exactSolvingSkill;
        public int wldSolvingSkill;
        public int playerTime;
        public int playerTimeIncrement;

        public PlayerInfo(int _player, int _skill, int _exact_skill, int _wld_skill, int _player_time,
                int _increment) {
            assert (_player == PLAYER_BLACK || _player == PLAYER_WHITE || _player == PLAYER_ZEBRA);
            playerColor = _player;
            skill = _skill;
            exactSolvingSkill = _exact_skill;
            wldSolvingSkill = _wld_skill;
            playerTime = _player_time;
            playerTimeIncrement = _increment;

        }
    }

    public static class InvalidMove extends Exception {
        private static final long serialVersionUID = 8970579827351672330L;
    }

    // Zebra move representation
    public static class Move {
        public static int PASS = -1;
        public int mMove;

        public Move(int move) {
            set(move);
        }

        public Move(int x, int y) {
            set(x, y);
        }

        public void set(int move) {
            mMove = move;
        }

        public void set(int x, int y) {
            mMove = (x + 1) * 10 + y + 1;
        }

        public int getY() {
            return mMove % 10 - 1;
        }

        public int getX() {
            return mMove / 10 - 1;
        }

        public String getText() {
            if (mMove == PASS)
                return "--";

            byte m[] = new byte[2];
            m[0] = (byte) ('a' + getX());
            m[1] = (byte) ('1' + getY());
            return new String(m);
        }
    }

    // candidate move with evals
    public class CandidateMove {
        public Move mMove;
        public boolean mHasEval;
        public String mEvalShort;
        public String mEvalLong;
        public boolean mBest;

        public CandidateMove(Move move) {
            mMove = move;
            mHasEval = false;
        }

        public CandidateMove(Move move, String evalShort, String evalLong, boolean best) {
            mMove = move;
            mEvalShort = evalShort;
            mEvalLong = evalLong;
            mBest = best;
            mHasEval = true;
        }
    }

    class GameState {
        public int mDisksPlayed;
        public byte[] mMoveSequence;
    }

}