001 /*
002 Copyright (c) 1996-2009, Damon Hart-Davis
003 All rights reserved.
004
005 Redistribution and use in source and binary forms, with or without
006 modification, are permitted provided that the following conditions are
007 met:
008
009 * Redistributions of source code must retain the above copyright
010 notice, this list of conditions and the following disclaimer.
011
012 * Redistributions in binary form must reproduce the above copyright
013 notice, this list of conditions and the following disclaimer in the
014 documentation and/or other materials provided with the
015 distribution.
016
017 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028 */
029
030 package org.hd.d.pg2k.ai.scorer;
031
032 import org.hd.d.pg2k.svrCore.MemoryTools;
033 import org.hd.d.pg2k.svrCore.Rnd;
034
035 /**Immutable (int) Scorer parameter.
036 * We make the class Internable to control instance count and thus save some memory
037 * for at least some values.
038 */
039 public final class ScorerParamInteger implements ScorerParam,
040 MemoryTools.Internable
041 {
042 /**Get instance with the default value, no bias and a unit delta; never null.
043 * Always intern()ed since we assume that there will be few distinct 'default' values.
044 */
045 public static ScorerParamInteger createScorerParamInteger(final int min, final int def, final int max, final String name)
046 { return(MemoryTools.intern(new ScorerParamInteger(min, def, max, name))); }
047
048 /**Get instance with the default value, and specified bias and delta; never null.
049 * Always intern()ed since we assume that there will be few distinct 'default' values.
050 */
051 public static ScorerParamInteger createScorerParamInteger(final int min, final int def, final int max, final int delta, final boolean biasedLow, final String name)
052 { return(MemoryTools.intern(new ScorerParamInteger(min, def, max, delta, biasedLow, name))); }
053
054 /**Get instance with the default value, and specified bias and delta; never null.
055 * Only intern()s values close to the default since we assume that there will be relatively few distinct values;
056 * this means that we will likely capture/cache all values of small-range parameters.
057 * We use a similar notion of 'close' to that in the Integer (etc) autoboxing mechanism.
058 */
059 public static ScorerParamInteger createScorerParamInteger(final int min, final int def, final int max, final int delta, final boolean biasedLow, final String name, final int value)
060 {
061 final ScorerParamInteger result = new ScorerParamInteger(min, def, max, delta, biasedLow, name, value);
062 if(Math.abs(def - value) < 128) { return(MemoryTools.intern(result)); }
063 return(result); // Don't try to intern() this value.
064 }
065
066 /**Construct with all components, validated and stored as-is. */
067 private ScorerParamInteger(final int min, final int def, final int max,
068 final int delta, final boolean biasedLow,
069 final String name, final int value)
070 {
071 if(!AbstractScorer.isValidParameterName(name)) { throw new IllegalArgumentException(); }
072 if((min > def) || (def > max)) { throw new IllegalArgumentException(); }
073 if((value < min) || (value > max)) { throw new IllegalArgumentException(); }
074 if(delta <= 0) { throw new IllegalArgumentException(); }
075 this.name = name;
076 this.value = value;
077 this.min = min;
078 this.def = def;
079 this.max = max;
080 this.delta = delta;
081 this.biasedLow = biasedLow;
082 }
083
084 /**Construct with the default value. */
085 private ScorerParamInteger(final int min, final int def, final int max,
086 final int delta, final boolean biasedLow,
087 final String name)
088 { this(min, def, max, delta, biasedLow, name, def); }
089
090 /**Construct with the default value, no bias and a unit delta. */
091 private ScorerParamInteger(final int min, final int def, final int max,
092 final String name)
093 { this(min, def, max, 1, false, name, def); }
094
095 /**Get the parameter name; never null or empty. */
096 public String getName() { return(name); }
097
098 /**Parameter name with allowed syntax as for Java variable name; never null. */
099 public final String name;
100 /**Parameter value in inclusive/closed range [min,max]. */
101 public final int value;
102
103 /**Minimum legal value for this parameter; no larger than max. */
104 public final int min;
105 /**Default value for this parameter; no smaller than min, no larger than max. */
106 public final int def;
107 /**Maximum legal value for this parameter; no smaller than min. */
108 public final int max;
109
110 /**The size of a small change during mutation; strictly positive. */
111 public final int delta;
112 /**If true, mutations are biased towards reducing the parameter value. */
113 public final boolean biasedLow;
114
115 /**Perturb value, usually by a small amount.
116 * We occasionally randomise completely to help a Scorer escape local minima.
117 * We do this with a frequency proportional to the time it would take
118 * to traverse the entire permitted range one delta at a time.
119 *
120 * @see org.hd.d.pg2k.ai.scorer.ScorerParam#perturb()
121 */
122 public ScorerParam perturb()
123 {
124 // We only attempt to make big leaps if the entire range
125 // could be traversed in a reasonable number of deltas in the same direction...
126 // We change to a new completely random value
127 // somewhat less often than that minimum
128 // though MUCH more frequently than a true random walk would take.
129 final long range = 1 + (max - (long) min);
130 if(range <= Integer.MAX_VALUE)
131 {
132 final long minDeltas = (delta == 1) ? range : (range / delta);
133 if(minDeltas < (Integer.MAX_VALUE >>> 3))
134 {
135 if(Rnd.fastRnd.nextInt(((int)minDeltas) << 2) == 0)
136 {
137 final int newValue = min + Rnd.goodRnd.nextInt((int) range);
138 // Return existing value if not actually different to save a little heap churn.
139 if(newValue == value) { return(this); }
140 return(createScorerParamInteger(min, def, max, delta, biasedLow, name, Math.max(min, Math.min(max, newValue))));
141 }
142 }
143 }
144
145 final boolean down = (!biasedLow) ? Rnd.goodRnd.nextBoolean() :
146 (Rnd.goodRnd.nextInt(3) != 0); // Apply bias.
147 // For delta > 1, pick random value up to given delta.
148 final int deltaToUse = (delta == 1) ? delta : (Rnd.goodRnd.nextInt(delta)+1);
149 final int newValue = down ? (value-deltaToUse) : (value+deltaToUse);
150 // Return existing value if not actually different to save a little heap churn.
151 if(newValue == value) { return(this); }
152 return(createScorerParamInteger(min, def, max, delta, biasedLow, name, Math.max(min, Math.min(max, newValue))));
153 }
154
155
156 /**Parse the String representation of a parameter returning a value of the same type; never null.
157 * If the argument is null or unparsable or out of range,
158 * then the result has the default value,
159 * else an instance with the requested parameter value is returned.
160 * @return legal value of the same type as this, with the parsed input String's value if possible
161 */
162 public ScorerParam parse(final String v)
163 {
164 int parsedValue = def; // Use default value unless the parse is successful.
165 if(v != null)
166 {
167 try
168 {
169 final int p = Integer.parseInt(v);
170 if((p >= min) && (p <= max)) { parsedValue = p; }
171 }
172 catch(final NumberFormatException e) { /* Ignore. */ }
173 }
174
175 // Memory optimisation where value is the same as for the current instance.
176 if(parsedValue == value) { return(this); }
177
178 // Return instance containing the parsed value.
179 return(createScorerParamInteger(min, def, max, delta, biasedLow, name, parsedValue));
180 }
181
182 /**Use the supplied parameter if possible (correct name/type/bounds), else return the default; never null.
183 * We return this to avoid creating a new instance whenever possible.
184 */
185 public ScorerParam extract(final ScorerParam p)
186 {
187 if((p == this) || equals(p)) { return(this); }
188 if(p.getClass() != getClass()) { return(createScorerParamInteger(min, def, max, delta, biasedLow, name)); }
189 final ScorerParamInteger pi = (ScorerParamInteger) p;
190 // Return default if this is not identical in every particular except the value.
191 if((pi.def != def) || !name.equals(pi.name) ||
192 (pi.min != min) || (pi.max != max) ||
193 (pi.delta != delta) || (pi.biasedLow != biasedLow))
194 { return(createScorerParamInteger(min, def, max, delta, biasedLow, name)); }
195 return(pi);
196 }
197
198
199 /**Generate the "name=value" text; never null or empty, though the value part may be empty. */
200 public String toNameValueString()
201 {
202 final StringBuilder sb = new StringBuilder(12 + name.length());
203 sb.append(name).append('=').append(value);
204 return(sb.toString());
205 }
206
207 /**Human-readable representation. */
208 @Override
209 public String toString() { return(toNameValueString()); }
210
211
212 /* (non-Javadoc)
213 * @see org.hd.d.pg2k.ai.scorer.ScorerParam#similar(org.hd.d.pg2k.ai.scorer.ScorerParam)
214 */
215 public boolean similar(final ScorerParam p)
216 {
217 if(p == this) { return(true); }
218 if(!(p instanceof ScorerParamInteger)) { return(false); }
219 // Treat as different if different name or bounds.
220 final ScorerParamInteger pi = (ScorerParamInteger) p;
221 if((max != pi.max) || (min != pi.min) || (def != pi.def) || (delta != pi.delta) ||
222 !name.equals(pi.name))
223 { return(false); }
224 // Similar if the values are within one delta either way.
225 return(Math.abs(pi.value - value) <= delta);
226 }
227
228
229 /**The hash depends on the name and the value as the most variable components. */
230 @Override public int hashCode() { return(name.hashCode() ^ value); }
231
232 /**Equality depends on all fields. */
233 @Override public boolean equals(final Object obj)
234 {
235 if(this == obj) {
236 return true;
237 }
238 if(obj == null) {
239 return false;
240 }
241 if(getClass() != obj.getClass()) {
242 return false;
243 }
244 final ScorerParamInteger other = (ScorerParamInteger) obj;
245 if(value != other.value) {
246 return false;
247 }
248 if(biasedLow != other.biasedLow) {
249 return false;
250 }
251 if(def != other.def) {
252 return false;
253 }
254 if(delta != other.delta) {
255 return false;
256 }
257 if(max != other.max) {
258 return false;
259 }
260 if(min != other.min) {
261 return false;
262 }
263 if(!name.equals(other.name)) {
264 return false;
265 }
266 return true;
267 }
268 }