Example #1
0
class Builder(object):
    '''
    The Builder class implements the main building loop that consists of:
        1. predicting the next block type
        2. predicting the position and rotation of the next block that is going to be placed
        3. validating model output with provided pattern data
        4. validating track characteristics such as its size and placement
        5. backtracking if the networks get stuck and cannot predict a good enough outcome
        6. Reporting progress to the user of the class

    Args:
        block_model (keras.models.Sequential): the block model to use
        position_model (keras.models.Sequential): the positionmodel model to use
        lookback (int): how many previous blocks are fed into the network at a time
        seed_data (list): optional sample data to evaluate network preformance
        temperature (float): the temperature to use
    '''
    def __init__(self, block_model: Sequential, position_model: Sequential, lookback: int,
                seed_data: list, pattern_data: dict, scaler: object, temperature: float=1.2):
        self.block_model = block_model
        self.position_model = position_model
        self.lookback = lookback
        self.seed_data = seed_data
        self.pattern_data = pattern_data
        self.scaler = scaler
        self.inp_len = len(STADIUM_BLOCKS) + POS_LEN + ROTATE_LEN
        self.temperature = temperature
        self.running = False
        self.gmap = None

    @staticmethod
    def random_start_block():
        '''
        Returns a start block in a random direction.

        Returns:
            tuple: the randomized start block
        '''
        return (START_LINE_BLOCK, 0, 0, 0, random.randrange(0, 4), 0)

    @staticmethod
    def unpack_position_preds_vector(preds: tuple):
        '''
        Unpacks position and rotation prediction from the model prediction.

        Args:
            preds (tuple): (position_preds: np.array, rotation_preds: np.array)

        Returns:
            tuple: the unpacked position and rotation prediction
        '''
        pos_vec = [int(round(axis)) for axis in preds[0][0]]
        pos_rot = np.argmax(preds[1][0])
        return pos_vec, pos_rot

    # Source:
    # https://github.com/keras-team/keras/blob/master/examples/lstm_text_generation.py#L66
    # Helper function to sample an index from a probability array
    def sample(self, preds):
        preds = np.asarray(preds).astype('float64')
        preds = np.log(preds) / self.temperature
        exp_preds = np.exp(preds)
        preds = exp_preds / np.sum(exp_preds)
        probas = np.random.multinomial(1, preds, 1)
        return np.argmax(probas)

    def predict_next_block(self, X_block: np.array, X_position: np.array, block_override: int=-1,
                        blacklist: list=[], block_preds: np.array=None):
        '''
        Predicts the next block in the main building loop.

        Asks the block model for prediction of the next block type
        based on the encoded previous blocks and then feeds the
        output to the position model.

        Args:
            X_block (np.array): encoded block types of shape (1, lookback, blocks_len)
            X_position (np.array): encoded blocks of shape (1, lookback, inp_len)
            block_override (int): the block type to use instead of predicting it
            blacklist (list): the list containing block IDs that should not 
                              be considered when sampling
            block_preds (list): cached array of block predictions from the previous
                                prediction, used when backtracking
            
            Returns:
                tuple: (predicted_block: tuple, block_preds: np.array)
        '''
        if block_override != -1:
            next_block = block_override
        else:
            if block_preds is None:
                block_preds = self.block_model.predict(X_block)[0]
                block_preds = block_preds * TECH_BLOCK_WEIGHTS

            block_preds[np.asarray(blacklist, dtype=int) - 1] = 0
            next_block = self.sample(block_preds) + 1

        X_position = np.roll(X_position, -1, 1)
        X_position[0, -1] = block_to_vec((next_block, 0, 0, 0, 0), self.inp_len, len(STADIUM_BLOCKS), self.scaler, False)

        pos_preds = self.position_model.predict(X_position)
        pos_vec, pos_rot = self.unpack_position_preds_vector(pos_preds)
        return (next_block, *pos_vec, pos_rot), block_preds

    def sample_seed(self, seed_len: int) -> list:
        '''
        Generates a random sample from seed data. Used for
        evaluating network performance when completing e.g training data samples.

        Args:
            seed_len (int): track length to seed
        
        Returns:
            list: the track from the seed
        '''
        seed_idx = random.randrange(0, len(self.seed_data))
        seed = self.seed_data[seed_idx][1][:seed_len]
        return seed

    def score_prediction(self, prev_block: tuple, next_block: tuple) -> int:
        '''
        Scores the prediction using the previous block and the block that
        was just placed, using pattern data.

        Args:
            prev_block (tuple): the previous block
            next_block (tuple): the block that was just placed

        Returns:
            int: the prediction score
        '''
        prev_block = (prev_block[BID], 0, 0, 0, prev_block[BROT])
        prev_block, next_block = rotate_track_tuples([prev_block, next_block], 4 - prev_block[BROT] % 4)

        next_block = (next_block[BID], next_block[BX] - prev_block[BX], next_block[BY] -
                      prev_block[BY], next_block[BZ] - prev_block[BZ], next_block[BROT])

        target = (prev_block[BID], next_block)
        try:
            return self.pattern_data[target]
        except KeyError:
            return 0

    def prepare_inputs(self):
        '''
        Prepares the block and position vector encodings used by predict_next_block.

        Returns:
            tuple: (X_block: np.array, X_position: np.array)
        '''
        X_block = np.zeros((1, self.lookback, len(STADIUM_BLOCKS)), dtype=np.bool)
        X_position = np.zeros((1, self.lookback, self.inp_len))

        blocks = self.gmap.track[-self.lookback:]
        i = -1
        for block in reversed(blocks):
            X_position[0, i] = block_to_vec(block, self.inp_len, len(STADIUM_BLOCKS), self.scaler, True)
            X_block[0, i] = one_hot_bid(block[BID], len(STADIUM_BLOCKS))
            i -= 1

        return X_block, X_position

    def stop(self):
        '''
        Stops the building process.
        '''
        self.running = False

    def build(self, track_len: int, use_seed: bool=False, failsafe: bool=True, verbose: bool=True,
            put_finish: bool=True, progress_callback=None, map_size: tuple=(20, 8, 20)):
        '''
        Builds the track according to the parameters.

        Args:
            track_len (int): the track length, in blocks
            use_seed (bool): whether to use a random seed from the seed data
            failsafe (bool): whether to enable various checking heuristics
            verbose (bool): print additional information while building
            put_finish (bool): whether to put a finish as the last block
            progress_callback: a function that is called whenever a new block is placed
            map_size (tuple): the map size to build the track in
        
        Returns:
            list: the resulting track
        '''
        self.running = True

        fixed_y = random.randrange(1, 7)
        if not self.gmap:
            self.gmap = GameMap(Vector3(map_size[0], map_size[1], map_size[2]), Vector3(0, fixed_y, 0))

        if use_seed and self.seed_data:
            self.gmap.track = self.sample_seed(3)
        elif len(self.gmap) == 0:
            self.gmap.add(self.random_start_block())

        self.gmap.update()

        blacklist = []
        current_block_preds = None
        while len(self.gmap) < track_len:
            if not self.running:
                return None

            end = len(self.gmap) == track_len - 1
            if len(blacklist) >= 10 or (len(blacklist) == 1 and end):
                if verbose:
                    print('More than 10 fails, going back.')

                if len(self.gmap) > track_len - 5:
                    back = 5
                elif end:
                    back = 10
                else:
                    back = random.randrange(2, 6)

                end_idx = min(len(self.gmap) - 1, back)
                if end_idx > 0:
                    del self.gmap.track[-end_idx:len(self.gmap)]

                blacklist = []
                current_block_preds = None

            X_block, X_position = self.prepare_inputs()

            block_override = FINISH_LINE_BLOCK if end and put_finish else -1

            next_block, current_block_preds = self.predict_next_block(
                X_block[:], X_position[:], block_override=block_override, blacklist=blacklist, block_preds=current_block_preds
            )

            self.gmap.add(next_block)
            decoded = self.gmap.decoded

            if failsafe:
                # Do not exceed map size
                if self.gmap.exceeds_map_size():
                    blacklist.append(next_block[BID])
                    self.gmap.pop()
                    continue

                occ = occupied_track_vectors([decoded[-1]])
                if len(occ) > 0:
                    min_y_block = min(occ, key=lambda pos: pos.y).y
                else:
                    min_y_block = decoded[-1][BY]

                # If we are above the ground
                if min_y_block > 1 and next_block[BID] in GROUND_BLOCKS:
                    blacklist.extend(GROUND_BLOCKS)
                    self.gmap.pop()
                    continue

                if (intersects(decoded[:-1], decoded[-1]) or  # Overlaps the track
                        (next_block[BID] == FINISH_LINE_BLOCK and not end)):  # Tries to put finish before desired track length
                    blacklist.append(next_block[BID])
                    self.gmap.pop()
                    continue

                if self.score_prediction(self.gmap[-2], next_block) < 5:
                    blacklist.append(next_block[BID])
                    self.gmap.pop()
                    continue

            blacklist = []
            current_block_preds = None

            next_block = (next_block[BID], next_block[BX], next_block[BY],
                          next_block[BZ], next_block[BROT])

            if progress_callback:
                progress_callback(len(self.gmap), track_len)

            if verbose:
                print(len(self.gmap))

        result_track = self.gmap.center()
        result_track = [block for block in result_track if block[BID] != STADIUM_BLOCKS['StadiumGrass']]
        return result_track
