diff --git a/SPD-classes/src/main/java/com/watabou/noosa/Group.java b/SPD-classes/src/main/java/com/watabou/noosa/Group.java index 8640a605a..73846a0c6 100644 --- a/SPD-classes/src/main/java/com/watabou/noosa/Group.java +++ b/SPD-classes/src/main/java/com/watabou/noosa/Group.java @@ -22,6 +22,7 @@ package com.watabou.noosa; import com.watabou.noosa.particles.Emitter; +import com.watabou.utils.Random; import java.util.ArrayList; @@ -275,7 +276,7 @@ public class Group extends Gizmo { public synchronized Gizmo random() { if (length > 0) { - return members.get( (int)(Math.random() * length) ); + return members.get( Random.Int(length) ); } else { return null; } diff --git a/SPD-classes/src/main/java/com/watabou/utils/Bundle.java b/SPD-classes/src/main/java/com/watabou/utils/Bundle.java index 8b4fae64a..249117e81 100644 --- a/SPD-classes/src/main/java/com/watabou/utils/Bundle.java +++ b/SPD-classes/src/main/java/com/watabou/utils/Bundle.java @@ -75,6 +75,10 @@ public class Bundle { public int getInt( String key ) { return data.optInt( key ); } + + public long getLong( String key ) { + return data.optLong( key ); + } public float getFloat( String key ) { return (float)data.optDouble( key, 0.0 ); @@ -238,6 +242,14 @@ public class Bundle { } } + + public void put( String key, long value ) { + try { + data.put( key, value ); + } catch (JSONException e) { + + } + } public void put( String key, float value ) { try { diff --git a/SPD-classes/src/main/java/com/watabou/utils/Random.java b/SPD-classes/src/main/java/com/watabou/utils/Random.java index b38714067..98c80d5d5 100644 --- a/SPD-classes/src/main/java/com/watabou/utils/Random.java +++ b/SPD-classes/src/main/java/com/watabou/utils/Random.java @@ -26,34 +26,64 @@ import java.util.HashMap; public class Random { - public static float Float( float min, float max ) { - return (float)(min + Math.random() * (max - min)); + private static java.util.Random rand = new java.util.Random(); + + public static void seed( ){ + rand = new java.util.Random(); } - - public static float Float( float max ) { - return (float)(Math.random() * max); + + public static void seed( long seed ){ + rand.setSeed(seed); } - + + //returns a uniformly distributed float in the range [0, 1) public static float Float() { - return (float)Math.random(); + return rand.nextFloat(); } - + + //returns a uniformly distributed float in the range [0, max) + public static float Float( float max ) { + return Float() * max; + } + + //returns a uniformly distributed float in the range [min, max) + public static float Float( float min, float max ) { + return min + Float(max - min); + } + + //returns a uniformly distributed int in the range [0, max) public static int Int( int max ) { - return max > 0 ? (int)(Math.random() * max) : 0; + return max > 0 ? rand.nextInt(max) : 0; } - + + //returns a uniformly distributed int in the range [min, max) public static int Int( int min, int max ) { - return min + (int)(Math.random() * (max - min)); + return min + Int(max - min); } - + + //returns a uniformly distributed int in the range [min, max] public static int IntRange( int min, int max ) { - return min + (int)(Math.random() * (max - min + 1)); + return min + Int(max - min + 1); } - + + //returns a triangularly distributed int in the range [min, max] public static int NormalIntRange( int min, int max ) { - return min + (int)((Math.random() + Math.random()) * (max - min + 1) / 2f); + return min + (int)((Float() + Float()) * (max - min + 1) / 2f); } - + + //returns a uniformly distributed long in the range [-2^63, 2^63) + public static long Long() { + return rand.nextLong(); + } + + //returns a uniformly distributed long in the range [0, max) + public static long Long( long max ) { + long result = Long(); + if (result < 0) result += Long.MAX_VALUE; + return result % max; + } + + //returns an index from chances, the probability of each index is the weight values in changes public static int chances( float[] chances ) { int length = chances.length; @@ -76,6 +106,7 @@ public class Random { } @SuppressWarnings("unchecked") + //returns a key element from chances, the probability of each key is the weight value it maps to public static K chances( HashMap chances ) { int size = chances.size(); @@ -102,12 +133,12 @@ public class Random { } public static int index( Collection collection ) { - return (int)(Math.random() * collection.size()); + return Int(collection.size()); } @SafeVarargs - public static T oneOf( T... array ) { - return array[(int)(Math.random() * array.length)]; + public static T oneOf(T... array ) { + return array[Int(array.length)]; } public static T element( T[] array ) { @@ -115,7 +146,7 @@ public class Random { } public static T element( T[] array, int max ) { - return array[(int)(Math.random() * max)]; + return array[Int(max)]; } @SuppressWarnings("unchecked") diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java index 0a2f8d506..470169551 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java @@ -58,6 +58,7 @@ import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; import com.shatteredpixel.shatteredpixeldungeon.scenes.StartScene; import com.shatteredpixel.shatteredpixeldungeon.ui.QuickSlotButton; import com.shatteredpixel.shatteredpixeldungeon.utils.BArray; +import com.shatteredpixel.shatteredpixeldungeon.utils.DungeonSeed; import com.shatteredpixel.shatteredpixeldungeon.windows.WndResurrect; import com.watabou.noosa.Game; import com.watabou.utils.Bundlable; @@ -70,7 +71,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashSet; public class Dungeon { @@ -136,18 +136,30 @@ public class Dungeon { public static SparseArray> droppedItems; public static int version; + + public static long seed; public static void init() { version = Game.versionCode; challenges = ShatteredPixelDungeon.challenges(); + seed = DungeonSeed.randomSeed(); + Actor.clear(); Actor.resetNextID(); - Scroll.initLabels(); - Potion.initColors(); - Ring.initGems(); + Random.seed( seed ); + + Scroll.initLabels(); + Potion.initColors(); + Ring.initGems(); + + transmutation = Random.IntRange( 6, 14 ); + + Room.shuffleTypes(); + + Random.seed(); Statistics.reset(); Journal.reset(); @@ -162,8 +174,6 @@ public class Dungeon { for (limitedDrops a : limitedDrops.values()) a.count = 0; - - transmutation = Random.IntRange( 6, 14 ); chapters = new HashSet(); @@ -171,8 +181,6 @@ public class Dungeon { Wandmaker.Quest.reset(); Blacksmith.Quest.reset(); Imp.Quest.reset(); - - Room.shuffleTypes(); Generator.initArtifacts(); hero = new Hero(); @@ -275,6 +283,19 @@ public class Dungeon { level.reset(); switchLevel( level, level.entrance ); } + + public static long seedCurDepth(){ + return seedForDepth(depth); + } + + public static long seedForDepth(int depth){ + Random.seed( seed ); + for (int i = 0; i < depth; i ++) + Random.Long(); //we don't care about these values, just need to go through them + long result = Random.Long(); + Random.seed(); + return result; + } public static boolean shopOnLevel() { return depth == 6 || depth == 11 || depth == 16; @@ -375,6 +396,7 @@ public class Dungeon { private static final String RN_DEPTH_FILE = "ranger%d.dat"; private static final String VERSION = "version"; + private static final String SEED = "seed"; private static final String CHALLENGES = "challenges"; private static final String HERO = "hero"; private static final String GOLD = "gold"; @@ -420,6 +442,7 @@ public class Dungeon { version = Game.versionCode; bundle.put( VERSION, version ); + bundle.put( SEED, seed ); bundle.put( CHALLENGES, challenges ); bundle.put( HERO, hero ); bundle.put( GOLD, gold ); @@ -519,6 +542,8 @@ public class Dungeon { version = bundle.getInt( VERSION ); + seed = bundle.contains( SEED ) ? bundle.getLong( SEED ) : DungeonSeed.randomSeed(); + Generator.reset(); Actor.restoreNextID( bundle ); diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/Level.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/Level.java index 150dfe38a..5d3f0e279 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/Level.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/Level.java @@ -173,6 +173,8 @@ public abstract class Level implements Bundlable { public void create() { + Random.seed( Dungeon.seedCurDepth() ); + setupSize(); PathFinder.setMapSize(width(), height()); passable = new boolean[length()]; @@ -275,6 +277,8 @@ public abstract class Level implements Bundlable { createMobs(); createItems(); + + Random.seed(); } protected void setupSize(){ diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/RegularLevel.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/RegularLevel.java index 1cab100a6..8c5c144af 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/RegularLevel.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/RegularLevel.java @@ -407,7 +407,7 @@ public abstract class RegularLevel extends Level { split( new Rect( rect.left, vh, rect.right, rect.bottom ) ); } else - if ((Math.random() <= (minRoomSize * minRoomSize / rect.square()) && w <= maxRoomSize && h <= maxRoomSize) || w < minRoomSize || h < minRoomSize) { + if ((Random.Float() <= (minRoomSize * minRoomSize / rect.square()) && w <= maxRoomSize && h <= maxRoomSize) || w < minRoomSize || h < minRoomSize) { rooms.add( (Room)new Room().set( rect ) ); diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/utils/DungeonSeed.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/utils/DungeonSeed.java new file mode 100644 index 000000000..1821062b1 --- /dev/null +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/utils/DungeonSeed.java @@ -0,0 +1,98 @@ +/* + * Pixel Dungeon + * Copyright (C) 2012-2015 Oleg Dolya + * + * Shattered Pixel Dungeon + * Copyright (C) 2014-2016 Evan Debenham + * + * 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 com.shatteredpixel.shatteredpixeldungeon.utils; + +import com.watabou.utils.Random; + +//This class defines the parameters for seeds in ShatteredPD and contains a few convenience methods +public class DungeonSeed { + + private static long TOTAL_SEEDS = 5429503678976L; //26^9 possible seeds + + public static long randomSeed(){ + return Random.Long( TOTAL_SEEDS ); + } + + //Seed codes take the form @@@-@@@-@@@ where @ is any letter from A to Z (only uppercase) + //This is effectively a base-26 number system, therefore 26^9 unique seeds are possible. + + //Seed codes exist to make sharing and inputting seeds easier + //ZZZ-ZZZ-ZZZ is much easier to enter and share than 5,429,503,678,975 + + + //Takes a seed code (@@@@@@@@@) and converts it to the equivalent long value + public static long convertFromCode( String code ){ + if (code.length() != 9) + throw new IllegalArgumentException("codes must be 9 A-Z characters."); + + long result = 0; + for (int i = 8; i >= 0; i--) { + char c = code.charAt(i); + if (c > 'Z' || c < 'A') + throw new IllegalArgumentException("codes must be 9 A-Z characters."); + + result += (c - 65) * Math.pow(26, (8 - i)); + } + return result; + } + + //Takes a long value and converts it to the equivalent seed code + public static String convertToCode( long seed ){ + if (seed < 0 || seed >= TOTAL_SEEDS) + throw new IllegalArgumentException("seeds must be within the range [0, TOTAL_SEEDS)"); + + //this almost gives us the right answer, but its 0-p instead of A-Z + String interrim = Long.toString(seed, 26); + String result = ""; + + //so we convert + for (int i = 0; i < 9; i++) { + + if (i < interrim.length()){ + char c = interrim.charAt(i); + if (c <= '9') c += 17; //convert 0-9 to A-J + else c -= 22; //convert a-p to K-Z + + result += c; + + } else { + result = 'A' + result; //pad with A (zeroes) until we reach length of 9 + + } + } + + return result; + } + + //Using this we can let users input 'fun' plaintext seeds and convert them to a long equivalent. + // This is basically the same as string.hashcode except with long, and accounting for overflow + // to ensure the produced seed is always in the range [0, TOTAL_SEEDS) + public static long convertFromText( String inputText ){ + long total = 0; + for (char c : inputText.toCharArray()){ + total = 31 * total + c; + } + if (total < 0) total += Long.MAX_VALUE; + total %= TOTAL_SEEDS; + return total; + } + +}