The Farmer Was Replaced

The Farmer Was Replaced

Not enough ratings
Solve The Maze in '24
By owenz
Solving the maze can be a complicated affair with so many strategies to choose from where does one start.

This is where you start, here with nothing till we build something.
   
Award
Favorite
Favorited
Unfavorite
The Problem!
So now you have a maze and are wondering how do I get this drone to that gold.


Now there are at least 2 other guides already,

https://gtm.steamproxy.vip/sharedfiles/filedetails/?id=2946730692
https://gtm.steamproxy.vip/sharedfiles/filedetails/?id=3068935898

Both of which offer a good amount of information and guidance on solving this problem.

Both lack a complete solution that sat well with me. So as a result I have explored this problem space a little more and offer it out to the world so that it can be built upon and made into something more.

Getting back to the problem at hand.

As I have already mentioned two other guides I will not get into the basics but talk about how we can build up from where they left off and implement a strategy that allows us to propagate forward the information about the maze in a away that does not soft lock us.

Not soft locking the maze solution is a hard problem, but we have the tools!

I will be mainly exploring the least walked direction to this problem and correcting anything that will not work with my solution during it as well.
Phase 1 - Building Code That Can Move Forward
Maze Creation

The first thing we need to do is make it so that the maze can be recreated dynamically. This will allow us to call simple functions to just get it done and not worry about how it is implemented.

It would be a really cool thing to see if any space on the board gives us a higher reward over time but that is beyond the scope of this guide so we will just randomize the starting (x, y) of the maze/drone. For that we will need a function to give us back a random int.

def randint(left, right): # Code can be seen in Code sections ...

Using the above function we can now get the random numbers we need to go to a random (x, y).

Let us create and call a function that generates the maze now.

def plant_maze(entity, ground): # Code can be seen in Code sections ...

Now, you may be wondering how the heck'n we can start randomly if we are not taking in a (x, y) into the plant function. We just move there first then plant at that location. The code code would look like

ws = get_world_size() x, y = randint(0, ws), randint(0, ws) move_to(x, y) plant_maze(get_entity_type, get_ground_type)

Upgrade The Plant

Now that we mentioned it, the plant function is the first thing we can change to get this maze created faster.

A few things of note
  • All plants can be planted on tilled soil
  • Only tilled soil can be watered
  • Fertilizer can be used basically instantly after the bush is grown till the maze is created

With these things in mind let's construct our plant_maze function.

def plant_maze(entity, ground): if entity() != Entities.Bush: harvest() if ground() != Grounds.Soil: till() plant(Entities.Bush) maintain_water_level(0.5) # Code in Helper Code Section while entity() != Entities.Hedge: if num_items(Items.Fertilizer) > 0 or trade(Items.Fertilizer, to_buy_at_once): use_item(Items.Fertilizer) else: return False return True

So all the thing that needed to happen are happening, but why am I return True/False you ask. By returning the truthiness of our success we can use it to determine if we need to switch from mazes back to a different phase so we can continue or not as we do need pumpkins for fertilizer.

Now we have a working maze creator and it does a very good job, there are still some things we could try to speed it up but that is left to the reader.

Navigation Destination

Next we need to get to the treasure, but the drone is a blind boi and cannot see the map from the same perspective that we can. So we have to tell it to go on an arduous journey which has never ending instructions to finally get its so coveted prize, that gold!

The method I will implement here is to calculate the least traveled direction. This will allow the little drone to circumnavigate the maze in a somewhat timely manner.

def move_maze(mv): # Code in Code Section ...

So here is the function signature that we will be using. We will be taking in a argument called mv which will be the propagated maze values, starting with an empty list.

Now going with that though let set up some basic abilities of the function

movements = (North, West, South, East) maze_values = mv

Ok, we are making the mv variable local to the scope of the function, faster access and no scoping blunders, and defining a variable that lets us have all the possible directions we can move locally.

Next, we need to populate the maze_values if it is not already done by the propagated values.

# Populate the maze values if maze_values == []: for i_ in range(get_world_size()): maze_values.append([]) for j_ in range(get_world_size()): maze_values[i_].append([]) for k in range(len(movements)): (maze_values[i_][j_]).append(0)

