De Scripts Básicos a Algoritmos Avanzados: Mid-Game en The Farmer Was Replaced

De Scripts Básicos a Algoritmos Avanzados: Mid-Game en The Farmer Was Replaced


1. Optimización del Movimiento: Geometría Toroidal

Cuando le pides al dron que vaya a una posición (X, Y), el camino más rápido no siempre es caminar en línea recta. Dado que el mapa de The Farmer Was Replaced tiene geometría toroidal (si caminas hacia el Oeste en X=0, apareces en el extremo Este), la función debe calcular si es más corto cruzar el borde o avanzar directo.

Demostración The Farmer Was Replaced

La Matemática detrás de go_to(x, y)

Si estás en X = 0 , Y = 0 y quieres ir a X = 13 , Y=1 en una grilla de 10x10:

  • Camino directo: 0 -> 1 -> 2 -> … -> 14 (14 pasos al Este).
  • Camino directo: 0 -> 1 (1 pasos al Norte).
  • Total de pasos: 15
Demostración The Farmer Was Replaced
  • Camino toroidal: 0 -> 1 -> 2 -> … -> 4 (4 solo paso al Oeste).
  • Camino toroidal: 0 -> 1 (1 pasos al Norte).
  • Total de pasos: 5
Demostración The Farmer Was Replaced

Esto, reduce demasiado el tiempo entre distancias largas.

El operador módulo % size nos permite calcular la distancia real convirtiendo desplazamientos negativos en la ruta más corta alrededor del toroide.

def go_to(tx, ty):
  # Sacamos el tamaño de nuestro espacio
	size = get_world_size() 
	
	# Calculo para determinar camino en X
	x = get_pos_x()
	east = (tx - x) % size #Obtine valor absoluto de la distancia
	west = (x - tx) % size #Obtine valor absoluto de la distancia
	
  #Determina cual es el camino mas corto y ejecuta
	if east <= west:
		for _ in range(east):
			move(East)
	else:
		for _ in range(west):
			move(West)

	# Calculo para determinar camino en Y
	y = get_pos_y()
	north = (ty - y) % size #Obtine valor absoluto de la distancia
	south = (y - ty) % size #Obtine valor absoluto de la distancia
	
  #Determina cual es el camino mas corto y ejecuta
	if north <= south:
		for _ in range(north):
			move(North)
	else:
		for _ in range(south):
			move(South)

2. Helpers y Funciones Utilitarias Defensivas

Para evitar que las funciones principales fallen por falta de tecnología o insumos, encapsulamos las acciones básicas con comprobaciones de seguridad.

def get_full_grid_coords():
	size = get_world_size()
	coords = []
	for x in range(size):
		for y in range(size):
			coords.append((x, y))
	return coords

def safe_plant_type(entity_type):
	safe_plant()
	plant(entity_type)

def safe_plant():
	if can_harvest():
		harvest()

def safe_water():
	if num_unlocked(Unlocks.Watering) == 0:
		return
	if num_items(Items.Water) > 0 and get_water() < 0.7:
		use_item(Items.Water)
		
def ensure_grassland():
	if get_ground_type() != Grounds.Grassland:
		till()
		
def ensure_soil():
	if get_ground_type() != Grounds.Soil:
		till()

def safe_fertilize():
	if num_unlocked(Unlocks.Fertilizer) > 0 and num_items(Items.Fertilizer) > 0:
		use_item(Items.Fertilizer)
		
def safe_cure():
	if num_items(Items.Weird_Substance) > 0:
		use_item(Items.Weird_Substance)

3. Módulos de Farmeo Especializados

Cada cultivo del mid-game introduce un problema algorítmico único que debemos resolver antes de pasar al orquestador principal.

A. Girasoles: Clasificación en Memoria (Bucket Sort / Counting Sort)

Cosechar el girasol con la mayor cantidad de pétalos en el campo otorga un multiplicador de hasta 8x en la recolección de Power. Sin embargo, los girasoles nacen con un número variable de pétalos (entre 7 y 15) distribuidos de forma aleatoria por la grilla.

El Dilema de Eficiencia: O(N^3) vs O(N^2)

  • Enfoque Ingenuo (O(N^3)): Buscar en cada paso el girasol con más pétalos escaneando todo el mapa. En una grilla de 10x10 (100 casillas), esto implica recorrer 100 casillas para encontrar el de 15 pétalos, cosecharlo, y volver a recorrer 100 casillas para buscar el siguiente. Esto genera un desperdicio masivo de ticks de ejecución.

  • Solución con Bucket Sort (O(N^2)): Aplicamos una variante de Bucket Sort (o Counting Sort). Hacemos una sola pasada de reconocimiento para escanear el terreno y clasificar las coordenadas en “cubos” o contenedores según su valor.