Example #2
0
class Builder(object):
    def __init__(self, block_model, position_model, lookback, seed_data, pattern_data, scaler, temperature=1.2, reset=True):
        self.block_model = block_model
        self.position_model = position_model
        self.lookback = lookback
        self.seed_data = seed_data
        self.pattern_data = pattern_data
        self.scaler = scaler
        self.inp_len = len(STADIUM_BLOCKS) + POS_LEN + ROTATE_LEN
        self.temperature = temperature
        self.reset = reset
        self.running = False
        self.gmap = None

    @staticmethod
    def random_start_block():
        return (START_LINE_BLOCK, 0, 0, 0, random.randrange(0, 4), 0)

    # Source:
    # https://github.com/keras-team/keras/blob/master/examples/lstm_text_generation.py#L66
    # Helper function to sample an index from a probability array
    def sample(self, preds):
        preds = np.asarray(preds).astype('float64')
        preds = np.log(preds) / self.temperature
        exp_preds = np.exp(preds)
        preds = exp_preds / np.sum(exp_preds)
        probas = np.random.multinomial(1, preds, 1)
        return np.argmax(probas)

    def unpack_position_preds_vector(self, preds):
        pos_vec = [int(round(axis)) for axis in preds[0][0]]
        pos_rot = np.argmax(preds[1][0])
        return pos_vec, pos_rot

    def predict_next_block(self, X_block, X_position, block_override=-1, blacklist=[], block_preds=None):
        if block_override != -1:
            next_block = block_override
        else:
            if block_preds is None:
                block_preds = self.block_model.predict(X_block)[0]
                block_preds = block_preds * TECH_BLOCK_WEIGHTS

            for bid in blacklist:
                block_preds[bid - 1] = 0

            next_block = self.sample(block_preds) + 1

        for i in range(1, self.lookback):
            X_position[0][i - 1] = X_position[0][i]

        X_position[0][-1] = block_to_vec((next_block, 0, 0, 0, 0), self.inp_len, len(STADIUM_BLOCKS), self.scaler, False)

        pos_preds = self.position_model.predict(X_position)
        pos_vec, pos_rot = self.unpack_position_preds_vector(pos_preds)
        return (next_block, pos_vec[0], pos_vec[1], pos_vec[2], pos_rot), block_preds

    def sample_seed(self, seed_len):
        seed_idx = random.randrange(0, len(self.seed_data))
        seed = self.seed_data[seed_idx][1][:seed_len]
        return seed

    def score_prediction(self, prev_block, next_block):
        prev_block = (prev_block[BID], 0, 0, 0, prev_block[BROT])
        normalized = rotate_track_tuples(
            [prev_block, next_block], 4 - prev_block[BROT] % 4)

        prev_block = normalized[0]
        next_block = normalized[1]
        next_block = (next_block[BID], next_block[BX] - prev_block[BX], next_block[BY] -
                      prev_block[BY], next_block[BZ] - prev_block[BZ], next_block[BROT])

        target = (prev_block[BID], next_block)
        try:
            return self.pattern_data[target]
        except KeyError:
            return 0

    def prepare_inputs(self):
        X_block = np.zeros((1, self.lookback, len(STADIUM_BLOCKS)), dtype=np.bool)
        X_position = np.zeros((1, self.lookback, self.inp_len))

        blocks = self.gmap.track[-self.lookback:]
        i = -1
        for block in reversed(blocks):
            X_position[0][i] = block_to_vec(block, self.inp_len, len(STADIUM_BLOCKS), self.scaler, True)
            X_block[0][i] = one_hot_bid(block[BID], len(STADIUM_BLOCKS))
            i -= 1

        return X_block, X_position

    def stop(self):
        self.running = False

    def build(self, 
        track_len,
        use_seed=False,
        failsafe=True,
        verbose=True,
        save=True,
        put_finish=True,
        progress_callback=None,
        map_size=(20, 8, 20)):

        self.running = True

        fixed_y = random.randrange(1, 7)
        if not self.gmap or self.reset:
            self.gmap = GameMap(
                Vector3(map_size[0], map_size[1], map_size[2]), Vector3(0, fixed_y, 0))

        if use_seed and self.seed_data:
            self.gmap.track = self.sample_seed(3)
        elif len(self.gmap) == 0:
            self.gmap.add(self.random_start_block())

        print(self.gmap.track)
        self.gmap.update()

        blacklist = []
        current_block_preds = None
        while len(self.gmap) < track_len:
            if not self.running:
                return None

            end = len(self.gmap) == track_len - 1
            if len(blacklist) >= 10 or (len(blacklist) == 1 and end) and self.reset:
                if verbose:
                    print('More than 10 fails, going back.')

                if len(self.gmap) > track_len - 5:
                    back = 5
                elif end:
                    back = 10
                else:
                    back = random.randrange(2, 6)

                end_idx = min(len(self.gmap) - 1, back)
                if end_idx > 0:
                    del self.gmap.track[-end_idx:len(self.gmap)]

                blacklist = []
                current_block_preds = None

            X_block, X_position = self.prepare_inputs()

            override_block = FINISH_LINE_BLOCK if end and put_finish else -1

            next_block, current_block_preds = self.predict_next_block(
                X_block[:], X_position[:], override_block, blacklist=blacklist, block_preds=current_block_preds)

            self.gmap.add(next_block)
            decoded = self.gmap.decoded

            if failsafe:
                # Do not exceed map size
                if self.gmap.exceeds_map_size():
                    blacklist.append(next_block[BID])
                    self.gmap.pop()
                    continue

                occ = occupied_track_vectors([decoded[-1]])
                if len(occ) > 0:
                    min_y_block = min(occ, key=lambda pos: pos.y).y
                else:
                    min_y_block = decoded[-1][BY]

                # If we are above the ground
                if min_y_block > 1 and next_block[BID] in GROUND_BLOCKS:
                    blacklist.extend(GROUND_BLOCKS)
                    self.gmap.pop()
                    continue

                if (intersects(decoded[:-1], decoded[-1]) or  # Overlaps the track
                        (next_block[BID] == FINISH_LINE_BLOCK and not end)):  # Tries to put finish before desired track length
                    blacklist.append(next_block[BID])
                    self.gmap.pop()
                    continue

                if self.score_prediction(self.gmap[-2], next_block) < 5:
                    blacklist.append(next_block[BID])
                    self.gmap.pop()
                    continue

            blacklist = []
            current_block_preds = None

            next_block = (next_block[BID], next_block[BX], next_block[BY],
                          next_block[BZ], next_block[BROT])

            if progress_callback:
                progress_callback(len(self.gmap), track_len)

            if verbose:
                print(len(self.gmap))

        result_track = self.gmap.center()
        result_track = [block for block in result_track if block[BID] != STADIUM_BLOCKS['StadiumGrass']]
        return result_track