Using the level builder #656
Replies: 4 comments 3 replies
-
|
You'd have to manually build the clone |
Beta Was this translation helpful? Give feedback.
-
|
Then what is the purpose of the "existing level number" field? Just for information? |
Beta Was this translation helpful? Give feedback.
-
|
Hey ! If you need to duplicate an existing level to edit it, well, I have made a tool for it in Python! Basically, you have to feed it an image, and it will output a grid with a code (1 letter = 1 color). If it's the same level builder as before, you can "code" your level, by giving it the grid directly made. The command I use to run my code is: with import sys
import string
import numpy as np
from PIL import Image
from sklearn.cluster import KMeans
# --- Auto-crop the grid from a screenshot ---
import cv2
def load_image(path):
"""Loads an image file and converts it to RGB."""
return cv2.cvtColor(cv2.imread(path), cv2.COLOR_BGR2RGB)
def save_image(img, path):
"""Saves an RGB image (numpy array) as PNG."""
Image.fromarray(img).save(path)
def find_grid_bbox(img_rgb, debug=False):
"""
Automatically detects the bounding box of the Queensgame grid.
Returns (x_min, y_min, x_max, y_max).
"""
# 1. Convert to grayscale, then threshold to isolate black borders
gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)
# Black borders = low values. The background is dark but not as black as the borders.
# We look for very black pixels
_, mask_black = cv2.threshold(gray, 20, 255, cv2.THRESH_BINARY_INV)
# Morphology to "close" small holes and connect borders
kernel = np.ones((3,3), np.uint8)
mask_clean = cv2.morphologyEx(mask_black, cv2.MORPH_CLOSE, kernel, iterations=2)
mask_clean = cv2.morphologyEx(mask_clean, cv2.MORPH_OPEN, kernel, iterations=1)
# 2. Find contours, then the biggest rectangular contour (the grid)
contours, _ = cv2.findContours(mask_clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
h, w = gray.shape
best_rect = None
best_area = 0
for cnt in contours:
x, y, ww, hh = cv2.boundingRect(cnt)
area = ww * hh
# We seek a rectangle that's large enough (at least 20% of the image) and not stuck to the edges
margin = 10
if area > 0.2 * h * w and \
x > margin and y > margin and \
x+ww < w-margin and y+hh < h-margin:
if area > best_area:
best_area = area
best_rect = (x, y, x+ww, y+hh)
if debug:
dbg = img_rgb.copy()
if best_rect:
cv2.rectangle(dbg, (best_rect[0], best_rect[1]), (best_rect[2], best_rect[3]), (255,0,0), 3)
Image.fromarray(dbg).save("debug_bbox.png") # Just a little debug image, to be sure that it's doing a good job of detecting. You can remove it if needed
if not best_rect:
raise RuntimeError("Grid not detected: adjust thresholds or check the image.")
return best_rect
def autocrop_grid(input_path, output_path, debug=False):
"""Automatically crops the Queensgame grid from a screenshot."""
img = load_image(input_path)
x_min, y_min, x_max, y_max = find_grid_bbox(img, debug=debug)
# Optional: small padding to avoid cutting off black borders. I let it, because it make me a good grid detection
pad = 2
h, w, _ = img.shape
x_min = max(0, x_min - pad)
y_min = max(0, y_min - pad)
x_max = min(w, x_max + pad)
y_max = min(h, y_max + pad)
cropped = img[y_min:y_max, x_min:x_max]
save_image(cropped, output_path)
return x_min, y_min, x_max, y_max
# --- Process a cropped grid image ---
def detect_grid_size(image_path, plot_debug=False):
img = Image.open(image_path).convert('L')
arr = np.array(img)
arr_bin = (arr < 128).astype(np.uint8) # 1 = black, 0 = white
# Vertical and horizontal projections (sum of black pixels)
v_proj = np.sum(arr_bin, axis=0)
h_proj = np.sum(arr_bin, axis=1)
# Detect columns/rows with lots of black (separators)
v_lines = np.where(v_proj > v_proj.max() * 0.7)[0]
h_lines = np.where(h_proj > h_proj.max() * 0.7)[0]
# Group close indices (in case a black line is several pixels wide)
def group_lines(lines, min_dist=5):
grouped = []
current = []
for l in lines:
if not current or l - current[-1] <= min_dist:
current.append(l)
else:
grouped.append(current)
current = [l]
if current:
grouped.append(current)
# Take the center of each group
return [int(np.mean(g)) for g in grouped]
v_lines_grouped = group_lines(v_lines)
h_lines_grouped = group_lines(h_lines)
grid_w = len(v_lines_grouped) - 1
grid_h = len(h_lines_grouped) - 1
# If the grid is square, take the max
grid_size = max(grid_w, grid_h)
print(f"[INFO] Detected grid: {grid_size} x {grid_size}")
# Optional: visual debug. It's useful to check, but not essential. You can comment this part
if plot_debug:
import matplotlib.pyplot as plt
plt.imshow(arr, cmap='gray')
for x in v_lines_grouped:
plt.axvline(x, color='red')
for y in h_lines_grouped:
plt.axhline(y, color='blue')
plt.title(f"Detected grid: {grid_size} x {grid_size}")
plt.show()
return grid_size, v_lines_grouped, h_lines_grouped
def crop_grid_by_lines(img, v_lines, h_lines):
# Crop each cell according to detected separators coordinates
cells = []
for y in range(len(h_lines)-1):
row = []
for x in range(len(v_lines)-1):
cell = img.crop((v_lines[x]+2, h_lines[y]+2, v_lines[x+1]-2, h_lines[y+1]-2))
row.append(cell)
cells.append(row)
return cells
def get_cell_colors(cells):
grid_colors = []
for row in cells:
grid_row = []
for cell in row:
np_cell = np.array(cell)
mean_color = np_cell.reshape(-1, 3).mean(axis=0)
grid_row.append(mean_color)
grid_colors.append(grid_row)
return np.array(grid_colors)
def cluster_colors(grid_colors, n_colors=None):
flat_colors = grid_colors.reshape(-1, 3)
if n_colors is None:
from sklearn.metrics import silhouette_score
best_k = 8
best_score = -1
for k in range(6, 13):
kmeans = KMeans(n_clusters=k, n_init=10).fit(flat_colors)
score = silhouette_score(flat_colors, kmeans.labels_)
if score > best_score:
best_score = score
best_k = k
n_colors = best_k
kmeans = KMeans(n_clusters=n_colors, n_init=10).fit(flat_colors)
labels = kmeans.labels_.reshape(grid_colors.shape[:2])
return labels, kmeans.cluster_centers_
def label_grid(labels):
letters = list(string.ascii_uppercase)
grid = [[letters[label] for label in row] for row in labels]
return grid
def print_grid_py(grid):
print("grid = [")
for row in grid:
print(" " + repr(row) + ",")
print("]")
def main(image_path, n_colors=None, autocrop=False, debug=False):
if autocrop:
# Generate a temporary filename for the cropped image
output_cropped = "cropped_grid_temp.png"
autocrop_grid(image_path, output_cropped, debug=debug)
grid_img_path = output_cropped
else:
grid_img_path = image_path
pil_img = Image.open(grid_img_path).convert('RGB')
grid_size, v_lines, h_lines = detect_grid_size(grid_img_path, plot_debug=debug)
cells = crop_grid_by_lines(pil_img, v_lines, h_lines)
grid_colors = get_cell_colors(cells)
labels, centers = cluster_colors(grid_colors, n_colors=n_colors)
grid = label_grid(labels)
print_grid_py(grid)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Queensgame automatic grid detection and extraction.")
parser.add_argument("image", help="Input image file (screenshot or cropped grid)")
parser.add_argument("--n_colors", type=int, default=None, help="Number of colors to detect (optional)")
parser.add_argument("--autocrop", action="store_true", help="Automatically detect and crop the grid from a screenshot")
parser.add_argument("--debug", action="store_true", help="Show debug images and save detected bbox")
args = parser.parse_args()
main(args.image, n_colors=args.n_colors, autocrop=args.autocrop, debug=args.debug) |
Beta Was this translation helpful? Give feedback.
-
|
You could also paste a screenshot directly in the level builder. |
Beta Was this translation helpful? Give feedback.

Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
#helpwanted
How does the level builder work if you want to start with a clone of an existing level?
Beta Was this translation helpful? Give feedback.
All reactions