Demostración The Farmer Was Replaced

¿Cómo funciona la teoría del Bucket Sort en el código?

  1. Creación de Cubos (Buckets): Inicializamos un diccionario donde cada clave es el número de pétalos posible (de 15 a 7) y su valor es una lista vacía de coordenadas: petals_map = {15: [], 14: [], ..., 7: []}.

  2. Distribución en Memoria (Scatter): El dron recorre la grilla una sola vez (100 pasos). Mide cada girasol con measure() y guarda tuplas (x, y) en el cubo correspondiente a su número de pétalos.

  3. Cosecha Directa (Gather): Iteramos sobre las claves del diccionario en orden descendente (15 -> 7). El dron se desplaza directo a las coordenadas guardadas usando go_to(x, y) y cosecha con la garantía de mantener el multiplicador al máximo sin realizar un solo escaneo extra.

def farm_sunflower(coords):
  # Se obtiene costos
	cost = get_cost(Entities.Sunflower)
	cost_carrot = cost[Items.Carrot]
	
  # Realiza un ciclo de plantacion
	for x, y in coords:
		if num_items(Items.Carrot) < cost_carrot:
			return False
		go_to(x, y)
		ensure_soil()
		safe_water()
		safe_plant_type(Entities.Sunflower)

  # Realiza el mapeo de los petalos mediante un diccionario
	petals_map = {}
	for p in range(15, 6, -1):
		petals_map[p] = []

  # Realiza el llenado de los tamaños
	for x, y in coords:
		go_to(x, y)
		p_count = measure()
		if p_count != None and p_count in petals_map:
			petals_map[p_count].append((x, y))

   # Realiza la cosecha en orden
	for p_count in range(15, 6, -1):
		target_coords = petals_map[p_count]
		
		for x, y in target_coords:
			go_to(x, y)
			if can_harvest():
				harvest()

	return True

B. Cactus: Ordenamiento de Matrices (Bubble Sort Bidimensional)

Cosechar un cactus en una grilla perfectamente ordenada desencadena un efecto dominó recursivo que recolecta todo el campo de golpe. La recompensa es exponencial: cosechar n cactus de forma simultánea otorga n^2 Items.Cactus.

La Regla del Orden Estricto

Para que la reacción en cadena ocurra, cada cactus debe cumplir con una condición de vecindad basada en su tamaño (medido con measure(), de 0 a 9):

  • Norte (N) y Este (E): Deben ser vecinos de tamaño mayor o igual (>=).
  • Sur (S) y Oeste (W): Deben ser vecinos de tamaño menor o igual (<=).

Esta regla implica que la grilla debe quedar ordenada de forma ascendente de Oeste a Este en las filas, y de Sur a Norte en las columnas.

Esquema del algoritmo Bubble Sort intercambiando elementos adyacentes

Aplicando Bubble Sort en 2D con swap()

A diferencia de una lista convencional en memoria donde reordenamos índices, aquí el dron debe desplazarse físicamente e intercambiar las plantas en el mapa usando swap(North) o swap(East).

El algoritmo se divide en dos fases iterativas de Bubble Sort (O(N^3) en llamadas de movimiento):

  1. Ordenamiento Horizontal (Filas): Iteramos sobre cada fila Y. Comparamos el cactus actual con su vecino del Este mediante measure() > measure(East). Si está desordenado, ejecutamos swap(East) y avanzamos.
  2. Ordenamiento Vertical (Columnas): Iteramos sobre cada columna X. Comparamos la casilla actual con su vecina del Norte mediante measure() > measure(North). Si es mayor, ejecutamos swap(North).
Esquema del algoritmo Bubble Sort intercambiando elementos adyacentes

Al finalizar ambas pasadas, la matriz entera queda perfectamente ordenada. Bastará con posicionar al dron en la esquina (0,0) y ejecutar un solo harvest() para recoger la cosecha masiva de n^2.

