• Please review our updated Terms and Rules here

Seeding Randomness with a String

neilobremski

Experienced Member
Joined
Oct 9, 2016
Messages
55
Location
Seattle, USA
Random numbers are great but sometimes you want them to be consistently random (an oxymoron!) so that you can rely on their pattern being the same. For example, in Magenta's Maze everything is generated from random numbers that are seeded by the spell name. This is a string which is boiled down to an integer that is used to reset the pseudo-random number generator. Thus, the level generated for a certain string is always the same even across computers and time.

In the first version, written in C, the following code was used to compute this "string hash":

Code:
void main(int argc, char *argv[])
{
	unsigned int i, j, seed = 0;
	char *spell = argv[1];

	/* process spell into random number seed */
	for (j = 0, i = 0; j < 8; i++) {
		char c = spell[i];
		unsigned int m = 0;

		if (c >= 'A' && c <= 'Z') {
			m = (unsigned int)(c - 'A' + 1);
			spell[j++] = c;
		} else if (c >= 'a' && c <= 'z') {
			m = (unsigned int)(c - 'a' + 1);
			spell[j++] = c - 'a' + 'A';
		} else if (c >= '0' && c <= '9') {
			m = (unsigned int)(c - '0' + 'Z' + 1);
			spell[j++] = c;
		} else if (!c) {
			break;
		} else {
			continue; /* ignore spaces/invalid characters */
		}

		printf("'%c' = %u\n", c, m);
		seed = seed ? seed * m : m;
	}

	printf("%s = %u\n", spell, seed);
}

Note that I wrapped this in a simple main() to demonstrate it at the command-line whereas in the game it is part of the puzzle making code. What this does is turn each character into a number that is then multiplied by the previous result (or used as is if there is no previous result). For example, "Hello" becomes 8 * 5 * 12 * 12 * 15 = 20864.

Hey, wait a minute! Yes, the overflow from the multiplication is being cut off. The previous result mathematically is 86,400 but that is too big to fit into 16-bits. In fact, it occupies 17: 10101000110000000. Truncating to 16-bits on the right yields 0101000110000000 which is indeed 20,864.

Now there is a bug in that old code that I'm keeping. Originally I was only going to support alphanumeric characters and a letter was translated into its place in the alphabet, e.g. 'A' is 1, 'B' is 2, and 'Z' is 26. Then I allowed digits and shoe-horned in that code without testing it thinking that the expression would cause '0' to be 27, '1' to be 28, etc. Unfortunately if you look closely, you'll see that '0' is 91, '1' is 92, etc. Thankfully this doesn't really cause any real problems and is mostly a matter of aesthetics.

Finally, here is the assembler code I've written to produce the same results:

Code:
a 540
; ----------------------------------------------------------------------------
; Str2Seed ()							:STR2SEED
;
; Converts 8 characters from DS:SI into seed in AX for srand().
; Also the string is normalized to uppercase w/o special characters.
;
XOR	BX, BX	; 540 reset temp seed
MOV	DI, SI	; 542 j
MOV	CX, 0008; 544 maximum 8 characters
		;
XOR	AX, AX	; 547						:STR2SEED_LOOP
LODSB		; 549
		;
OR	AL, AL	; 54A
JZ	0584	; 54C if (!c) >STR2SEED_END
		;
CMP	AL, 30	; 54E
JB	0547	; 550 if (c < 48) >STR2SEED_LOOP
		;
CMP	AL, 39	; 552						:STR2SEED_DIG
JA	055C	; 554 >STR2SEED_AUPP
MOV	[DI],AL	; 556
ADD	AL, 2B	; 558 (AL - '0' + 'Z' + 1)
JMP	057A	; 55A >STR2SEED_NEXT
		;
CMP	AL, 41	; 55C						:STR2SEED_AUPP
JB	0547	; 55E if (c < 65) >STR2SEED_LOOP
CMP	AL, 5A	; 560
JA	056A	; 562 >STR2SEED_ALOW
MOV	[DI],AL	; 564
SUB	AL, 40	; 566 (AL - 'A' + 1)
JMP	057A	; 568 >STR2SEED_NEXT
		;
CMP	AL, 61	; 56A						:STR2SEED_ALOW
JB	0547	; 56C if (c < 97) >STR2SEED_LOOP
CMP	AL, 7A	; 56E
JA	0547	; 570 >STR2SEED_LOOP
SUB	AL, 20	; 572 (AL - 'a' + 'A')
MOV	[DI],AL	; 574
SUB	AL, 40	; 576 (AL - 'A' + 1)
JMP	057A	; 578 >STR2SEED_NEXT
		;
INC	DI	; 57A						:STR2SEED_NEXT
OR	BX, BX	; 57B
JZ	0581	; 57D if (!BP) >STR2SEED_SET
MUL	BX	; 57F
XCHG	AX, BX	; 581	 					:STR2SEED_SET
LOOP	0547	; 582 >STR2SEED_LOOP
		;
XOR	AX, AX	; 584						:STR2SEED_END
MOV	[DI],AL	; 586 NULL terminator
INC	DI	; 588
MOV	[DI],AL	; 589 double NULL terminator
MOV	AX, BX	; 58B
RET		; 58D

There is a tiny difference in that this code will add two NULL terminators so that it can be used with my TextBlit() function. Here is a screenshot of me testing it. You can see that "NeilO" yields the same value in AX (hex!):

 
Back
Top