Skip to content

Instantly share code, notes, and snippets.

@aangairbender
Last active March 14, 2024 06:01
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save aangairbender/52f82f62413816f85007ff8b9cd1867b to your computer and use it in GitHub Desktop.
Save aangairbender/52f82f62413816f85007ff8b9cd1867b to your computer and use it in GitHub Desktop.
CG Fall Challenge 2023 PM of aangairbender

aangairbender, Legend 11th!

Overview

Very important for me was having good toolkit for this contest:

  • code for state serialization (every turn I print base64 encoded state of the bot to stderr, so I can run my bot locally from any replay from any frame and debug the bot's behavior)
  • state visualizer (to debug fish tracking mostly)

Bot itself consists of 3 components:

  • fish tracking
  • planning
  • plan execution

Fish tracking

For each fish I maintain an axis aligned rectangle of possible positions.

For the first turn it would look something like this (for this match):

Each turn:

  1. I enlarge each rectangle by 200 (to simulate fish movement), while limiting rectangle to the habitat area.
  2. I cut rectangles based on the input radar blips
  3. if fish appeared in scans of a drone, I intersect that fish's rect with AABB (axis-aligned bounding box) of that drone scan circle.
  4. if fish did not appear in scans of a drone, I cut out rectangular intersection of that drone scan circle from fish's rect. E.g. here I would cut out green part:

cut

  1. If no drone has reached fish.y - 1400 coordinate, then I dont let fish's rect exceed middle of the map (because fish would collide with symmetrical fish and bounce back).

For example same match frame 14. My bot's predictions:

It's important to simulate fish movements after fish goes out of your vision range. On the screen above you can see all fish of type 0 are not visible, but are accurately tracked.

I assume fish position is center of the rectangle for undiscovered fishes.

Planning

There are 26 tasks:

  • 12 scan fish tasks
  • 12 scare away fish tasks (so that opponent cant scan that fish)
  • 2 report tasks (1 per drone)

For each pair of drone and task I assign some value based on the current state and then I use assignment algorithm (munkres) to come up with the best assignments. Then I mark used tasks with some bad values and repeat assignment for 2 more times. This is needed for cases when drone can scare away a fish in 1 turn, but it should do it in a way to be closer to completing his next task.

Factors I include in the scan task value:

  • score I would get from target fish
  • how close opponent is to scaring that fish
  • whether the fish is on the same side of the map (left or right) as the drone

Factors I include in the scare task value:

  • fish type (higher = better)
  • how close I am to scaring that fish

Report tasks value is always zero.

As a result for each of my drones I have non empty list of tasks.

Important point is that for each drone I detect if that drone should report based on whether that drone scanned at least one type 2 fish or not.

If my drone should report, I check opponent same side drone position and decide max_y for that drone.

// d - my drone, w - opponent drone
int calc_max_y(const ISDrone& d, const ISDrone& w) {
	if (turns_to_report(d) < turns_to_report(w)) {
		return 500 + (turns_to_report(w) - 1) * 600;
	}
	else if (turns_to_report(d) == turns_to_report(w)) {
		return 500 + turns_to_report(w) * 600;
	}
	else {
		return 500 + (turns_to_report(d) - 1) * 600;
	}
}

I also check If my drone is below the opponent then I set max_y in a way, that I can scan/scare the fish below opponent while not letting opponent scan that fish. This usually means being around 1400 above the fish (if opponent is sticking to my drone). This gave me a huge score boost and pushed me from top10 to top3 at some point.

When I do simulation later, for each turn in the future I subtract 600 from the max_y (assuming opponent is going up).

For lights, I turn on light if my drone is in range of 2000 of any unscanned fish this turn or if it would be in range of 2000 of type 2 fish in the next turn.

Plan execution

I have simple HC (Hill Climbing). So basically I maintain 1 solution (solution being next 3 moves for each of my drone), try to mutate it, see if eval is better than the current. If it is then the new solution becomes the current one.

My mutations:

  • change the movement direction to one of the 16 predefined ones
  • change the movement direction to the hinted position (for each task I store hint where the target is, e.g. for the scan fish task the hint is that fish's position)
  • divide movement vector's length by 2
  • multiply movement vector's length by 2

For eval I just use eval function for each type of the task (all of them return value from 0 to 1):

  • eval_scan - just returns how close the drone is to the fish
  • eval_scare - based on how many turns it would be needed to scare away fish. This consists of turns to reach the fish side (f.x +- 800, f.y) and turns for fish to leave the map
  • eval_report - based on how many turns needed to report

If the drone exceeds max_y or is in emergency mode - there is a huge penalty.

For each drone I sum his tasks' eval results (with each consecutive task result multiplied by 0.1).

And the eval of the HC solution is sum of the drones' evals (I sum eval results for each depth with some factor).

@bruno-olivia
Copy link

Nice PM, congratulations on your achievement

@DiMasta
Copy link

DiMasta commented Jan 9, 2024

Very nice article indeed, thank you and congratulations.
For the visual debug tool you've implemented, how did you scale down the 10K x 10K image to something manageable? Or you just worked in 1K x 1K and divide each coordinate by 1000?

@aangairbender
Copy link
Author

aangairbender commented Jan 9, 2024

@DiMasta I just scaled down all the locations and sizes (basically divided everything by 1000 as you mentioned)

@Neralem
Copy link

Neralem commented Jan 9, 2024

Thanks a lot for sharing this. Well played!

@DiMasta
Copy link

DiMasta commented Jan 12, 2024

@aangairbender Hi, I have some questions again:

  1. Do I get it right that you choose 3 tasks per drone using the Munkres algorithm on your planning phase? Which will be best for your drone fulfil in the next 3 turns or to be as close as possible to fulfilling them?
  2. In your execution phase you simulate 3 turns in the future for each drone and evaluate the simulated state based on the chosen tasks, summing up the evaluations for the 3 tasks chosen in the planning phase? And then your HC tries to optimize this evaluation?
  3. If I'm right for 1 and 2 how do you choose the initial moves for the drone when starting the simulation, do you choose 3 random move or something more specific?

@aangairbender
Copy link
Author

@DiMasta

  1. The main intention is to choose 1 next task for each drone. But I have to choose at least 1 more just in case the drone can complete his first task in 1 turn in multiple ways, I need to ensure the drone is closer to completing his next task. (This usually happens when the drone can scare the fish off the map in 1 turn, completing his first task). During the planning phase I am not considering the next N turns, the plan is just for the future, no matter how long it would take to complete the plan.
  2. For my execution phase, I represent a solution as a struct with 6 moves (3 future moves for each of my drones). I simulate the solution and also evaluate the state each turn after applying 1st set of moves, 2nd and 3rd with different weights. The evaluation just shows how close each drone is to completing his task (float from 0 to 1). I use HC to randomly modify some move(s) in my current solution and see if evaluation result improves.
  3. initial moves are random (which can be improved I think)

@DiMasta
Copy link

DiMasta commented Mar 3, 2024

Hi @aangairbender It's me again, sorry for bothering. I didn't have the time to finish the task previously but I'm again determined to reach the legendary league and I want to use your approach, because I like it very much :)

So far I think I've managed to implement a proper 2D visual debugger for the game. I'll attach screenshots at the end of the post.

I've also implemented Munkres' Assignment Algorithm, using this article: https://users.cs.duke.edu/~brd/Teaching/Bio/asmb/current/Handouts/munkres.html
And it seems to work, as expected, for the given example.

Now I have some problems implementing the Planning phase:

  1. When you say that there are 26 tasks, you mean that this is the maximum number of tasks for the drones, right? For example in the beginning of the game when all fish are invisible you do not take into account the scare tasks in the assignment algorithm?
  2. When you say "Important point is that for each drone I detect if that drone should report based on whether that drone scanned at least one type 2 fish or not." Do you mean that you're including report tasks in the assignment algorithm only if a drone has scanned type 2 fish?
  3. For the scan task costs I first use the FishType + 1, if the enemy hasn't scan this fish already I multiply it by 2. Then I subtract the cost from 10. Afterwards I get the closest enemy drone to the fish and divide the cost by MAP_WIDTH and then by closest enemy distance. If the drone and the scan target are not in the same sides of the map I divide the cost by 2. The final formula is something like this:
float droneScanTaskCost = (10 - (enemyScannedFish ? (fishType + 1)  : (fishType + 1) * 2)) / MAP_WIDTH / closestEnemyDroneToFishDist;
droneScanTaskCost = fishSameSideAsDrone ? droneScanTaskCost  / 2 : droneScanTaskCost ;

Does that make sense? Or I'm completely in the wrong direction?
4. For the scare fish task cost I check the distance from the drone to +-800 fish.x, based on the side of the fish. Then I use the formula:
float droneScareTaskCost = (5 - fishType) * (MAP_WIDTH / scareDroneDist)
Does it sound reasonable to you or I'm missing something again?
5. The idea of the max Y is to use it only when you evaluate the simulations of future turns right?

Screenshots from my 2D visual debugger, for the replay https://www.codingame.com/replay/772339349
A
B
C
D
E

@aangairbender
Copy link
Author

@DiMasta

Wow, you are doing great! Let me answer your questions

  1. in the beginning of the game all fish are invisible, but I know the predicted rectangle for each fish. So I just use the center of that rectangle as the fish's position. So still all the tasks are considered. I only skip task (set cost to infinity) if doing that task does not make sense (e.g for scan task I would set infinite cost if fish was scared away already or opponent already reported that fish, etc)
  2. "whether I should report" is just a boolean variable that helps to choose max_y parameter correctly. By itself it is not used in the planning phase. But max_y is used when evaluating tasks later in the simulation.
  3. Your formula makes sense, but it's hard to tell how good it is without testing. My formula is just a linear combination (weighted sum) of factors I mentioned in the PM. Also, the weights are chosen in a way similar to this: f1*1000 + f2*100 + f3*10. It works like a comparison of the tuple of factors, but sometimes I make my factor go out of 0..1 range to make a huge impact (but very rarely, more like a hack).
  4. Looks okay, but I would do 1.0 - (scareDroneDist / MAP_WIDTH) instead of MAP_WIDTH / scareDroneDist to get "closer -> bigger" function
  5. Yes, max_y is used like this: if my drone is below max_y at any simulation step I apply huge penalty.

Wish you the legend!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment