diff --git a/DroidFish/AndroidManifest.xml b/DroidFish/AndroidManifest.xml index 4bde503..a6cae17 100644 --- a/DroidFish/AndroidManifest.xml +++ b/DroidFish/AndroidManifest.xml @@ -118,5 +118,9 @@ - + + + diff --git a/DroidFish/res/layout-land/editoptions.xml b/DroidFish/res/layout-land/editoptions.xml new file mode 100644 index 0000000..5649eb9 --- /dev/null +++ b/DroidFish/res/layout-land/editoptions.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + diff --git a/DroidFish/res/layout/editoptions.xml b/DroidFish/res/layout/editoptions.xml new file mode 100644 index 0000000..a081618 --- /dev/null +++ b/DroidFish/res/layout/editoptions.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/DroidFish/res/layout/uci_option_button.xml b/DroidFish/res/layout/uci_option_button.xml new file mode 100644 index 0000000..d09ac41 --- /dev/null +++ b/DroidFish/res/layout/uci_option_button.xml @@ -0,0 +1,5 @@ + + diff --git a/DroidFish/res/layout/uci_option_check.xml b/DroidFish/res/layout/uci_option_check.xml new file mode 100644 index 0000000..3cfe697 --- /dev/null +++ b/DroidFish/res/layout/uci_option_check.xml @@ -0,0 +1,5 @@ + + diff --git a/DroidFish/res/layout/uci_option_combo.xml b/DroidFish/res/layout/uci_option_combo.xml new file mode 100644 index 0000000..0ddc983 --- /dev/null +++ b/DroidFish/res/layout/uci_option_combo.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/DroidFish/res/layout/uci_option_spin.xml b/DroidFish/res/layout/uci_option_spin.xml new file mode 100644 index 0000000..dc739bc --- /dev/null +++ b/DroidFish/res/layout/uci_option_spin.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/DroidFish/res/layout/uci_option_string.xml b/DroidFish/res/layout/uci_option_string.xml new file mode 100644 index 0000000..df2bec2 --- /dev/null +++ b/DroidFish/res/layout/uci_option_string.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/DroidFish/res/values/strings.xml b/DroidFish/res/values/strings.xml index 3352952..39c9343 100644 --- a/DroidFish/res/values/strings.xml +++ b/DroidFish/res/values/strings.xml @@ -169,6 +169,7 @@ you are not actively using the program.\ Load Scid Game Load Position CPU Warning + UCI Options Failed to read PGN data Var: Add Analysis @@ -201,6 +202,7 @@ you are not actively using the program.\ Stockfish CuckooChess Select Engine + Set options Configure Network Engine Create Network Engine Delete Network Engine? diff --git a/DroidFish/src/org/petero/droidfish/DroidFish.java b/DroidFish/src/org/petero/droidfish/DroidFish.java index d2915d8..f555a3a 100644 --- a/DroidFish/src/org/petero/droidfish/DroidFish.java +++ b/DroidFish/src/org/petero/droidfish/DroidFish.java @@ -30,11 +30,13 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.TreeMap; import org.petero.droidfish.ChessBoard.SquareDecoration; import org.petero.droidfish.activities.CPUWarning; import org.petero.droidfish.activities.EditBoard; +import org.petero.droidfish.activities.EditOptions; import org.petero.droidfish.activities.EditPGNLoad; import org.petero.droidfish.activities.EditPGNSave; import org.petero.droidfish.activities.LoadFEN; @@ -42,6 +44,7 @@ import org.petero.droidfish.activities.LoadScid; import org.petero.droidfish.activities.Preferences; import org.petero.droidfish.book.BookOptions; import org.petero.droidfish.engine.EngineUtil; +import org.petero.droidfish.engine.UCIOptions; import org.petero.droidfish.gamelogic.DroidChessController; import org.petero.droidfish.gamelogic.ChessParseError; import org.petero.droidfish.gamelogic.Move; @@ -155,7 +158,6 @@ public class DroidFish extends Activity implements GUIInterface { // FIXME!!! Handle PGN non-file intents with more than one game. // FIXME!!! Save position to fen/epd file - // FIXME!!! Strength setting for external engines // FIXME!!! Selection dialog for going into variation // FIXME!!! Use two engines in engine/engine games @@ -1275,6 +1277,7 @@ public class DroidFish extends Activity implements GUIInterface { static private final int RESULT_OI_PGN_LOAD = 6; static private final int RESULT_OI_FEN_LOAD = 7; static private final int RESULT_GET_FEN = 8; + static private final int RESULT_EDITOPTIONS = 9; @Override public boolean onOptionsItemSelected(MenuItem item) { @@ -1326,6 +1329,7 @@ public class DroidFish extends Activity implements GUIInterface { showDialog(SELECT_BOOK_DIALOG); return true; case R.id.manage_engines: + removeDialog(MANAGE_ENGINES_DIALOG); showDialog(MANAGE_ENGINES_DIALOG); return true; case R.id.set_color_theme: @@ -1429,6 +1433,14 @@ public class DroidFish extends Activity implements GUIInterface { setFenHelper(fen); } break; + case RESULT_EDITOPTIONS: + if (resultCode == RESULT_OK) { + @SuppressWarnings("unchecked") + Map uciOpts = + (Map)data.getSerializableExtra("org.petero.droidfish.ucioptions"); + ctrl.setEngineUCIOptions(uciOpts); + } + break; } } @@ -2726,20 +2738,48 @@ public class DroidFish extends Activity implements GUIInterface { } private final Dialog manageEnginesDialog() { - final CharSequence[] items = { - getString(R.string.select_engine), - getString(R.string.configure_network_engine) - }; + final int SELECT_ENGINE = 0; + final int SET_ENGINE_OPTIONS = 1; + final int CONFIG_NET_ENGINE = 2; + List lst = new ArrayList(); + List actions = new ArrayList(); + lst.add(getString(R.string.select_engine)); actions.add(SELECT_ENGINE); + if (ctrl.computerIdle()) { + UCIOptions uciOpts = ctrl.getUCIOptions(); + if (uciOpts != null) { + boolean visible = false; + for (String name : uciOpts.getOptionNames()) + if (uciOpts.getOption(name).visible) { + visible = true; + break; + } + if (visible) { + lst.add(getString(R.string.set_engine_options)); + actions.add(SET_ENGINE_OPTIONS); + } + } + } + lst.add(getString(R.string.configure_network_engine)); actions.add(CONFIG_NET_ENGINE); + final List finalActions = actions; AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.option_manage_engines); - builder.setItems(items, new DialogInterface.OnClickListener() { + builder.setItems(lst.toArray(new CharSequence[lst.size()]), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int item) { - switch (item) { - case 0: + switch (finalActions.get(item)) { + case SELECT_ENGINE: removeDialog(SELECT_ENGINE_DIALOG); showDialog(SELECT_ENGINE_DIALOG); break; - case 1: + case SET_ENGINE_OPTIONS: { + Intent i = new Intent(DroidFish.this, EditOptions.class); + UCIOptions uciOpts = ctrl.getUCIOptions(); + if (uciOpts != null) { + i.putExtra("org.petero.droidfish.ucioptions", uciOpts); + startActivityForResult(i, RESULT_EDITOPTIONS); + } + break; + } + case CONFIG_NET_ENGINE: removeDialog(NETWORK_ENGINE_DIALOG); showDialog(NETWORK_ENGINE_DIALOG); break; diff --git a/DroidFish/src/org/petero/droidfish/activities/EditOptions.java b/DroidFish/src/org/petero/droidfish/activities/EditOptions.java new file mode 100644 index 0000000..3924d51 --- /dev/null +++ b/DroidFish/src/org/petero/droidfish/activities/EditOptions.java @@ -0,0 +1,247 @@ +/* + DroidFish - An Android chess program. + Copyright (C) 2014 Peter Ă–sterlund, peterosterlund2@gmail.com + + This program 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. + + This program 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 this program. If not, see . +*/ + +package org.petero.droidfish.activities; + +import java.util.Locale; +import java.util.TreeMap; + +import org.petero.droidfish.R; +import org.petero.droidfish.Util; +import org.petero.droidfish.engine.UCIOptions; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.ToggleButton; + +/** Edit UCI options. */ +public class EditOptions extends Activity { + private UCIOptions uciOpts = null; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); + Util.setFullScreenMode(this, settings); + + Intent i = getIntent(); + uciOpts = (UCIOptions)i.getSerializableExtra("org.petero.droidfish.ucioptions"); + if (uciOpts != null) { + initUI(); + } else { + setResult(RESULT_CANCELED); + finish(); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + initUI(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + sendBackResult(); + return true; + } + return super.onKeyDown(keyCode, event); + } + + @SuppressLint("CutPasteId") + private final void initUI() { + setContentView(R.layout.editoptions); + Util.overrideFonts(findViewById(android.R.id.content)); + + LinearLayout content = (LinearLayout)findViewById(R.id.eo_content); + Button okButton = (Button)findViewById(R.id.eo_ok); + Button cancelButton = (Button)findViewById(R.id.eo_cancel); + + for (String name : uciOpts.getOptionNames()) { + UCIOptions.OptionBase o = uciOpts.getOption(name); + if (!o.visible) + continue; + switch (o.type) { + case CHECK: { + View v = View.inflate(this, R.layout.uci_option_check, null); + CheckBox checkBox = (CheckBox)v.findViewById(R.id.eo_value); + checkBox.setText(o.name); + final UCIOptions.CheckOption co = (UCIOptions.CheckOption)o; + checkBox.setChecked(co.value); + checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + co.set(isChecked); + } + }); + content.addView(v); + break; + } + case SPIN: { + View v = View.inflate(this, R.layout.uci_option_spin, null); + TextView label = (TextView)v.findViewById(R.id.eo_label); + EditText value = (EditText)v.findViewById(R.id.eo_value); + final UCIOptions.SpinOption so = (UCIOptions.SpinOption)o; + String labelText = String.format(Locale.US, "%s (%d\u2013%d)", so.name, so.minValue, so.maxValue); + label.setText(labelText); + value.setText(so.getStringValue()); + if (so.minValue >= 0) + value.setInputType(android.text.InputType.TYPE_CLASS_NUMBER); + value.addTextChangedListener(new TextWatcher() { + public void onTextChanged(CharSequence s, int start, int before, int count) { } + public void beforeTextChanged(CharSequence s, int start, int count, int after) { } + @Override + public void afterTextChanged(Editable s) { + try { + int newVal = Integer.parseInt(s.toString()); + if (newVal < so.minValue) + so.set(so.minValue); + else if (newVal > so.maxValue) + so.set(so.maxValue); + else + so.set(newVal); + } catch (NumberFormatException ex) { + } + } + }); + content.addView(v); + break; + } + case COMBO: { + View v = View.inflate(this, R.layout.uci_option_combo, null); + TextView label = (TextView)v.findViewById(R.id.eo_label); + Spinner value = (Spinner)v.findViewById(R.id.eo_value); + label.setText(o.name); + final UCIOptions.ComboOption co = (UCIOptions.ComboOption)o; + ArrayAdapter adapter = + new ArrayAdapter(this, android.R.layout.simple_spinner_item, + co.allowedValues); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + value.setAdapter(adapter); + value.setSelection(adapter.getPosition(co.value)); + value.setOnItemSelectedListener(new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView av, View view, int position, long id) { + if ((position >= 0) && (position < co.allowedValues.length)) + co.set(co.allowedValues[position]); + } + public void onNothingSelected(AdapterView arg0) { } + }); + content.addView(v); + break; + } + case BUTTON: { + View v = View.inflate(this, R.layout.uci_option_button, null); + ToggleButton button = (ToggleButton)v.findViewById(R.id.eo_label); + final UCIOptions.ButtonOption bo = (UCIOptions.ButtonOption)o; + bo.trigger = false; + button.setText(o.name); + button.setTextOn(o.name); + button.setTextOff(o.name); + button.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + bo.trigger = isChecked; + } + }); + content.addView(v); + break; + } + case STRING: { + View v = View.inflate(this, R.layout.uci_option_string, null); + TextView label = (TextView)v.findViewById(R.id.eo_label); + EditText value = (EditText)v.findViewById(R.id.eo_value); + label.setText(o.name + " "); + final UCIOptions.StringOption so = (UCIOptions.StringOption)o; + value.setText(so.value); + value.addTextChangedListener(new TextWatcher() { + public void onTextChanged(CharSequence s, int start, int before, int count) { } + public void beforeTextChanged(CharSequence s, int start, int count, int after) { } + @Override + public void afterTextChanged(Editable s) { + so.set(s.toString()); + } + }); + content.addView(v); + break; + } + } + } + + okButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + sendBackResult(); + } + }); + cancelButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + setResult(RESULT_CANCELED); + finish(); + } + }); + } + + private final void sendBackResult() { + if (uciOpts != null) { + TreeMap uciMap = new TreeMap(); + for (String name : uciOpts.getOptionNames()) { + UCIOptions.OptionBase o = uciOpts.getOption(name); + if (o != null) { + if (o instanceof UCIOptions.ButtonOption) { + UCIOptions.ButtonOption bo = (UCIOptions.ButtonOption)o; + if (bo.trigger) + uciMap.put(name, ""); + } else { + uciMap.put(name, o.getStringValue()); + } + } + } + Intent i = new Intent(); + i.putExtra("org.petero.droidfish.ucioptions", uciMap); + setResult(RESULT_OK, i); + finish(); + } else { + setResult(RESULT_CANCELED); + finish(); + } + } +} diff --git a/DroidFish/src/org/petero/droidfish/engine/DroidComputerPlayer.java b/DroidFish/src/org/petero/droidfish/engine/DroidComputerPlayer.java index 661b99f..5849b6b 100644 --- a/DroidFish/src/org/petero/droidfish/engine/DroidComputerPlayer.java +++ b/DroidFish/src/org/petero/droidfish/engine/DroidComputerPlayer.java @@ -25,6 +25,7 @@ import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.Locale; +import java.util.Map; import java.util.regex.Pattern; import org.petero.droidfish.EngineOptions; @@ -266,6 +267,18 @@ public class DroidComputerPlayer { } } + /** Return true if computer player is in IDLE state. */ + public final synchronized boolean computerIdle() { + return engineState.state == MainState.IDLE; + } + + public final synchronized UCIOptions getUCIOptions() { + UCIEngine uci = uciEngine; + if (uci == null) + return null; + return uci.getUCIOptions(); + } + /** Return maximum number of PVs supported by engine. */ public final synchronized int getMaxPV() { return maxPV; @@ -280,6 +293,14 @@ public class DroidComputerPlayer { engineOptions = options; } + public synchronized void setEngineUCIOptions(Map uciOptions) { + if (engineState.state == MainState.IDLE) { + UCIEngine uci = uciEngine; + if (uci != null) + uci.setUCIOptions(uciOptions); + } + } + /** Return all book moves, both as a formatted string and as a list of moves. */ public final Pair> getBookHints(Position pos, boolean localized) { return book.getAllBookMoves(pos, localized); @@ -744,24 +765,11 @@ public class DroidComputerPlayer { } listener.notifyEngineName(engineName); } - } else if (tokens.length > 2) { - String optName = tokens[2].toLowerCase(Locale.US); - for (int i = 3; i < tokens.length; i++) { - if ("type".equals(tokens[i])) - break; - optName += " " + tokens[i].toLowerCase(Locale.US); - } - uci.registerOption(optName); - if (optName.equals("multipv")) { - try { - for (int i = 3; i < tokens.length; i++) { - if (tokens[i].equals("max") && (i+1 < tokens.length)) { - maxPV = Math.max(maxPV, Integer.parseInt(tokens[i+1])); - break; - } - } - } catch (NumberFormatException nfe) { } - } + } else if (tokens[0].equals("option")) { + UCIOptions.OptionBase o = uci.registerOption(tokens); + if (o instanceof UCIOptions.SpinOption && + o.name.toLowerCase(Locale.US).equals("multipv")) + maxPV = Math.max(maxPV, ((UCIOptions.SpinOption)o).maxValue); } return false; } diff --git a/DroidFish/src/org/petero/droidfish/engine/InternalStockFish.java b/DroidFish/src/org/petero/droidfish/engine/InternalStockFish.java index 4b4fb97..18090bc 100644 --- a/DroidFish/src/org/petero/droidfish/engine/InternalStockFish.java +++ b/DroidFish/src/org/petero/droidfish/engine/InternalStockFish.java @@ -28,6 +28,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Locale; import java.util.zip.GZIPInputStream; import android.content.Context; @@ -43,11 +44,14 @@ public class InternalStockFish extends ExternalEngine { @Override protected File getOptionsFile() { File extDir = Environment.getExternalStorageDirectory(); - return new File(extDir, "uci/stockfish.ini"); + return new File(extDir, "/DroidFish/uci/stockfish.ini"); } @Override protected boolean configurableOption(String name) { + name = name.toLowerCase(Locale.US); + if (!super.configurableOption(name)) + return false; if (name.equals("skill level")) return false; return true; diff --git a/DroidFish/src/org/petero/droidfish/engine/UCIEngine.java b/DroidFish/src/org/petero/droidfish/engine/UCIEngine.java index 9bc43b9..2eb2e92 100644 --- a/DroidFish/src/org/petero/droidfish/engine/UCIEngine.java +++ b/DroidFish/src/org/petero/droidfish/engine/UCIEngine.java @@ -18,6 +18,8 @@ package org.petero.droidfish.engine; +import java.util.Map; + import org.petero.droidfish.EngineOptions; public interface UCIEngine { @@ -37,6 +39,12 @@ public interface UCIEngine { /** Read UCI options from .ini file and send them to the engine. */ public void applyIniFile(); + /** Set engine UCI options. */ + public void setUCIOptions(Map uciOptions); + + /** Get engine UCI options. */ + public UCIOptions getUCIOptions(); + /** Return true if engine options have correct values. * If false is returned, engine will be restarted. */ public boolean optionsOk(EngineOptions engineOptions); @@ -66,14 +74,17 @@ public interface UCIEngine { /** Set an engine boolean option. */ public void setOption(String name, boolean value); - /** Set an engine string option. */ - public void setOption(String name, String value); + /** Set an engine option. If the option is not a string option, + * value is converted to the correct type. + * @return True if the option was changed. */ + public boolean setOption(String name, String value); /** Clear list of supported options. */ public void clearOptions(); - /** Register an option as supported by the engine. */ - public void registerOption(String optName); + /** Register an option as supported by the engine. + * @param tokens The UCI option line sent by the engine, split in words. */ + public UCIOptions.OptionBase registerOption(String[] tokens); /** Set number of search threads to use. */ public void setNThreads(int nThreads); diff --git a/DroidFish/src/org/petero/droidfish/engine/UCIEngineBase.java b/DroidFish/src/org/petero/droidfish/engine/UCIEngineBase.java index 304b7a9..5a562a1 100644 --- a/DroidFish/src/org/petero/droidfish/engine/UCIEngineBase.java +++ b/DroidFish/src/org/petero/droidfish/engine/UCIEngineBase.java @@ -20,9 +20,9 @@ package org.petero.droidfish.engine; import java.io.File; import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; -import java.util.HashMap; -import java.util.HashSet; +import java.util.ArrayList; import java.util.Locale; import java.util.Map; import java.util.Properties; @@ -35,8 +35,7 @@ import android.content.Context; public abstract class UCIEngineBase implements UCIEngine { private boolean processAlive; - private HashSet allOptions; - private HashMap currOptions; + UCIOptions options; protected boolean isUCI; public static UCIEngine getEngine(Context context, String engine, @@ -55,8 +54,7 @@ public abstract class UCIEngineBase implements UCIEngine { protected UCIEngineBase() { processAlive = false; - allOptions = new HashSet(); - currOptions = new HashMap(); + options = new UCIOptions(); isUCI = false; } @@ -92,22 +90,56 @@ public abstract class UCIEngineBase implements UCIEngine { if (ent.getKey() instanceof String && ent.getValue() instanceof String) { String key = ((String)ent.getKey()).toLowerCase(Locale.US); String value = (String)ent.getValue(); - if (key.startsWith("uci_") || key.equals("hash") || key.equals("ponder") || - key.equals("multipv") || key.equals("gaviotatbpath") || - key.equals("syzygypath") || key.equals("threads") || key.equals("cores")) - continue; - if (!configurableOption(key)) - continue; - setOption(key, value); + if (configurableOption(key)) + setOption(key, value); } } } + @Override + public final void setUCIOptions(Map uciOptions) { + boolean modified = false; + for (Map.Entry ent : uciOptions.entrySet()) { + String key = ((String)ent.getKey()).toLowerCase(Locale.US); + String value = (String)ent.getValue(); + if (configurableOption(key)) + modified |= setOption(key, value); + } + if (modified) { // Save .ini file + Properties iniOptions = new Properties(); + for (String name : options.getOptionNames()) { + UCIOptions.OptionBase o = options.getOption(name); + if (configurableOption(name) && o.modified()) + iniOptions.put(o.name, o.getStringValue()); + } + File optionsFile = getOptionsFile(); + FileOutputStream os = null; + try { + os = new FileOutputStream(optionsFile); + iniOptions.store(os, null); + } catch (IOException ex) { + } finally { + if (os != null) + try { os.close(); } catch (IOException ex) {} + } + } + } + + @Override + public final UCIOptions getUCIOptions() { + return options; + } + /** Get engine UCI options file. */ protected abstract File getOptionsFile(); /** Return true if the UCI option can be changed by the user. */ protected boolean configurableOption(String name) { + name = name.toLowerCase(Locale.US); + if (name.startsWith("uci_") || name.equals("hash") || name.equals("ponder") || + name.equals("multipv") || name.equals("gaviotatbpath") || + name.equals("syzygypath") || name.equals("threads") || name.equals("cores")) + return false; return true; } @@ -120,47 +152,143 @@ public abstract class UCIEngineBase implements UCIEngine { } @Override - public void clearOptions() { - allOptions.clear(); + public final void clearOptions() { + options.clear(); } @Override - public void registerOption(String optName) { - allOptions.add(optName); + public final UCIOptions.OptionBase registerOption(String[] tokens) { + if (tokens.length < 5 || !tokens[1].equals("name")) + return null; + String name = tokens[2]; + int i; + for (i = 3; i < tokens.length; i++) { + if ("type".equals(tokens[i])) + break; + name += " " + tokens[i]; + } + + if (i >= tokens.length - 1) + return null; + i++; + String type = tokens[i++]; + + String defVal = null; + String minVal = null; + String maxVal = null; + ArrayList var = new ArrayList(); + try { + for (; i < tokens.length; i++) { + if (tokens[i].equals("default")) { + String stop = null; + if (type.equals("spin")) + stop = "min"; + else if (type.equals("combo")) + stop = "var"; + defVal = ""; + while (i+1 < tokens.length && !tokens[i+1].equals(stop)) { + if (defVal.length() > 0) + defVal += " "; + defVal += tokens[i+1]; + i++; + } + } else if (tokens[i].equals("min")) { + minVal = tokens[++i]; + } else if (tokens[i].equals("max")) { + maxVal = tokens[++i]; + } else if (tokens[i].equals("var")) { + String value = ""; + while (i+1 < tokens.length && !tokens[i+1].equals("var")) { + if (value.length() > 0) + value += " "; + value += tokens[i+1]; + i++; + } + var.add(value); + } else + return null; + } + } catch (ArrayIndexOutOfBoundsException ex) { + return null; + } + + UCIOptions.OptionBase option = null; + if (type.equals("check")) { + if (defVal != null) { + defVal = defVal.toLowerCase(Locale.US); + option = new UCIOptions.CheckOption(name, defVal.equals("true")); + } + } else if (type.equals("spin")) { + if (defVal != null && minVal != null && maxVal != null) { + try { + int defV = Integer.parseInt(defVal); + int minV = Integer.parseInt(minVal); + int maxV = Integer.parseInt(maxVal); + if (minV <= defV && defV <= maxV) + option = new UCIOptions.SpinOption(name, minV, maxV, defV); + } catch (NumberFormatException ex) { + } + } + } else if (type.equals("combo")) { + if (defVal != null && var.size() > 0) { + String[] allowed = var.toArray(new String[var.size()]); + for (String s : allowed) + if (s.equals(defVal)) { + option = new UCIOptions.ComboOption(name, allowed, defVal); + break; + } + } + } else if (type.equals("button")) { + option = new UCIOptions.ButtonOption(name); + } else if (type.equals("string")) { + if (defVal != null) + option = new UCIOptions.StringOption(name, defVal); + } + + if (option != null) { + if (!configurableOption(name)) + option.visible = false; + options.addOption(option); + } + return option; } /** Return true if engine has option optName. */ - protected boolean hasOption(String optName) { - return allOptions.contains(optName); + protected final boolean hasOption(String optName) { + return options.contains(optName); } @Override - public void setOption(String name, int value) { + public final void setOption(String name, int value) { setOption(name, String.format(Locale.US, "%d", value)); } @Override - public void setOption(String name, boolean value) { + public final void setOption(String name, boolean value) { setOption(name, value ? "true" : "false"); } @Override - public void setOption(String name, String value) { - String lcName = name.toLowerCase(Locale.US); - if (!allOptions.contains(lcName)) - return; - String currVal = currOptions.get(lcName); - if (value.equals(currVal)) - return; - writeLineToEngine(String.format(Locale.US, "setoption name %s value %s", name, value)); - currOptions.put(lcName, value); + public final boolean setOption(String name, String value) { + if (!options.contains(name)) + return false; + UCIOptions.OptionBase o = options.getOption(name); + if (o instanceof UCIOptions.ButtonOption) { + writeLineToEngine(String.format(Locale.US, "setoption name %s", name)); + } else if (o.setFromString(value)) { + if (value.length() == 0) + value = ""; + writeLineToEngine(String.format(Locale.US, "setoption name %s value %s", name, value)); + return true; + } + return false; } @Override - public void setNThreads(int nThreads) { - if (allOptions.contains("threads")) + public final void setNThreads(int nThreads) { + if (options.contains("Threads")) setOption("Threads", nThreads); - else if (allOptions.contains("cores")) + else if (options.contains("Cores")) setOption("Cores", nThreads); } } diff --git a/DroidFish/src/org/petero/droidfish/engine/UCIOptions.java b/DroidFish/src/org/petero/droidfish/engine/UCIOptions.java new file mode 100644 index 0000000..041d235 --- /dev/null +++ b/DroidFish/src/org/petero/droidfish/engine/UCIOptions.java @@ -0,0 +1,228 @@ +package org.petero.droidfish.engine; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; + +public class UCIOptions implements Serializable { + private static final long serialVersionUID = 1L; + private ArrayList names; + private Map options; + + public static enum Type { + CHECK, + SPIN, + COMBO, + BUTTON, + STRING + } + + public abstract static class OptionBase implements Serializable { + private static final long serialVersionUID = 1L; + public String name; + public Type type; + public boolean visible = true; + + /** Return true if current value != default value. */ + abstract public boolean modified(); + + /** Return current value as a string. */ + abstract public String getStringValue(); + + /** Set option from string value. Return true if option was modified. */ + public final boolean setFromString(String value) { + OptionBase o = this; + switch (o.type) { + case CHECK: + if (value.toLowerCase(Locale.US).equals("true")) + return ((CheckOption)o).set(true); + else if (value.toLowerCase(Locale.US).equals("false")) + return ((CheckOption)o).set(false); + return false; + case SPIN: + try { + int val = Integer.parseInt(value); + SpinOption so = (SpinOption)o; + return so.set(val); + } catch (NumberFormatException ex) { + } + return false; + case COMBO: + return ((ComboOption)o).set(value); + case BUTTON: + return false; + case STRING: + return ((StringOption)o).set(value); + } + return false; + } + } + + public static final class CheckOption extends OptionBase { + private static final long serialVersionUID = 1L; + public boolean value; + public boolean defaultValue; + CheckOption(String name, boolean def) { + this.name = name; + this.type = Type.CHECK; + this.value = def; + this.defaultValue = def; + } + @Override + public boolean modified() { + return value != defaultValue; + } + @Override + public String getStringValue() { + return value ? "true" : "false"; + } + public boolean set(boolean value) { + if (this.value != value) { + this.value = value; + return true; + } + return false; + } + } + + public static final class SpinOption extends OptionBase { + private static final long serialVersionUID = 1L; + public int minValue; + public int maxValue; + public int value; + public int defaultValue; + SpinOption(String name, int minV, int maxV, int def) { + this.name = name; + this.type = Type.SPIN; + this.minValue = minV; + this.maxValue = maxV; + this.value = def; + this.defaultValue = def; + } + @Override + public boolean modified() { + return value != defaultValue; + } + @Override + public String getStringValue() { + return String.format(Locale.US, "%d", value); + } + public boolean set(int value) { + if ((value >= minValue) && (value <= maxValue)) { + if (this.value != value) { + this.value = value; + return true; + } + } + return false; + } + } + + public static final class ComboOption extends OptionBase { + private static final long serialVersionUID = 1L; + public String[] allowedValues; + public String value; + public String defaultValue; + ComboOption(String name, String[] allowed, String def) { + this.name = name; + this.type = Type.COMBO; + this.allowedValues = allowed; + this.value = def; + this.defaultValue = def; + } + @Override + public boolean modified() { + return !value.equals(defaultValue); + } + @Override + public String getStringValue() { + return value; + } + public boolean set(String value) { + for (String allowed : allowedValues) { + if (allowed.toLowerCase(Locale.US).equals(value.toLowerCase(Locale.US))) { + if (!this.value.equals(allowed)) { + this.value = allowed; + return true; + } + break; + } + } + return false; + } + } + + public static final class ButtonOption extends OptionBase { + private static final long serialVersionUID = 1L; + public boolean trigger; + ButtonOption(String name) { + this.name = name; + this.type = Type.BUTTON; + this.trigger = false; + } + @Override + public boolean modified() { + return false; + } + @Override + public String getStringValue() { + return ""; + } + } + + public static final class StringOption extends OptionBase { + private static final long serialVersionUID = 1L; + public String value; + public String defaultValue; + StringOption(String name, String def) { + this.name = name; + this.type = Type.STRING; + this.value = def; + this.defaultValue = def; + } + @Override + public boolean modified() { + return !value.equals(defaultValue); + } + @Override + public String getStringValue() { + return value; + } + public boolean set(String value) { + if (!this.value.equals(value)) { + this.value = value; + return true; + } + return false; + } + } + + UCIOptions() { + names = new ArrayList(); + options = new TreeMap(); + } + + public void clear() { + options.clear(); + } + + public boolean contains(String optName) { + return getOption(optName) != null; + } + + public final String[] getOptionNames() { + return names.toArray(new String[names.size()]); + } + + public final OptionBase getOption(String name) { + return options.get(name.toLowerCase(Locale.US)); + } + + final void addOption(OptionBase p) { + String name = p.name.toLowerCase(Locale.US); + names.add(name); + options.put(name, p); + } +} diff --git a/DroidFish/src/org/petero/droidfish/engine/cuckoochess/CuckooChessEngine.java b/DroidFish/src/org/petero/droidfish/engine/cuckoochess/CuckooChessEngine.java index dc5fd3f..fac5a94 100644 --- a/DroidFish/src/org/petero/droidfish/engine/cuckoochess/CuckooChessEngine.java +++ b/DroidFish/src/org/petero/droidfish/engine/cuckoochess/CuckooChessEngine.java @@ -78,11 +78,14 @@ public class CuckooChessEngine extends UCIEngineBase { @Override protected File getOptionsFile() { File extDir = Environment.getExternalStorageDirectory(); - return new File(extDir, "uci/cuckoochess.ini"); + return new File(extDir, "/DroidFish/uci/cuckoochess.ini"); } @Override protected boolean configurableOption(String name) { + name = name.toLowerCase(Locale.US); + if (!super.configurableOption(name)) + return false; if (name.equals("strength")) return false; return true; diff --git a/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java b/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java index 75ed6f1..ea838f0 100644 --- a/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java +++ b/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java @@ -35,6 +35,7 @@ import org.petero.droidfish.PGNOptions; import org.petero.droidfish.Util; import org.petero.droidfish.book.BookOptions; import org.petero.droidfish.engine.DroidComputerPlayer; +import org.petero.droidfish.engine.UCIOptions; import org.petero.droidfish.engine.DroidComputerPlayer.SearchRequest; import org.petero.droidfish.engine.DroidComputerPlayer.SearchType; import org.petero.droidfish.gamelogic.Game.GameState; @@ -153,6 +154,7 @@ public class DroidChessController { } } + /** Set engine options. */ public final synchronized void setEngineOptions(EngineOptions options, boolean restart) { if (!engineOptions.equals(options)) { engineOptions = options; @@ -184,6 +186,12 @@ public class DroidChessController { } } + /** Set engine UCI options. */ + public final synchronized void setEngineUCIOptions(Map uciOptions) { + if (computerPlayer != null) + computerPlayer.setEngineUCIOptions(uciOptions); + } + /** Return current engine identifier. */ public final synchronized String getEngine() { return engine; @@ -287,6 +295,17 @@ public class DroidChessController { return (computerPlayer != null) && computerPlayer.computerBusy(); } + /** Return true if computer player is in IDLE state. */ + public final synchronized boolean computerIdle() { + return (computerPlayer != null) && computerPlayer.computerIdle(); + } + + public final synchronized UCIOptions getUCIOptions() { + if (!computerIdle() || computerPlayer == null) + return null; + return computerPlayer.getUCIOptions(); + } + /** Make a move for a human player. */ public final synchronized void makeHumanMove(Move m) { if (humansTurn()) {