def farm_cactus(coords):
	size = get_world_size()
	cost = get_cost(Entities.Cactus)
	cost_carrot = cost[Items.Pumpkin]

	# 1. FASE DE SIEMBRA
	for x, y in coords:
		if num_items(Items.Carrot) < cost_carrot:
			return False
		go_to(x, y)
		ensure_soil()
		safe_water()
		safe_plant_type(Entities.Cactus)

	# 2. ESPERA DE CRECIMIENTO
	for x, y in coords:
		go_to(x, y)
		while not can_harvest():
			safe_water()

	# 3. ORDENAMIENTO BUBBLE SORT DE MATRIZ
	# Ordenar Filas (Eje X: De Oeste a Este)
	for y in range(size):
		for i in range(size):
			for x in range(size - 1):
				go_to(x, y)
				if measure() > measure(East):
					swap(East)

	# Ordenar Columnas (Eje Y: De Sur a Norte)
	for x in range(size):
		for i in range(size):
			for y in range(size - 1):
				go_to(x, y)
				if measure() > measure(North):
					swap(North)

	# 4. COSECHA EN CADENA (Recompensa n^2)
	go_to(0, 0)
	harvest()
	return True

C. Dinosaurio: Ciclo Hamiltoniano (Snake Solver)

Equipar el Sombrero de Dinosaurio transformará la dinámica del juego por completo, convirtiendo la grilla en una partida del clásico Snake. Cada manzana recolectada hace crecer la cola del dron en 1 segmento y genera una nueva manzana en una posición aleatoria. Al desequipar el sombrero, se cosecha la cola obteniendo n^2 Items.Bone (donde n es la longitud alcanzada).

El Problema de las Colisiones y la Topología Rígida

Bajo esta mecánica, el juego impone dos restricciones severas:

  1. Desactivación del Toroide: No se puede cruzar los bordes del mapa.
  2. Colisiones Fatales: Si el dron intenta avanzar hacia una casilla ocupada por su propio cuerpo o cola, la función move() falla retornando False.

Si intentamos ir directamente tras la manzana mediante búsquedas voraces (Greedy), la cola terminará encerrando al dron y provocando una colisión inevitable.

Esquema del algoritmo Bubble Sort intercambiando elementos adyacentes

Garantía Topológica: El Ciclo Hamiltoniano

Un Ciclo Hamiltoniano es un camino cerrado dentro de un grafo que visita cada vértice (casilla) exactamente una vez y regresa al punto de partida. Al obligar al dron a seguir un ciclo hamiltoniano cerrado en loop infinito, garantizamos matemáticamente que nunca se estrellará, ya que la cabeza únicamente avanza hacia la casilla que la punta de la cola acaba de liberar.

Estructura del Recorrido en la Granja

Para construir este ciclo cerrado de forma determinista en una grilla NxN, dividimos el mapa en dos zonas:

  1. Autopista de Retorno Reservada (Columna X = 0): El dron asciende en línea recta desde (0,0) hasta (0, N-1) exclusivamente para regresar del Sur al Norte.
  2. Serpenteo de Limpieza (X<1): Descendemos serpenteando en Zig-Zag (Este-Oeste / Norte-Sur) por el resto del mapa hasta volver a la base en (1,0) y reconectar con (0,0).
Esquema del algoritmo Bubble Sort intercambiando elementos adyacentes

Cuando el cuerpo llena completamente el espacio y ya no existen casillas libres para avanzar, el dron detecta el bloqueo final, rompe el bucle de forma segura y desequipa el sombrero para cobrar la recompensa masiva en huesos.

def farm_dinosaur_bones(coords):
	clear()
	size = get_world_size()
	cost = get_cost(Entities.Apple)
	cost_cactus = cost[Items.Cactus]

	if num_items(Items.Cactus) < cost_cactus:
		return False

	# 1. Posicionar al dron en (0,0) antes de iniciar
	go_to(0, 0)

	# 2. Equipar el sombrero para arrancar el minijuego
	change_hat(Hats.Dinosaur_Hat)

	# 3. Bucle en Ciclo Hamiltoniano
	game_running = True
	while game_running:
		# A. Subir por la columna 0 hasta el extremo Norte
		while get_pos_y() < size - 1:
			if not move(North):
				game_running = False
				break
		
		if not game_running:
			break

		# B. Moverse a la derecha para iniciar el patrón en zigzag descendente/ascendente
		if not move(East):
			break

		# C. Recorrer el resto de la grilla (Columnas 1 a N-1)
		# El dron desciende por x=1..N-1 y serpentea
		for y in range(size - 1, -1, -1):
			if y % 2 == (size - 1) % 2:
				# Ir hacia la derecha (East) hasta el borde
				while get_pos_x() < size - 1:
					if not move(East):
						game_running = False
						break
			else:
				# Ir hacia la izquierda (West) hasta x = 1 (sin tocar x = 0)
				while get_pos_x() > 1:
					if not move(West):
						game_running = False
						break
			
			if not game_running:
				break

			# Bajar una fila si no estamos en la base (y = 0)
			if y > 0:
				if not move(South):
					game_running = False
					break

		if not game_running:
			break

		# D. Regresar a la columna 0 para cerrar el ciclo
		if not move(West):
			break

		# Verificar si nos quedamos sin cactus para la siguiente manzana
		if num_items(Items.Cactus) < cost_cactus:
			break

	# 4. Desequipar para cosechar los huesos (Items.Bone = n^2)
	change_hat(Hats.Straw_Hat)
	return True