So, wow that is a lot of loops! (DON'T really need them but again that is left to the viewer)
The data structure that we are creating looks something like
maze_values = [ [[0, 0, 0, 0], ... ... ]

Where each of the int stored in the multi dimensional list is the value we will use to determine the least traveled direction.

Now it's time to get to the nitty gritty of this function and start moving around and mapping the least traveled as we go.

# Walk the maze until the trasure is found max_lwq = 12 while True: if get_entity_type() == Entities.Treasure: return maze_values lwd = 3 lwq = max_lwq pbw = (get_pos_x(), get_pos_y()) for d_ in range(len(movements)): imv = maze_values[pbw[0]][pbw[1]][d_] if imv < lwq: lwd = d_ lwq = maze_values[pbw[0]][pbw[1]][d_] if move(movements[lwd]): maze_values[pbw[0]][pbw[1]][lwd] += 1 b_ = lwd - 2 if b_ < 0: b_ += 4 maze_values[get_pos_x()][get_pos_y()][b_] += 1 else: maze_values[pbw[0]][pbw[1]][lwd] = max_lwq

Oh boy! There is a ton going on here. So starting off we need a "wall" value, or a value which will limit that direction for travel, so that the drone will not choose it. Most algorithms that use this approach will select much higher values for the lwq than 12, but for this implementation we need a lower value that can be overridden as the maze does change through it's permutations.

Next is the whole of the while True loop, first inside we have to check we hit the jackpot and exit from the loop if we did as that is our loops exit condition.

Following that we initialize several variables that will decide which direction the drone is moving. lwd is least walked direction and is set to 3, 3 is max index of directions, at the start just so we can access it outside the following for loop. lwq is the wall value we will use. pbw is just our current (x, y)

The loop

Inside the the for loop we are doing a bunch of work. The first thing is that the for loop is going over the directions we have possible in movements variable that we declared before. Then it attempts to move in that direction, if it failed we set that direction to lwq so that it is hard to select it later on.

Looking at this line
imv = maze_values[pbw[0]][pbw[1]][d_]

We are getting the value for selected direction

Then
if imv < lwq: lwd = d_ lwq = maze_values[pbw[0]][pbw[1]][d_]

Which is getting the least direction traveled, and as a side effect if this never results to True we will have a bug where lwd never changes so we can potentially get stuck on multiple solves, that's for later though.

Then the final chunk
if move(movements[lwd]): maze_values[pbw[0]][pbw[1]][lwd] += 1 b_ = lwd - 2 if b_ < 0: b_ += 4 maze_values[get_pos_x()][get_pos_y()][b_] += 1 else: maze_values[pbw[0]][pbw[1]][lwd] = max_lwq

This whole bit can be improved and shortened, basically what is happening is that if the movement is successful then we will add a point for this direction as we have traveled it before.

Then using the b_ variable we decide the opposite direction and add a point to the current cell we have moved too. Else a wall.
Phase 1 - Code
Phase 1 is where we can solve the maze in a basic manner. There is no reuse of any part of the previous maze nor could we use the bonus system to replant the maze to get more gold.

def plant_maze(entity, ground): if entity() == Entities.Hedge: return False if not plant_bush(entity(), ground()): while can_harvest(): if num_items(Items.Fertilizer) > 0 or trade(Items.Fertilizer, to_buy_at_once): use_item(Items.Fertilizer) if get_entity_type() == Entities.Hedge: break if entity() == Entities.Hedge: return True else: maintain_water_level(0.5) do_a_flip() return plant_maze(entity, ground) def move_maze(mv): movements = (North, West, South, East) maze_values = mv # Populate the maze values if maze_values == []: for i_ in range(get_world_size()): maze_values.append([]) for j_ in range(get_world_size()): maze_values[i_].append([]) for k in range(len(movements)): (maze_values[i_][j_]).append(0) norm_maze(maze_values) # Walk the maze until the trasure is found max_lwq = 12 while True: if get_entity_type() == Entities.Treasure: return maze_values lwd = 3 lwq = max_lwq pbw = (get_pos_x(), get_pos_y()) for d_ in range(len(movements)): imv = maze_values[pbw[0]][pbw[1]][d_] if imv < lwq: lwd = d_ lwq = maze_values[pbw[0]][pbw[1]][d_] if move(movements[lwd]): maze_values[pbw[0]][pbw[1]][lwd] += 1 b_ = lwd - 2 if b_ < 0: b_ += 4 maze_values[get_pos_x()][get_pos_y()][b_] += 1 else: maze_values[pbw[0]][pbw[1]][lwd] = max_lwq def mazes(): maze_val = [] ws = get_world_size() sx, sy = randint(0, ws), randint(0, ws) if get_entity_type() != Entities.Hedge: clear() move_self(sx, sy) # Plant Maze if not plant_maze(get_entity_type, get_ground_type): print("Failed") # Solve Maze maze_val = [] while get_entity_type() == Entities.Treasure: if num_items(Items.Fertilizer) > 0 or trade( Items.Fertilizer, to_buy_at_once ): use_item(Items.Fertilizer) maze_val = move_maze(maze_val) # quick_print("Next Treasure", measure()) harvest() if num_items(Items.Water_Tank) < 1000: water_maintenance(1) return maze_val
Phase 2 - Reuse The Previous Maze (Tech Talk)
Alright, by this point we have all the bits and bobs needed to create and solve a maze. At max unlocks and map size it takes <10 secs to solve which is worth 500 Gold!

Now, how do we make this WAY better, like way, way better? How about using the regrow option for the treasure before harvesting it.

Let me give you some metrics to start, without showing you anything else to see if it's worth.
All these tests are done with only propagation no use of something like A* to quickly get to the destination.

Also as we have to normalize the data structure each propagation it takes a long time as the data-structure is terrible, again another thing the viewer can work on.

I
Time
Gold
G/S
1
6.5s
500
76.9
10
30.8s
5000
162.3
20
60.2s
10000
166.1
30
85.6s
15000
175.2

As we can see, there are time savings and we get a sizable bonus for each retry for the extra time we spent. If strategies that involved building a node tree and then looking to see if the next location, given by measure on the treasure before harvest, is given then moving towards that using the tree was employed it would be many times faster.

Even with this basic, and junk, go at it we can directly see a gain over time of doing it. We can also see though that it will take a monstrous amount of time to get through all 300 possible growths if we wanted to attempt it.

Following the pattern set currently it would be about ~3 secs per attempt which is 299 * 3 or 897 secs to complete a whole maximized set.
Phase 2 - Reuse The Previous Maze
As we have shown redoing the maze even with a trash strategy is worth it. So lets continue using the previous code we have created with a few tweaks to get it going.

One we need to start passing the value forward to the next call. We will do that in the mazes function that contains all the logic for doing the mazes, shown in code section for phase 1.

def mazes(): # See full code in Code Section ... # Solve Maze change int value to set how many retries wanted for i in range(300): while get_entity_type() == Entities.Treasure: if num_items(Items.Fertilizer) > 0 or trade( Items.Fertilizer, to_buy_at_once ): use_item(Items.Fertilizer) maze_val = move_maze(maze_val) # quick_print("Next Treasure", measure()) harvest() ...

Everything is this code block is doing the things that I mentioned before. It is taking the previous maze and passing it forward to the next maze solve. Unsolved maze with many missing wallsThis comes with some inherent problems as you go forward with the maze solves it get's "easier" to get to the treasure as there are less walls as seen here.

Though we will not worry about it in this phase as we are where we need to be with forward propagated maze data. The next challenge is to move to a move efficient data-structure and strategy to solve the maze in a faster time.
Phase 2 - Code
Phase 2 is where we can start reusing the previous map state and use that to solve the maze faster as we are not longer in the discovery phase.

def mazes(): maze_val = [] ws = get_world_size() sx, sy = randint(0, ws), randint(0, ws) if get_entity_type() != Entities.Hedge: clear() move_self(sx, sy) # Plant Maze if not plant_maze(get_entity_type, get_ground_type): print("Failed") # Solve Maze for i in range(35): while get_entity_type() == Entities.Treasure: if num_items(Items.Fertilizer) > 0 or trade( Items.Fertilizer, to_buy_at_once ): use_item(Items.Fertilizer) maze_val = move_maze(maze_val) # quick_print("Next Treasure", measure()) harvest() if num_items(Items.Water_Tank) < 1000: water_maintenance(1) return maze_val
Helper Functions - Code
Here are all the helper functions mentioned as code, there will not be any help provided here for these functions. This is just a reference to these functions that will be used throughout this guide.

def coord_int(x, y, world_size): return x + y * world_size def int_coord(pos, world_size): return (pos % world_size, pos // world_size) def randint(left, right): m = random() * (right - left) return (m + left) // 1 def normilize(x, minimum, maximum): rval = (x - minimum + 0.001) / (maximum - minimum - 0.001) return rval * maximum // 1 def move_self(tx, ty): ws = get_world_size() dx, dy = get_pos_X() - tx, get_pos_y() - ty # Will always eval to 1 or -1 so None will not be selected ns = (None, South, North) # Will always eval to 1 or -1 so None will not be selected we = (None, West, East) def inner_move(delta, move_dir): if abs(delta) > ws // 2: delta -= (delta / abs(delta)) * ws for i in range(0, delta, delta / abs(delta)): move(move_dir[delta / abs(delta)]) inner_move(dx, we) inner_move(dy, ns) return get_pos_x(), get_pos_y() def norm_maze(maze_vals): max_val = 0 min_val = 1000000 for i_ in maze_vals: for j_ in i_: cmax = max(j_) cmin = min(j_) if cmax > max_val: max_val = cmax if cmin < min_val: min_val = cmin for i_ in range(len(maze_vals)): for j_ in range(len(maze_vals[i_])): for k in range(len(maze_vals[i_][j_])): maze_vals[i_][j_][k] = normilize(maze_vals[i_][j_][k], min_val, max_val) def buy_water(count): return trade(Items.Empty_Tank, count) def use_water(count): if count == 0: return False if num_items(Items.Water_Tank) > count: for i in range(count): if not use_item(Items.Water_Tank): return False return True def maintain_water_level(perc): if get_water() < perc: uses = (perc - get_water()) // 0.25 return use_water(uses) return False # Given the wanted water per sec, calculates the required amount of water tanks def water_maintenance_needed(req_water_per_sec): return req_water_per_sec * (200) def water_maintenance(water_per_sec): needed = water_maintenance_needed(water_per_sec) - num_items(Items.Empty_Tank) if needed > 0: return buy_water(needed) return False
Phase 3 - Moving To Better Data-Structures And Strategies
It will be left to the viewer for these following suggestions.

One thing is to reduce the size complexity of the data-structure used to store the values for the discover map. This can be done multiple ways such as using (key: value) with a dict as this would make the normalization of the data faster.

Another thing would be to just have a shifting maximum value to use as the limit and then change the amount you add per loop in proportion to that so that you will always be able to check for new paths that have opened up. Or other hacks that basically negate the need to normalize the data.

Pathing
A big change that can save some time is implementing a better and faster solver so that you can minimize the time between each growth of the treasure.

One that would not be too complicated to implement is the A* but you still have to discover the map or do in place discovery. There are many ways to implement something like but I will leave it up to the viewer to do some research on this to implement their own solution. As A* is an already solved map solver just google it for python and bang your head against the keyboard till it works.


Update 6/19/24

I implemented a breadth-first search (BFS) and was able to get an average of ~1.7 seconds per set until it hit the maximum of 300 for max gold generation. So even without going full A* or Dijkstra the times can still get decent.

The below code will not have any helper functions or what not, it is just for reference as I do not want to ruin any gains someone could get from figuring how to implement it out themselves. The code bit here is like a procedure code block as most of it is missing. The logic used for this particular implementation of a BFS can be found on the wiki or many other locations but gearing it up for your own use is a fun challenge if you are not used to doing so.

Feel free to ask any questions.

def maze_solve_bfs(target, graph=None): visited = {} queue = [] if target not in graph: return False node = (get_pos_x(), get_pos_y()) visited[node] = None queue.append(node) while queue: cur_node = queue.pop(0) if cur_node == target: path = [] while cur_node: path.append(cur_node) cur_node = visited[cur_node] return path[::-1] if cur_node not in graph: return [] for adj_node in graph[cur_node]: if adj_node not in visited: visited[adj_node] = cur_node queue.append(adj_node) return []
Phase 4 - Fixing Errors
So an easy fix for the code I am using was to simply ensure that I was not building the graph each iteration. Do this saved a heap of time as it was not needed to build the graph each time.

After changing the graph building the code is now able to do all 300 iterations in a amazingly fast 192.19. The new function to do so looks like

def get_gold(runs=300): plant_maze(get_pos_x(), get_pos_y()) path = [] op_count_start = get_op_count() maze, dest = build_maze_sp() graph = build_graph(maze) for i in range(runs): if i > 0 and i % 150 == 0: maze, dest = build_maze_sp() graph = build_graph(maze) path = maze_solve_bfs(dest, graph) if not walk_bfs(path): quick_print("Failed") return dest = measure() if dest == None: return if not fertilize_treasure(): quick_print("Run Failed") return False quick_print(i / runs * 100, "% Complete") end_op = get_op_count() quick_print("Op cost: ", op_count_start - end_op) op_count_start = end_op while can_harvest(): harvest()
Video
Here is a video of the maze being solved.

https://www.youtube.com/watch?v=zc-rftetn_8

Note:
Was browsing the discussion and the following was there

https://gtm.steamproxy.vip/app/2060160/discussions/0/4521136185566148104/

This person's solution is really clever, check it out!
4 Comments
YourImagination 6 Oct, 2024 @ 6:32am 
you call "to_buy_at_once" 5 times, but never define it in the code shown
owenz  [author] 17 Jun, 2024 @ 2:19pm 
That's my bad on the move_oe should be move_self. Thanks for that. Wrote the guild using 2 different code bases and copy pasta'd from the wrong one.
casperorillian 17 Jun, 2024 @ 7:27am 
I think theres a typo, theres "move_oe" thats never declared anywhere.
Thing_II 16 Jun, 2024 @ 9:51pm 
You mention A*, but I find Dijkstra's algo to be a bit easier for beginners to understand and is probably sufficient. For beginners, A* is an optimization of Dijkstra's for some systems - so you can always modify your Dijkstra's implementation to become A* after the fact (but I found Dijkstra's to be sufficient, honestly).