-
Notifications
You must be signed in to change notification settings - Fork 4
/
sanitization.go
841 lines (651 loc) · 27.3 KB
/
sanitization.go
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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
package boardgame
import (
"hash/fnv"
"math/rand"
"sort"
"strconv"
"strings"
"github.com/jkomoros/boardgame/enum"
"github.com/jkomoros/boardgame/errors"
)
//sanitizationTransformation contains which policy to apply for every property
//in the state. Missing properties will be treated as PolicyVisible.
type sanitizationTransformation struct {
Game subStateSanitizationTransformation
Players []subStateSanitizationTransformation
DynamicComponentValues map[string]subStateSanitizationTransformation
}
//Map of policy to apply for each propname in this sub-state
type subStateSanitizationTransformation map[string]Policy
//SanitizationDefaultGroup is the implied sanitization group name if no group
//name is included. See StructInflater.PropertySanitizationPolicy for more on
//sanitization policy groups.
const SanitizationDefaultGroup = "all"
//SanitizationDefaultPlayerGroup is the implied sanitization group name if no group
//name is included for player states. See
//StructInflater.PropertySanitizationPolicy for more on sanitization policy
//groups.
const SanitizationDefaultPlayerGroup = "other"
//the only part of machinery that treats these specially is
//gameManager.computedPlayerGroupMembership.
const sanitizationGroupSelf = "self"
const sanitizationGroupOther = SanitizationDefaultPlayerGroup
/*
Policy is the type that reprsents a sanitization policy.
A sanitization policy reflects how to tranform a given State property when
presenting to someone outside of the target group. They are returned from your
GameDelegate's SanitizationPolicy method, and the results are used to configure
how properties are modified or elided in state.SanitizedForPlayer.
For most types of properties, there are two effective policies: PolicyVisible,
in which the property is left untouched, and PolicyHidden, in which case the
value is sanitized to its zero value.
However Stacks are much more complicated and have more policies. Even when the
specific components in a stack aren't visible, it can still be important to know
that a given ComponentInstance in one state is the same as another
ComponentInstance in another state, which allows for example animations of the
same logical card from one stack to another, even though the value of the card
is not visible.
In order to do this, every component has a semi-stable Id. This Id is calculated
based on a hash of the component, deck, deckIndex, gameId, and also a secret
salt for the game. This way, the same component in different games will have
different Ids, and if you have never observed the value of the component in a
given game, it is impossible to guess it. However, it is possible to keep track
of the component as it moves between different stacks within a game.
Every stack has an ordered list of Ids representing the Id for each component.
Components can also be queried for their Id.
Stacks also have an unordered set of IdsLastSeen, which tracks the last time the
Id was affirmitively seen in a stack. The basic time this happens is when a
component is first inserted into a stack. (See below for additional times when
this hapepns)
Different Sanitization Policies will do different things to Ids and IdsLastSeen,
according to the following table:
| Policy | Values Behavior | Ids() | IdsLastSeen() | ShuffleCount() |Notes |
|----------------|------------------------------------------------------------------|-----------------------------|---------------|----------------|-------------------------------------------------------------------------------------------------------|
| PolicyVisible | All values visible | Present | Present | Present | Visible is effectively no transformation |
| PolicyOrder | All values replaced by generic component | Present | Present | Present | PolicyOrder is similar to PolicyLen, but the order of components is observable |
| PolicyLen | All values replaced by generic component | Sorted Lexicographically | Present | Present | PolicyLen makes it so it's only possible to see the length of a stack, not its order. |
| PolicyNonEmpty | Values will be either 0 components or a single generic component | Absent | Present | Absent | PolicyNonEmpty makes it so it's only possible to tell if a stack had 0 items in it or more than zero. |
| PolicyHidden | Values are completely empty | Absent | Absent | Absent | PolicyHidden is the most restrictive; stacks look entirely empty. |
However, in some cases it is not possible to keep track of the precise order of
components, even with perfect observation. The canonical example is when a stack
is shuffled. Another example would be when a card is inserted at an unknown
location in a deck.
For this reason, a component's Id is only semi-stable. When one of these secret
moves has occurred, the Ids is randomized. However, in order to be able to keep
track of where the component is, the component is "seen" in IdsLastSeen
immediately before having its Id scrambled, and immediately after. This
procedure is referred to as "scrambling" the Ids.
stack.Shuffle() automatically scrambles the ids of all items in the stack.
SecretMoveComponent, which is similar to the normal MoveComponent, moves the
component to the target stack and then scrambles the Ids of ALL components in
that stack as described above. This is because if only the new item's id
changed, it would be trivial to observe that the new Id is equivalent to the old
Id.
Note that DynamicComponentValues behave slightly differently than values in
other SubStates; all properties in them are effectively PolicyHidden unless the
component they are attached to is PolicyVisible (either directly, or
transatively)--in which case their configured policy is used.
*/
type Policy int
const (
//PolicyVisible means non sanitized. For non-group properties (e.g. strings, ints, bools), any
//policy other than PolicyVisible is effectively PolicyHidden.
PolicyVisible Policy = iota
//PolicyOrder means that for groups (e.g. stacks, int slices), return a
//group that has the same length, and whose Ids() represents the identity of
//the items. In practice, stacks will be set so that their NumComponents()
//is the same, but every component that exists returns the GenericComponent.
//This policy is similar to Len, but allows observers to keep track of the
//identity of cards as they are reordered in the stack.
PolicyOrder
//PolicyLen means that for groups (e.g. stacks, int slices), return a group
//that has the same length. For all else, it's effectively PolicyHidden. In
//practice, stacks will be set so that their NumComponents() is the same,
//but every component that exists returns the GenericComponent.
PolicyLen
//PolicyNonEmpty means that for groups, PolicyNonEmpty will allow it to be
//observed that the stack's NumComponents is either Empty (0 components) or
//non-empty (1 components). So for default Stacks, it will either have no
//components or 1 component. And for SizedStack, either all of the slots
//will be empty, or the first slot will be non-empty. In all cases, the
//Component present, if there is one, will be the deck's GenericComponent.
PolicyNonEmpty
//PolicyHidden returns effectively the zero value for the type. For
//stacks, the deck it is, and the Size (for SizedStack) is set, but
//nothing else is.
PolicyHidden
//PolicyInvalid is not a valid Policy. It can be provided to signal an
//illegal policy, which will cause the sanitization policy pipeline to
//error.
PolicyInvalid
//TODO: implement the other policies.
)
func policyFromString(policyName string) Policy {
policyName = strings.ToLower(policyName)
policyName = strings.TrimSpace(policyName)
switch policyName {
case "visible":
return PolicyVisible
case "order":
return PolicyOrder
case "len":
return PolicyLen
case "nonempty":
return PolicyNonEmpty
case "hidden":
return PolicyHidden
}
return PolicyInvalid
}
func (s *state) SanitizedForPlayer(player PlayerIndex) (ImmutableState, error) {
//If the playerIndex isn't an actuall player's index, just return self.
if player < -1 || int(player) >= len(s.playerStates) {
return s, nil
}
transformation, err := s.generateSanitizationTransformation(player)
if err != nil {
return nil, err
}
sanitized, err := s.applySanitizationTransformation(transformation)
if err != nil {
return nil, err
}
return sanitized, nil
}
func groupMembershipForPlayerState(playerState ImmutableSubState) (map[int]bool, map[string]bool) {
groupMembership := make(map[int]bool)
stringGroupMembership := make(map[string]bool)
if playerState != nil {
delegate := playerState.ImmutableState().Manager().Delegate()
groupMembership = delegate.GroupMembership(playerState)
if groupMembership == nil {
groupMembership = make(map[int]bool)
}
groupEnum := delegate.GroupEnum()
if groupEnum != nil {
for k, v := range groupMembership {
stringGroupMembership[groupEnum.String(k)] = v
}
}
}
stringGroupMembership[SanitizationDefaultGroup] = true
return groupMembership, stringGroupMembership
}
//generateSanitizationTransformation creates a sanitizationTransformation by
//consulting the delegate for each property on each sub-state.
func (s *state) generateSanitizationTransformation(player PlayerIndex) (*sanitizationTransformation, error) {
result := &sanitizationTransformation{}
var viewingAsPlayerState ImmutableSubState
if player >= 0 {
viewingAsPlayerState = s.playerStates[player]
}
viewingAsPlayerGroupMembership, viewingAsPlayerStringGroupMembership := groupMembershipForPlayerState(viewingAsPlayerState)
ref := StatePropertyRef{
Group: StateGroupGame,
}
//we pass the viewingAsPlayer's groupMembership to all transformations,
//including game, because things like 'guesser:hidden' should apply even if
//the group membership comes from the player's state.
result.Game = generateSubStateSanitizationTransformation(s.GameState(), ref, viewingAsPlayerStringGroupMembership)
result.Players = make([]subStateSanitizationTransformation, len(s.PlayerStates()))
for i, playerState := range s.PlayerStates() {
ref := StatePropertyRef{
Group: StateGroupPlayer,
}
playerStateGroupMembership, playerStateStringGroupMembership := groupMembershipForPlayerState(playerState)
//extend the groupMembership map to include any computed ones--any ones who are not included in groupEnum.
for groupName := range s.Manager().playerValidator.sanitizationPolicyGroupNames(s.Manager().Delegate().GroupEnum()) {
inGroup, err := s.Manager().computedPlayerGroupMembership(groupName, PlayerIndex(i), player, playerStateGroupMembership, viewingAsPlayerGroupMembership)
if err != nil {
return nil, errors.New("Couldn't get group membership for groupName: " + groupName + ": " + err.Error())
}
playerStateStringGroupMembership[groupName] = inGroup
}
result.Players[i] = generateSubStateSanitizationTransformation(playerState, ref, playerStateStringGroupMembership)
}
result.DynamicComponentValues = make(map[string]subStateSanitizationTransformation)
for deckName, deckValues := range s.DynamicComponentValues() {
if len(deckValues) == 0 {
return nil, errors.New("No deck values")
}
ref := StatePropertyRef{
Group: StateGroupDynamicComponentValues,
DeckName: deckName,
}
result.DynamicComponentValues[deckName] = generateSubStateSanitizationTransformation(deckValues[0], ref, viewingAsPlayerStringGroupMembership)
}
return result, nil
}
func generateSubStateSanitizationTransformation(subState ImmutableSubState, propertyRef StatePropertyRef, fullGroupMembership map[string]bool) subStateSanitizationTransformation {
result := make(subStateSanitizationTransformation)
delegate := subState.ImmutableState().Manager().Delegate()
for propName := range subState.Reader().Props() {
//Since propertyRef is passed in by value we can modify it locally without a problem
propertyRef.PropName = propName
result[propName] = delegate.SanitizationPolicy(propertyRef, fullGroupMembership)
}
return result
}
//applySanitizationTransformation takes a generated sanitizationTransformation
//and applies it to the given tate, returning a new state that has been
//transformed accordingly. The DynamicComponentValues transformations are set
//to Hidden (instead of how they are configured) unless the stacks that
//contain them in Game and Player states resolve to PolicyVisible.
func (s *state) applySanitizationTransformation(transformation *sanitizationTransformation) (State, error) {
sanitized, err := s.copy(true)
if err != nil {
return nil, errors.New("Couldn't copy state: " + err.Error())
}
if len(transformation.Players) != len(s.PlayerStates()) {
return nil, errors.New("the transformation did not have a record for each player state")
}
//We need to figure out which components that have dynamicvalues are
//visible after sanitizing game and player states. We'll have
//sanitizeStateObj tell us which ones are visible, and which player's
//state they're visible through, by accumulating the information in
//visibleDyanmicComponents.
visibleDynamicComponents := make(map[string]map[int]bool)
for deckName := range s.dynamicComponentValues {
visibleDynamicComponents[deckName] = make(map[int]bool)
}
err = sanitizeStateObj(sanitized.gameState.ReadSetConfigurer(), transformation.Game, visibleDynamicComponents)
if err != nil {
return nil, errors.Extend(err, "Couldn't sanitize game state")
}
playerStates := sanitized.playerStates
for i := 0; i < len(playerStates); i++ {
err = sanitizeStateObj(playerStates[i].ReadSetConfigurer(), transformation.Players[i], visibleDynamicComponents)
if err != nil {
return nil, errors.Extend(err, "Couldn't sanitize player state number "+strconv.Itoa(i))
}
}
//Some of the DynamicComponentValues that were marked as visible might
//have their own stacks with dynamic values that are visible, so we need
//to go through and mark those, too..
transativelyMarkDynamicComponentsAsVisible(sanitized.dynamicComponentValues, visibleDynamicComponents)
//Now that all dynamic components are marked, we need to go through and
//sanitize all of those objects according to the policy.
if err := sanitizeDynamicComponentValues(sanitized.dynamicComponentValues, visibleDynamicComponents, transformation.DynamicComponentValues); err != nil {
return nil, errors.Extend(err, "Couldn't sanitize dyanmic component values")
}
return sanitized, nil
}
//sanitizeStateObj applies the given sanitizationTransformation to the given
//sub-state. It also keeps track of which components within it resolve to
//PolicyVisible, so later that information can be used to only reveal that
//information in DynamicComponentValues if the components they're related to
//were visible.
func sanitizeStateObj(readSetConfigurer PropertyReadSetConfigurer, transformation subStateSanitizationTransformation, visibleDynamic map[string]map[int]bool) error {
for propName, propType := range readSetConfigurer.Props() {
prop, err := readSetConfigurer.Prop(propName)
if err != nil {
return errors.Extend(err, propName+" had an error")
}
policy := transformation[propName]
if policy == PolicyInvalid {
return errors.New("Effective policy computed to PolicyInvalid")
}
readSetConfigurer.ConfigureProp(propName, applyPolicy(policy, prop, propType))
if visibleDynamic != nil {
if policy != PolicyVisible {
continue
}
var stacks []ImmutableStack
if propType == TypeStack {
stacks = []ImmutableStack{prop.(ImmutableStack)}
} else if propType == TypeBoard {
stacks = prop.(Board).ImmutableSpaces()
}
for _, stack := range stacks {
if _, ok := visibleDynamic[stack.Deck().Name()]; ok {
for _, c := range stack.ImmutableComponents() {
if c == nil {
continue
}
visibleDynamic[c.Deck().Name()][c.DeckIndex()] = true
}
}
}
}
}
return nil
}
//transitivelyMarkDynamicComponentsAsVisible expands which
//dynamiccomponentvalues are visible by extending the visibility throughout
//any items that are in stacks on dynamiccomponentvalues that are visible.
func transativelyMarkDynamicComponentsAsVisible(dynamicComponentValues map[string][]ConfigurableSubState, visibleComponents map[string]map[int]bool) {
//All dynamic component values are hidden, except for ones that currently
//reside in stacks that have resolved to being Visible based on this
//current sanitization configuration. However, DynamicComponents may
//themselves have stacks that reference other dynamic components. This
//method effectively "spreads out" the visibility from visible dynamic
//compoonents to other ones they point to.
//TODO: TEST THIS!
type workItem struct {
deckName string
deckIndex int
}
var workItems []workItem
//Fill the list of items to work through with all visible items.
for deckName, visibleItems := range visibleComponents {
for index := range visibleItems {
workItems = append(workItems, workItem{deckName, index})
}
}
//We can't use range because we will be adding more items to it as we go.
for i := 0; i < len(workItems); i++ {
item := workItems[i]
values := dynamicComponentValues[item.deckName][item.deckIndex]
reader := values.Reader()
for _, stack := range stacksForReader(reader) {
if _, ok := dynamicComponentValues[stack.Deck().Name()]; !ok {
//This stack is for a deck that has no dynamic values, can skip.
continue
}
//Ok, if we get to here then we have a stack with items in a deck that does have dynamic values.
for _, c := range stack.ImmutableComponents() {
if c == nil {
continue
}
//There can't possibly be a collision because each component may only be in a single stack at a time.
visibleComponents[c.Deck().Name()][c.DeckIndex()] = true
//Take note that there's another item to add to the queue to explore.
workItems = append(workItems, workItem{c.Deck().Name(), c.DeckIndex()})
}
}
}
}
//sanitizeDynamicComponentValues is more complex than just applying a
//straightforward sanitizationTransformation because the components should
//only folow the configured property if the component they're affiliated with
//was PolicyVisible.
func sanitizeDynamicComponentValues(dynamicComponentValues map[string][]ConfigurableSubState, visibleComponents map[string]map[int]bool, transformation map[string]subStateSanitizationTransformation) error {
for name, slice := range dynamicComponentValues {
visibleDynamicDeck := visibleComponents[name]
for i, value := range slice {
readSetConfigurer := value.ReadSetConfigurer()
if _, visible := visibleDynamicDeck[i]; visible {
if err := sanitizeStateObj(readSetConfigurer, transformation[name], nil); err != nil {
return errors.Extend(err, "Couldn't sanitize random dynamic component")
}
} else {
//Make it a hidden
for propName, propType := range readSetConfigurer.Props() {
prop, err := readSetConfigurer.Prop(propName)
if err != nil {
continue
}
readSetConfigurer.ConfigureProp(propName, applyPolicy(PolicyHidden, prop, propType))
}
}
}
}
return nil
}
func applyPolicy(policy Policy, input interface{}, propType PropertyType) interface{} {
if policy == PolicyVisible {
return input
}
//Go through the propTypes where everythign that's not PolicyVisible is
//effectively PolicyHidden.
switch propType {
case TypeBool:
return false
case TypeInt:
return 0
case TypeString:
return ""
case TypePlayerIndex:
return 0
case TypeTimer:
return NewTimer()
case TypeEnum:
e := input.(enum.Val).ImmutableCopy()
res, _ := e.Enum().NewImmutableVal(e.Enum().DefaultValue())
return res
}
//Now the ones that are non-stack containers
switch propType {
case TypeIntSlice:
return applySanitizationPolicyIntSlice(policy, input.([]int))
case TypeBoolSlice:
return applySanitizationPolicyBoolSlice(policy, input.([]bool))
case TypeStringSlice:
return applySanitizationPolicyStringSlice(policy, input.([]string))
case TypePlayerIndexSlice:
return applySanitizationPolicyPlayerIndexSlice(policy, input.([]PlayerIndex))
}
if propType == TypeBoard {
board := input.(Board)
board.applySanitizationPolicy(policy)
return input
}
//Now we're left with len-properties.
stack := input.(Stack)
stack.applySanitizationPolicy(policy)
return input
}
func applySanitizationPolicyIntSlice(policy Policy, input []int) []int {
if policy == PolicyVisible {
return input
}
if policy == PolicyLen || policy == PolicyOrder {
return make([]int, len(input))
}
if policy == PolicyNonEmpty {
if len(input) > 0 {
return make([]int, 1)
}
return make([]int, 0)
}
//if we get to here it's either PolicyHidden, or an unknown policy. If the
//latter, it's better to fail by being restrictive.
return make([]int, 0)
}
func applySanitizationPolicyBoolSlice(policy Policy, input []bool) []bool {
if policy == PolicyVisible {
return input
}
if policy == PolicyLen || policy == PolicyOrder {
return make([]bool, len(input))
}
if policy == PolicyNonEmpty {
if len(input) > 0 {
return make([]bool, 1)
}
return make([]bool, 0)
}
//if we get to here it's either PolicyHidden, or an unknown policy. If the
//latter, it's better to fail by being restrictive.
return make([]bool, 0)
}
func applySanitizationPolicyStringSlice(policy Policy, input []string) []string {
if policy == PolicyVisible {
return input
}
if policy == PolicyLen || policy == PolicyOrder {
return make([]string, len(input))
}
if policy == PolicyNonEmpty {
if len(input) > 0 {
return make([]string, 1)
}
return make([]string, 0)
}
//if we get to here it's either PolicyHidden, or an unknown policy. If the
//latter, it's better to fail by being restrictive.
return make([]string, 0)
}
func applySanitizationPolicyPlayerIndexSlice(policy Policy, input []PlayerIndex) []PlayerIndex {
if policy == PolicyVisible {
return input
}
if policy == PolicyLen || policy == PolicyOrder {
return make([]PlayerIndex, len(input))
}
if policy == PolicyNonEmpty {
if len(input) > 0 {
return make([]PlayerIndex, 1)
}
return make([]PlayerIndex, 0)
}
//if we get to here it's either PolicyHidden, or an unknown policy. If the
//latter, it's better to fail by being restrictive.
return make([]PlayerIndex, 0)
}
func (b *board) applySanitizationPolicy(policy Policy) {
for _, stack := range b.spaces {
stack.applySanitizationPolicy(policy)
}
}
func (g *growableStack) applySanitizationPolicy(policy Policy) {
if policy == PolicyVisible {
return
}
if policy == PolicyLen || policy == PolicyOrder {
//Keep Ids before we blank-out components, but put them in a random
//order.
g.overrideIds = make([]string, len(g.indexes))
for i, c := range g.Components() {
if c == nil {
continue
}
g.overrideIds[i] = c.ID()
}
if policy == PolicyLen {
g.overrideIds = overrideIDsForLen(g)
}
indexes := make([]int, len(g.indexes))
for i := 0; i < len(indexes); i++ {
indexes[i] = genericComponentSentinel
}
g.indexes = indexes
return
}
g.shuffleCount = 0
//Anything other than PolicyVisible and PolicyLen (at least currently)
//will move Ids to PossibleIds.
for _, c := range g.Components() {
if c == nil {
continue
}
id := c.ID()
g.idSeen(id)
}
if policy == PolicyNonEmpty {
if g.NumComponents() == 0 {
g.indexes = make([]int, 0)
} else {
g.indexes = []int{genericComponentSentinel}
}
return
}
//if we get to here it's either PolicyHidden, or an unknown policy. If the
//latter, it's better to fail by being restrictive.
g.indexes = make([]int, 0)
g.idsLastSeen = make(map[string]int)
return
}
func overrideIDsForLen(stack Stack) []string {
ids := stack.IDs()
sort.Strings(ids)
return ids
}
//returns a random permutation of size stack.Len(). The permutation will be
//predictable given this exact stack and its state, but unpredictable in
//general. This makes it give predictable results for testing but still be
//unguessable if you don't have the stack's game's SecretSalt. This method
//exists even though state has a soruce of randomness because this library
//should only use state.Rand() as a source if it's deterministically called,
//whereas any given state might have multiple sanitizated states created
//implicitly.
func randPermForStack(stack Stack) []int {
//We want this to be deterministic for two reasons: to have stable goldens
//to compare against in testing. But also so that the same stack,
//sanitized with order, will have a somewhat-stable list of IDs. If it
//changes every time then they might animate on the client, which is what
//caused #711.
//We want something unguessable, different per stack, that's semi-stable,
//but that doesn't change when items in the stack are reordered. Ideally
//we'd use a stable stack.Id(), but those don't exist. Another option
//would be to do it based on the secret salt of the game, as well as the
//StatePropertyRef of this stack in state, which is stable. But we don't
//actually know that cheaply.
//As a compromise, iterate through the stack to find the lowest-ID'd
//component, and use that. That will change if that particular component
//happens to leave the stack, but that fact is already observable via
//lastSeenIds, so that's OK>
lowestComponentID := ""
for _, c := range stack.Components() {
if c == nil {
continue
}
if lowestComponentID == "" {
lowestComponentID = c.ID()
continue
}
if c.ID() < lowestComponentID {
lowestComponentID = c.ID()
}
}
seedStr := stack.state().game.secretSalt + lowestComponentID
h := fnv.New64()
h.Write([]byte(seedStr))
seed := h.Sum64()
r := rand.New(rand.NewSource(int64(seed)))
return r.Perm(stack.Len())
}
func (s *sizedStack) applySanitizationPolicy(policy Policy) {
if policy == PolicyVisible {
return
}
if policy == PolicyLen || policy == PolicyOrder {
//Keep Ids before we blank-out components, but put them in a random
//order.
s.overrideIds = make([]string, len(s.indexes))
for i, c := range s.Components() {
if c == nil {
continue
}
s.overrideIds[i] = c.ID()
}
if policy == PolicyLen {
s.overrideIds = overrideIDsForLen(s)
}
indexes := make([]int, len(s.indexes))
for i := 0; i < len(indexes); i++ {
if s.indexes[i] == emptyIndexSentinel {
indexes[i] = emptyIndexSentinel
} else {
indexes[i] = genericComponentSentinel
}
}
s.indexes = indexes
return
}
s.shuffleCount = 0
//Anything other than PolicyVisible and PolicyLen (at least currently)
//will move Ids to PossibleIds.
for _, c := range s.Components() {
if c == nil {
continue
}
id := c.ID()
s.idSeen(id)
}
//if we get to here it's either PolicyHidden, PolicyNonEmpty or an unknown
//policy. If the latter, it's better to fail by being restrictive.
hasComponents := s.NumComponents() > 0
indexes := make([]int, len(s.indexes))
for i := 0; i < len(indexes); i++ {
indexes[i] = -1
}
s.indexes = indexes
if policy == PolicyNonEmpty && hasComponents {
s.indexes[0] = genericComponentSentinel
}
if policy == PolicyHidden {
s.idsLastSeen = make(map[string]int)
}
return
}