D. Laberintos: Búsqueda en Grafos (DFS con Backtracking)

Fertilizar un arbusto con suficiente Items.Weird_Substance transformará la granja en un laberinto de setos (Entities.Hedge). Escondido en alguna coordenada aleatoria se encuentra un tesoro de oro (Entities.Treasure) cuya recompensa equivale al área total del mapa.

El Desafío de la Navegación a Ciegas

A diferencia de los cultivos convencionales, en el laberinto el dron no puede sobrevolar las paredes: la función move() retornará False si intenta atravesar un seto. Para encontrar el tesoro sin destruir el laberinto por error (lo cual ocurre si ejecutamos harvest() en una casilla equivocada), debemos tratar la grilla como un grafo no dirigido y aplicar un algoritmo de exploración sistemática.

Esquema del algoritmo Bubble Sort intercambiando elementos adyacentes

La Teoría: Depth-First Search (DFS) y Retroceso (Backtracking)

El algoritmo de Búsqueda en Profundidad (DFS) explora un camino hacia adelante tanto como sea posible antes de retroceder. Representamos las casillas del laberinto como nodos y los pasillos abiertos como aristas.

  1. Memoria de Nodos Visitados (set()): Mantenemos un conjunto dinámico visited con las tuplas (x, y) por las que el dron ya transitó. Esto evita que el dron entre en bucles infinitos oscilando entre dos casillas contiguas.
  2. Exploración Recursiva: En cada casilla, el dron evalúa las 4 direcciones (North, East, South, West). Si can_move(dir) es verdadero y la casilla destino no está en visited, el dron se desplaza e invoca recursivamente la función de búsqueda.
  3. Mecánica de Backtracking (Regreso sobre sus Pasos): Si el dron llega a un callejón sin salida (todas las direcciones adyacentes están bloqueadas o ya fueron visitadas), la llamada recursiva finaliza y el dron ejecuta move(reverse_dir) para regresar exactamente una casilla atrás y probar un camino alternativo.
Esquema del algoritmo Bubble Sort intercambiando elementos adyacentes

En cuanto get_entity_type() == Entities.Treasure se vuelve verdadero, la llamada retorna True, la pila recursiva se desenreda al instante y el dron reclama el botín sin dar un solo paso innecesario.

def solve_maze_dfs(visited, directions, rev_directions):
	if get_entity_type() == Entities.Treasure:
		harvest()
		return True

	curr_pos = (get_pos_x(), get_pos_y())
	visited.add(curr_pos)

	for i in range(4):
		direction = directions[i]
		reverse_dir = rev_directions[i]

		if can_move(direction):
			# Calcular la posición adyacente
			nx, ny = curr_pos[0], curr_pos[1]
			if direction == North: 
				ny += 1
			elif direction == South: 
				ny -= 1
			elif direction == East: 
				nx += 1
			elif direction == West: 
				nx -= 1

			# Si la casilla no ha sido visitada, avanzamos
			if (nx, ny) not in visited:
				move(direction)
				if solve_maze_dfs(visited, directions, rev_directions):
					return True
				# Backtracking: regresar si el camino fue un callejón sin salida
				move(reverse_dir)

	return False

def farm_maze(coords):
	# 1. Calcular el costo exacto de sustancia rara
	substance_needed = get_world_size() * (2 ** (num_unlocked(Unlocks.Mazes) - 1))
	
	if num_items(Items.Weird_Substance) < substance_needed:
		return False

	# 2. Plantar el arbusto inicial e invocar el laberinto
	go_to(0, 0)
	if get_ground_type() != Grounds.Grassland:
		till() # Asegurar terreno si es necesario
	plant(Entities.Bush)
	
	use_item(Items.Weird_Substance, substance_needed)

	# 3. Estructuras para la resolución DFS
	directions = [North, East, South, West]
	rev_directions = [South, West, North, East]
	visited = set()

	# 4. Resolver y Cosechar Tesoro
	solve_maze_dfs(visited, directions, rev_directions)
	return True

4. Orquestación Inteligente mediante Histeresis

Conforme escalamos hacia múltiples cultivos, surge un problema clásico de los sistemas de control: el Flapping o conmutación crítica.

El Problema del Limite Fijo (Sin Histeresis)

