Protocol for defining Snakes and Ladders game board setup

Dictionary of keys (strings) --> values:

  • size: <int>,
  • snakes: <sequence of integer pairs>,
  • ladders: <sequence of integer pairs>,
  • diesides: <int>,
  • name: <str>,
  • URL: <str>

Integer pairs (a,b) must satisfy a != b and no values of a or b can repeat in either sequence.


In [38]:
def validate_setup(d):
    """
    Validate keys and top-level types of values for game setup dictionary, raising a
      KeyError or TypeError if invalid, otherwise silent.
    Does not distinguish list or tuple types of snakes/ladders pairs.
    Does not check other data constraints, such as snakes or ladder pair values being
      not equal or clashing with another pair's values.
    """
    keys = sorted(d.keys())
    need_keys = sorted(['size', 'snakes', 'ladders', 'diesides', 'name', 'URL'])
    if keys != need_keys:
        raise KeyError("Correct keys not present in setup dictionary")
    if not isinstance(d['size'], int):
        raise TypeError("'size' must be int")
    if not isinstance(d['diesides'], int):
        raise TypeError("'diesides' must be int")
    for kind in ('snakes', 'ladders'):
        if isinstance(d[kind], (list, tuple)):
            try:
                for i1, i2 in d[kind]:
                    # this loop is agnostic about type of sequence
                    if not isinstance(i1+i2, int):
                        raise TypeError("'{}' must be a sequence of pairs of ints".format(kind))
            except ValueError:
                # too many or not enough values to unpack from d['snakes']
                raise TypeError("'{}' must be a sequence of pairs of ints".format(kind))
        else:
            raise TypeError("'{}' must be a sequence of pairs of ints".format(kind))

    
        
def standardize_setup(d):
    # don't standardize if valid
    validate_setup(d)
    r = d.copy() # copy
    r['snakes'] = [tuple(p) for p in d['snakes']]
    r['ladders'] = [tuple(p) for p in d['ladders']]
    return r

In [27]:
A = {'size': 16, 
    'snakes': [(11,4), (15,6)],
    'ladders': [(2,10),(8,14)],
    'diesides': 4,
    'name': "Rob's game",
    'URL': ""
        }

B = {'size': 16, 
    'snakes': [(15,6), (11,4)],
    'ladders': ((2,10),(8,14)),
    'diesides': 4,
    'name': "",
    'URL': ""
        }

# Criterion: A represents the same game as B

C = {'size': 16, 
    'snakes':[(11,5), (15,6)],
    'ladders':[(4,10),(8,14)],
    'diesides': 4,
    'name': "Rob's game",
    'URL': ""
        }

# Criterion: A does not represent the same game as C

D = {'size': 16, 
    'snakes': [[11,5], [15,6]], 
    'ladders': [[4,10], [8,14]],
    'diesides': 4,
    'name': "list-only version",
    'URL': ""
        }

# Criterion: D represents the same game as A

E_invalid = {'size': 'h', 
    'snakes': [[11,5], [15,6]], 
    'ladders': [[4,10], [8,14]],
    'diesides': 4}

In [17]:
def compare_SL_setup(d1, d2):
    # only 4 keys we care about
    test1 = d1['size'] == d2['size']
    test2 = d1['diesides'] == d2['diesides']
    test3 = sorted(d1['snakes']) == sorted(d2['snakes'])
    test4 = sorted(d1['ladders']) == sorted(d2['ladders'])
    return test1 and test2 and test3 and test4

An effective way to test data structure criteria is through assertions


In [19]:
assert compare_SL_setup(A, B)
assert not compare_SL_setup(A, C)

In [6]:
import json
with open('game_setup1.json', 'w') as fp:
    # auto-closes file at end of 'with' block
    json.dump(A, fp)

In [39]:
with open('game_setup1.json', 'r') as fp:
    d = standardize_setup(json.load(fp))

In [41]:
standardize_setup(d)


Out[41]:
{u'URL': u'',
 u'diesides': 4,
 u'ladders': [(2, 10), (8, 14)],
 u'name': u"Rob's game",
 u'size': 16,
 u'snakes': [(11, 4), (15, 6)]}