File size: 9,064 Bytes
a09cfc1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import json
import sys
import os
from collections import Counter

# This file contains utility functions for analyzing and describing levels in both Lode Runner and Super Mario Bros.

# Could define these via the command line, but for now they are hardcoded
coarse_locations = True
coarse_counts = True
pluralize = True
give_staircase_lengths = False

def describe_size(count):
    if count <= 4: return "small"
    else: return "big"

def describe_quantity(count):
    if count == 0: return "no"
    elif count == 1: return "one"
    elif count == 2: return "two"
    elif count < 5: return "a few"
    elif count < 10: return "several"
    else: return "many"

def get_tile_descriptors(tileset):
    """Creates a mapping from tile character to its list of descriptors."""
    result = {char: set(attrs) for char, attrs in tileset["tiles"].items()}
    # Fake tiles. Should these contain anything? Note that code elsewhere expects everything to be passable or solid
    result["!"] = {"passable"}
    result["*"] = {"passable"}
    return result

def analyze_floor(scene, id_to_char, tile_descriptors, describe_absence):
    """Analyzes the last row of the 32x32 scene and generates a floor description."""
    WIDTH = len(scene[0])
    last_row = scene[-1]  # The FLOOR row of the scene
    solid_count = sum(
        1 for tile in last_row
        if tile in id_to_char and (
            "solid" in tile_descriptors.get(id_to_char[tile], []) or
            "diggable" in tile_descriptors.get(id_to_char[tile], [])
        )
    )
    passable_count = sum(
        1 for tile in last_row if "passable" in tile_descriptors.get(id_to_char[tile], [])
    )

    if solid_count == WIDTH:
        return "full floor"
    elif passable_count == WIDTH:
        if describe_absence:
            return "no floor"
        else:
            return ""
    elif solid_count > passable_count:
        # Count contiguous groups of passable tiles
        gaps = 0
        in_gap = False
        for tile in last_row:
            # Enemies are also a gap since they immediately fall into the gap
            if "passable" in tile_descriptors.get(id_to_char[tile], []) or "enemy" in tile_descriptors.get(id_to_char[tile], []):
                if not in_gap:
                    gaps += 1
                    in_gap = True
            elif "solid" in tile_descriptors.get(id_to_char[tile], []):
                in_gap = False
            else:
                print("error")
                print(tile)
                print(id_to_char[tile])
                print(tile_descriptors)
                print(tile_descriptors.get(id_to_char[tile], []))
                raise ValueError("Every tile should be passable, solid, or enemy")
        return f"floor with {describe_quantity(gaps) if coarse_counts else gaps} gap" + ("s" if pluralize and gaps != 1 else "")
    else:
        # Count contiguous groups of solid tiles
        chunks = 0
        in_chunk = False
        for tile in last_row:
            if "solid" in tile_descriptors.get(id_to_char[tile], []):
                if not in_chunk:
                    chunks += 1
                    in_chunk = True
            elif "passable" in tile_descriptors.get(id_to_char[tile], []) or "enemy" in tile_descriptors.get(id_to_char[tile], []):
                in_chunk = False
            else:
                print("error")
                print(tile)
                print(tile_descriptors)
                print(tile_descriptors.get(tile, []))
                raise ValueError("Every tile should be either passable or solid")
        return f"giant gap with {describe_quantity(chunks) if coarse_counts else chunks} chunk"+("s" if pluralize and chunks != 1 else "")+" of floor"

def count_in_scene(scene, tiles, exclude=set()):
    """ counts standalone tiles, unless they are in the exclude set """
    count = 0
    for r, row in enumerate(scene):
        for c, t in enumerate(row): 
            #if exclude and t in tiles: print(r,c, exclude)
            if (r,c) not in exclude and t in tiles:
                #if exclude: print((r,t), exclude, (r,t) in exclude)
                count += 1
    #if exclude: print(tiles, exclude, count)
    return count

def count_caption_phrase(scene, tiles, name, names, offset = 0, describe_absence=False, exclude=set()):
    """ offset modifies count used in caption """
    count = offset + count_in_scene(scene, tiles, exclude)
    #if name == "loose block": print("count", count)
    if count > 0: 
        return f" {describe_quantity(count) if coarse_counts else count} " + (names if pluralize and count > 1 else name) + "."
    elif describe_absence:
        return f" no {names}."
    else:
        return ""

