aboutsummaryrefslogtreecommitdiff
path: root/rand.s
blob: 89ee85c5da3c970a95693cdfe9aa1af777acdc26 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
; Random number generator and wrappers for Taipan.

; Originally I used POKEY's RANDOM register. It made for smaller code,
; and seemed OK...
; After some crude statistical analysis, I've decided to go with cc65's
; rand() implementation. It seems to return more evenly distributed
; results.

 .export _randl, _rand1to3
 .importzp sreg

 .include "atari.inc"

 .ifdef CART_TARGET
  .segment "HIGHCODE"
 .else
  .code
 .endif

;;; Wrappers for rand():

; unsigned long __fastcall__ randl(void);
; this returns the full range of an unsigned long, 0 to 2**32-1
_randl:
 jsr _rand
 sta sreg
 jsr _rand
 sta sreg+1
 jsr _rand
 pha
 jsr _rand
 tax
 pla
 rts

; return 1, 2, or 3. equivalent to: randi()%3+1
; replacing both occurences of the expression in taipan.c with a calls
; to this function saves 11 bytes.
_rand1to3:
	jsr _rand      ; returns 16 bits: X is MSB (which we ignore), A is LSB
	and #$03       ; A now 0..3
	beq _rand1to3  ; try again, if it's 0
	ldx #0         ; now A is 1..3, but we have to force X to 0...
	rts

;;; rand() itself.
;;; This rand() function copied from cc65-2.19's libsrc/common/rand.s
;;; and modified for my nefarious purposes.
;;; srand() is not present (we don't use it).
;
; Random number generator
;
; Written and donated by Sidney Cadot - sidney@ch.twi.tudelft.nl
; 2016-11-07, modified by Brad Smith
; 2019-10-07, modified by Lewis "LRFLEW" Fox
;
; May be distributed with the cc65 runtime using the same license.
;
;
; int rand (void);
; void srand (unsigned seed);
;
;  Uses 4-byte state.
;  Multiplier must be 1 (mod 4)
;  Added value must be 1 (mod 2)
;  This guarantees max. period (2**32)
;  The lowest bits have poor entropy and
;  exhibit easily detectable patterns, so
;  only the upper bits 16-22 and 24-31 of the
;  4-byte state are returned.
;
;  The best 8 bits, 24-31 are returned in the
;  low byte A to provide the best entropy in the
;  most commonly used part of the return value.
;
;  Uses the following LCG values for ax + c (mod m)
;  a = $01010101
;  c = $B3B3B3B3
;  m = $100000000 (32-bit truncation)
;
;  The multiplier was carefully chosen such that it can
;  be computed with 3 adc instructions, and the increment
;  was chosen to have the same value in each byte to allow
;  the addition to be performed in conjunction with the
;  multiplication, adding only 1 additional adc instruction.
;

 .export _rand, _randseed, _initrand, _addrandbits

.bss

; The seed. Not ANSI C compliant: we default to 0 rather than 1.
; Yes, this means we get a constant stream of 0 from rand()
; if we never seed it. No, we haven't forgot to seed it!
_randseed:   .res 4

.code

_rand:  clc
        lda     _randseed+0
        adc     #$B3
        sta     _randseed+0
        adc     _randseed+1
        sta     _randseed+1
        adc     _randseed+2
        sta     _randseed+2
        and     #$7f            ; Suppress sign bit (make it positive)
        tax
        lda     _randseed+2
        adc     _randseed+3
        sta     _randseed+3
        rts                     ; return bit (16-22,24-31) in (X,A)

;;; End of cc65 code.

; cc65's srand() is ANSI/ISO compliant... meaning it takes an int
; argument, which on 6502 means only 16 bits for the initial seed.
; So even though rand() generates a list of 4.3 billion nonrepeating
; results, there would only be 65535 starting points. To keep things
; less predictable, replace srand() with initrand() and addrandbits().
; initrand() sets the initial 32-bit random state from 4 successive
; reads of the Atari's POKEY pseudo-random register. The register
; keeps clocking all the time, so essentially its state is dependent
; on how long the Atari has been powered up. If we just used that for
; a random seed, the game might be too predictable (a given copy of
; the game will possibly take the same amount of time to load on the
; same drive, plus the cartridge doesn't load from disk so it's 100%
; deterministic). So addrandbits() adds entropy based on user actions:
; characters typed and the timing of the typing, to 1 jiffy precision.

; Initial plan was to generate a 32-bit seed value in the range 1 to
; 2**32-1. However I fudged it a little: none of the bytes will ever
; be initialized to 0, so it's really 1 to 255**4-1 (which is 98.5%
; of the full range: still over 4 billion possibilities).

;;; The next 2 paragraphs are speculative, don't relate to any actual code:

; Note: I came up with an elaborate scheme to sample the KBCODE
; register every scanline. Some users might actually be able to time
; their typing to 1/60 sec precision, but nobody could do that at
; scanline precision (a scanline is something like 64 microseconds).
; We could use the raw scanline number as the random value, or even
; do some calculations to get the color clock when the register
; changed (no idea whether that's useful, whether POKEY's keyboard
; hardware is that precise).
; Unfortunately it probably won't work in emulators because they
; generally process keyboard events once per frame. I didn't bother
; to code it & test it because it's overkill anyway.