Si programamos el bot con un umbral único (por ejemplo: “Si Hay < 100,000, junta heno”), el dron entrará en un bucle ineficiente. Al llegar a 99,999, se desplazará a sembrar heno; cosechará un par de casillas, llegará a 100,001 y cambiará a otra tarea. Acto seguido, gastará 2 unidades de heno para sembrar zanahorias, el stock caerá a 99,999 y el ciclo de cambio brusco de contexto se repetirá infinitamente.

Este comportamiento destruye la tasa de cosecha por segundo (TPS) debido al costo de movimiento entre sectores.

La Solución: Control por Banda Min/Max (Histeresis)

Tomado del diseño de termostatos y sistemas electrónicos, aplicamos un Control por Histeresis utilizando banderas de estado booleanas que conservan memoria entre iteraciones del orquestador.

  1. Gatillo de Activación (Min / Low): El bot ignora la recolección de un recurso mientras se mantenga sobre el nivel crítico (STOCK_MIN_BASE = 100,000). Solo activa la bandera farming_x = True cuando cae por debajo de este límite.
  2. Gatillo de Desactivación (Max / High): Una vez encendida la bandera, la tarea no se interrumpe hasta que el inventario se llena por completo hasta el techo máximo (STOCK_MAX_BASE = 120,000).
Esquema del algoritmo Bubble Sort intercambiando elementos adyacentes

Evaluación de Prioridades en Cascadas

Al combinar la histeresis con una máquina de estados, el smart_farm() evalúa la salud de los almacenes en orden de dependencia en cada iteración del bucle principal:

  • Prioridad 1 (Power): Mantener la energía activa (2,000+) para sostener la velocidad 2x del dron.
  • Prioridad 2 y 3 (Insumos Base): Asegurar que Hay y Wood no caigan del colchón de 100k.
  • Prioridad 4 (Moneda Intermedia): Mantener el flujo de Carrots para pagar semillas avanzadas.
  • Prioridad 5 (Oro / Minijuegos): Ejecutar laberintos o minijuegos mientras los recursos base se mantengan en zona segura.
  • Prioridad por Defecto (Producción de Alto Valor): Farmeo continuo de Pumpkins gigantes o cultivos avanzados.
# --- UMBRALES DE HISTERESIS ---
STOCK_MIN_BASE = 100000
STOCK_MAX_BASE = 120000

POWER_MIN = 2000
POWER_MAX = 10000

GOLD_MIN = 10000
GOLD_MAX = 50000

# Banderas de estado
farming_grass = False
farming_wood = False
farming_power = False
farming_carrots = False
farming_gold = False

coords = get_full_grid_coords()

def smart_farm():
	global farming_grass
	global farming_wood
	global farming_power
	global farming_carrots
	global farming_gold
	
	hay = num_items(Items.Hay)
	wood = num_items(Items.Wood)
	carrots = num_items(Items.Carrot)
	power = num_items(Items.Power)
	gold = num_items(Items.Gold)

	# Evaluadores de Histeresis
	if hay < STOCK_MIN_BASE: 
		farming_grass = True
	elif hay >= STOCK_MAX_BASE:
		farming_grass = False

	if wood < STOCK_MIN_BASE: 
		farming_wood = True
	elif wood >= STOCK_MAX_BASE: 
		farming_wood = False

	if power < POWER_MIN: 
		farming_power = True
	elif power >= POWER_MAX: 
		farming_power = False

	if carrots < STOCK_MIN_BASE: 
		farming_carrots = True
	elif carrots >= STOCK_MAX_BASE: 
		farming_carrots = False

	if gold < GOLD_MIN: 
		farming_gold = True
	elif gold >= GOLD_MAX: 
		farming_gold = False

	# PRIORIDADES
	if farming_power:
		if carrots >= 100:
			farm_sunflower(coords)
			return

	if farming_grass:
		farm_grass(coords)
		return

	if farming_wood:
		farm_wood(coords)
		return   

	if farming_carrots:
		farm_carrots(coords)
		return

	# Prioridad: Recolectar Oro mediante Laberintos
	if farming_gold:
		farm_maze(coords)
		return

	# Producción Avanzada
	farm_giant_pumpkins(coords)

# Una vez finalizada la ronda paralela, continúa el flujo del dron principal
while True:
	smart_farm()

Conclusión

Con esta arquitectura modular, nuestra granja es capaz de gestionar recursos complejos, autocurarse, resolver minijuegos y mantener un flujo constante de producción sin atascarse.

En la Parte 3 (Late-Game / End-Game) exploraremos el procesamiento distribuido con múltiples drones en paralelo, optimización multihilo y sincronización sin condiciones de carrera.