def in_column(scene, x, tile):
    for row in scene:
        if row[x] == tile:
            return True

    return False

def analyze_ceiling(scene, id_to_char, tile_descriptors, describe_absence, ceiling_row = 1):
    """

    Analyzes ceiling row (0-based index) to detect a ceiling.

    Returns a caption phrase or an empty string if no ceiling is detected.

    """
    WIDTH = len(scene[0])

    row = scene[ceiling_row]
    solid_count = sum(1 for tile in row if "solid" in tile_descriptors.get(id_to_char[tile], []))
    
    if solid_count == WIDTH:
        return " full ceiling."
    elif solid_count > WIDTH//2:
        # Count contiguous gaps of passable tiles
        gaps = 0
        in_gap = False
        for tile in row:
            # Enemies are also a gap since they immediately fall into the gap, but they are marked as "moving" and not "passable"
            if "passable" in tile_descriptors.get(id_to_char[tile], []) or "moving" in tile_descriptors.get(id_to_char[tile], []):
                if not in_gap:
                    gaps += 1
                    in_gap = True
            else:
                in_gap = False
        result = f" ceiling with {describe_quantity(gaps) if coarse_counts else gaps} gap" + ("s" if pluralize and gaps != 1 else "") + "."

        # Adding the "moving" check should make this code unnecessary
        #if result == ' ceiling with no gaps.':
        #    print("This should not happen: ceiling with no gaps")
        #    print("ceiling_row:", scene[ceiling_row])
        #    result = " full ceiling."

        return result
    elif describe_absence:
        return " no ceiling."
    else:
        return ""  # Not enough solid tiles for a ceiling

def extract_tileset(tileset_path):
    # Load tileset
    with open(tileset_path, "r") as f:
        tileset = json.load(f)
        #print(f"tileset: {tileset}")
        tile_chars = sorted(tileset['tiles'].keys())
        # Wiggle room for the tileset to be a bit more flexible.
        # However, this requires me to add some bogus tiles to the list.
        # tile_chars.append('!') 
        # tile_chars.append('*') 
        #print(f"tile_chars: {tile_chars}")
        id_to_char = {idx: char for idx, char in enumerate(tile_chars)}
        #print(f"id_to_char: {id_to_char}")
        char_to_id = {char: idx for idx, char in enumerate(tile_chars)}
        #print(f"char_to_id: {char_to_id}")
        tile_descriptors = get_tile_descriptors(tileset)
        #print(f"tile_descriptors: {tile_descriptors}")

    return tile_chars, id_to_char, char_to_id, tile_descriptors

def flood_fill(scene, visited, start_row, start_col, id_to_char, tile_descriptors, excluded, pipes=False, target_descriptor=None):
    stack = [(start_row, start_col)]
    structure = []

    while stack:
        row, col = stack.pop()
        if (row, col) in visited or (row, col) in excluded:
            continue
        tile = scene[row][col]
        descriptors = tile_descriptors.get(id_to_char[tile], [])
        # Use target_descriptor if provided, otherwise default to old solid/pipe logic
        if target_descriptor is not None:
            if target_descriptor not in descriptors:
                continue
        else:
            if "solid" not in descriptors or (not pipes and "pipe" in descriptors) or (pipes and "pipe" not in descriptors):
                continue

        visited.add((row, col))
        structure.append((row, col))

        # Check neighbors
        for d_row, d_col in [(-1,0), (1,0), (0,-1), (0,1)]:
            # Weird special case for adjacent pipes
            if (id_to_char[tile] == '>' or id_to_char[tile] == ']') and d_col == 1: # if on the right edge of a pipe
                continue # Don't go right if on the right edge of a pipe
            if (id_to_char[tile] == '<' or id_to_char[tile] == '[') and d_col == -1: # if on the left edge of a pipe
                continue # Don't go left if on the left edge of a pipe

            n_row, n_col = row + d_row, col + d_col
            if 0 <= n_row < len(scene) and 0 <= n_col < len(scene[0]):
                stack.append((n_row, n_col))

    return structure