; Note: I came up with a weird idea that might be useful to someone.
; When you enable players and/or missiles, but don't enable their DMA,
; they display garbage, which as I understand it is the contents of
; the data bus being read by ANTIC while the 6502 is still running.
; You could position a player or missile so one bit of it (bit 0 or
; 7) overlaps the playfield, then loop every scanline and read the
; collision register, shifting the bits into a result register. This
; would get you a lot of potentially-random bits, but since the
; 6502 is executing real code, they might not really be all that
; random. I'd like to code it up and analyze the results someday, but
; that's *way* outside the scope of this game!

;;; End of speculations, back to code.

; If you're trying to debug initrand() and/or addrandbits(), uncomment
; the #define RANDSEED_TEST at the top of taipan.c

; extern void __fastcall__ initrand(void);
; Initially the seed comes from sequential reads of POKEY's random
; register. It never returns 0 so we're guaranteed to have a usable
; seed. However, the sequential reads and the fact that initrand()
; gets called a constant amount of time after startup, means the
; initial seed won't be very random by itself. It'll get mutated
; by agetc() and addrandbits() as the user types the firm name. Even
; if he only types a 1-character name followed by Return, that's still
; going to give us a decent random seed.

_initrand:
 ldx #3
@l:
 lda RANDOM
 sta _randseed,x
 dex
 bpl @l
 rts

; extern void __fastcall__ addrandbits(char);
; Called by init_game() after each character is typed.
; The point of this is to turn the little bit of entropy in the
; user's input and timing into more entropy from POKEY. The less
; frequently you read RANDOM, the less correlated (and more random)
; the results will be.
; Caller passes us a user keystroke as ATASCII, in A. We take bits 0
; to 2 [*], wait that many scanlines, get a random number from POKEY, and
; EOR it into the _randseed byte pointed to by the low 2 bits of the
; frame counter (RTCLOK+2).
; Doing it this way, it's guaranteed that at least one byte of
; _randseed will be modified by addrandbits(). It's likely that
; more than one will be, but not guaranteed.
; Note that agetc() is still calling rand() on odd frames while all
; this is going on, so the bytes in the seed might not be the ones
; initrand() or addrandbits() put there!

_addrandbits:
 and #$07
 tax
@scanloop:    ; wait X scanlines
 sta WSYNC
 dex
 bpl @scanloop
 lda RTCLOK+2 ; the jiffy timer
 and #$03     ; destination byte (offset 0-3 from _randseed)
 tax
 lda RANDOM
@e:
 eor _randseed,x ; combine with the existing bits
 beq @e          ; if the result is 0, undo the eor.
 sta _randseed,x
 rts

; [*] I did some analysis of English text (various novels in ASCII
; e-book form) and it appears that normally bits 0 and 2 are set on
; 50% of the bytes on average (and clear the other 50% of course). Bit
; 1 is only set 33% of the time on average. This is a result of the
; frequency-of-use of the characters. Remember ETAOIN SHRDLU? Look at
; bits 0-2 of each byte... Also spaces are very common, and bits 0-2
; are of course all 0 there. So using bits 0-2 of the user's typing
; is actually biased (we get 0 to 7, but the values 0, 1, 4, 5 occur
; twice as often as 2, 3, 6, 7). This doesn't matter much, as we're
; using the result as a scanline counter to delay a read from RANDOM:
; the original bits don't end up in the seed.

;;; rest of file is commented out, left for reference.

; RANDOM is the POKEY LFSR read address. According to the POKEY data
; sheet, this is the high 8 bits bits of a 17-bit LFSR (Atari calls it
; a poly counter). Unfortunately, a read from this address never seems
; to return 0, which confuses me: an LFSR can never return 0, but since
; we're only reading 8 bits of it, we should be able to get a 0 (some
; of the other 9 bits would still be 1).

; Might use this at some point:
;_randbit:
; lda RANDOM
; asl
; lda #0
; adc #0
; rts

; unsigned char __fastcall__ randbit(void);
;_randbit:
; ldx #0
;randbit:
; lda RANDOM
; lsr
; and #$01
; rts

; This doesn't give evenly distributed results, it's twice as
; likely to return 2 or 3 than 0, 1, or 4.
; unsigned char __fastcall__ rand1in5(void);
;_rand1in5:
; ldx #0
;rand1in5:
; lda RANDOM
; lsr
; lsr
; and #$03
; adc #0
; rts


; unsigned char __fastcall__ randb(void);
;;_randb: ; C-callable entry point
;; ldx #0
;;randb:  ; asm-callable (doesn't trash X reg)
;; lda RANDOM ; bit 7 of this read ends up as bit 0 of result
;; sta tmp3
;; nop        ; let the LFSR cook for a bit...
;; nop
;; lda RTCLOK+2 ; different amount of cooking depending on whether
;; and #$01     ; we're on an even or odd numbered TV frame
;; bne @1
;; nop
;; nop
;; nop
;;@1:
;; rol tmp3   ; tmp3 bit 7 now in carry
;; lda RANDOM
;; rol        ; carry now in bit 0 of A
;; nop
;; nop
;; rts

; unsigned int __fastcall__ randi(void);
; NB cc65's rand() returns a positive signed int, meaning
; 0 to 0x7fff.
;;_randi:
;; jsr randb
;; and #$7f
;; tax
;; jsr randb
;; rts

; unsigned long __fastcall__ randl(void);
; this returns the full range of an unsigned long, 0 to 2**32-1
;;_randl:
;; jsr randb
;; sta sreg
;; jsr randb
;; sta sreg+1
;; jsr randb
;; tax
;; jsr randb
;; rts