Code a la Mode - Feedback & Strategies

13th Legend. I went out of my way to do something complicated and sub-optimal this time, so the result is a nice surprise!
Regarding the contest, it’s really cool that it allows heuristics to be competitive, so kudos for that. On the other hand, scoring was ridiculously random, my bot could end anywhere between 10th and 40th… I wonder if there might be ways to improve this aspect.
For my solution:

  • First I try to figure out which order my opponent is working on. If I can, I ignore this order.
  • I didn’t want to write a score function, that seemed a bit hard to get right… So I went for a depth-2 MCTS. For it to be able to converge, I had to be extremely severe in the choice of actions to consider, so I guess a big heuristic would have done just as well… But where’s the fun in that?
  • Each simulation would end either with a complete and delivered order, or when no more actions where possible. The score was simply the order’s award.
  • To simulate the other chef, which made a big impact on my rank, I used very simple heuristics like “If he’s got a raw tart, he’s going for the oven!”
  • I would let my sim run on all remaining orders.

In retrospect I would probably have been better off taking the time to write a score function and limit the search to a few turns, but using more action possibilities, and simulating the other chef as well. But god do I hate to write score functions.

I have 179 ifs in my code. Could have been worse I guess.
Thanks again to csj and Matteh!

6 Likes

Great contest. Thanks a lot to the creators. I ended 8th and put my post mortem here:

Post Mortem

7 Likes

Many thanks to csj and Matteh for an excellent competition!

I ended up coming second. Many congratulations to morozec, a clear winner in a messy contest.

My strategy
In summary: a nest of ifs. Perhaps the core choice that I made is not to head for particular dishes. Instead, I make ingredients that are needed, and then assemble items that have all ingredients ready as they come up.

I have two main phases: target selection, which assumes my partner will stay still, and route planning, which thinks about partner movement.

Target selection

This returns our next usable destination and our probable destination after that, and is the meat of the algorithm.

We start by prioritising croissants, tarts and strawberries. We only care about items needed in the current three recipes that we don’t already have enough of. Within those, we prioritise those that don’t exist, and that our partner isn’t making. After that, we prioritise those we need the most of.

We then go down the following list, taking the first action that applies:

  • If we put things in the oven, or if our partner did and clearly isn’t going to make it, we stand next to the oven or remove things. During that, we can pop away for a turn or two to pick up a plate or other useful item, but always come back when the item is done.
  • If we are holding chopped dough/uncooked strawberries, make a tart/chopped strawberries.
  • If we are holding a raw tart, and the oven is nearly free, go there.
  • If we are holding dough, and the oven is nearly free, and croissants are higher priority than tarts, go there. Otherwise, chop it towards a tart.
  • If we are holding something on the way to a dish we can complete (that our partner isn’t clearly building), complete it and deliver.
  • If there’s a raw tart or chopped dough on a table, and we’re closer to it than our partner, and the oven will be free in time, go pick it up.
  • Complete a dish and deliver it, if we are closer to the most important item than our partner.
  • Go down the priority list, making an item if we can. We only make an item if we are closer to the source (dough/strawberries) than our parther. For baking, we additionally have to expect the oven to be free when we need it.
  • Complete any dish and deliver it.
  • Partially assemble any dish.
  • Drop what we are holding.
  • Move directly away from our partner.

Route planning

This takes a destination and a next destination from the target selection

  • Run a simple algorithm to work out our partner’s eventual destinations. This looks at what they are carrying, where they might want to take it, and their last move, to work out their plans as far as they can be predicted. So if they are holding dough, and their last move was away from the oven but towards the chopping board, they are going chopping->blueberries->oven.
  • Run a search, where our partner moves greedily towards their destination (as with a USE command), and we move as far as we can in one direction, next to our partner, next to our destination, or into the special passing places (IIRC, 4,4 and 6,2), without backtracking. When either bot gets to their final predicted destination, they stop moving.
  • Optimise that search for [our turns] + [partner turns] + c_1 * [our turns to next destination] + c_2 * [how many pieces of equipment we make it harder for our partner to access]

Other bits and bobs

  • If the game state (items, locs, and oven timer) is identical to two turns ago, we may have looped. We run away from our partner.
  • I got occasional timeouts in the search, so it breaks and just uses a MOVE command if time runs out.

Unimplemented ideas (mostly half-baked)

  • Incorporate planning and target selection. I sometimes selected targets that looked good, but turned out to be a massive pain for my partner.
  • More use of distance in target selection - my bot was fairly agnostic to how far away things were if it was closer than its partner.
  • Clever oven management - leaving things in longer, taking partner items, leaving items for partner.
  • Making any use at all of the value of dishes.
  • Changing my strategy as time runs out.

What I prioritised was definitely guided as much by what fitted into my (messy) code as what was important or right.

Thoughts on the game and my approach to it

  • It was super interesting - a cooperative game meant there was heavy emphasis on strategies that play nicely with different partner. It felt very different from any bot I’ve programmed.
  • The cooperative nature also made it messy to evaluate ideas, and also to evaluate bots. I ended up watching a lot of replays, finding things I did wrong, and changing things until they were fixed and I did better with the same settings.
  • This was my first contest on CodinGame - I signed up to do it. This was also the first coding competition I’ve entered that ran for less than around 3 months! Personally, I’m slightly frustrated at the end point - I feel like I still had a few good ideas in me.
  • I coded messily in Python, which is the fastest way for me to write code up to a certain point. However, I got way past that point - I’d have gone faster overall by keeping things clean and working in a strongly typed language - and also had some very mild timing issues near the end.
  • My final bot came in at 1192 lines, and 356 ifs. That’s 0.299 per line.

Thanks again to the creators for a fun contest!

20 Likes

My Strategy: Finished #107 in Legend

My core idea is behind an assumption that me and partner will always try to deliver dishes of different customers I predict the customer my partner is delivering dishes to and switch my customer accordingly. I keep track of my partner’s past customer just so if he drops an item on the table, I would still know his target.

Positives: First of it’s kind. I love playing team games. I always enjoy coding for grid games. (love to code anything without collision lolz)

Negatives: Last battles was always crashing. Heavy memory crashed chrome with error message “Aw snap” very frequently. We need fast servers at least during contest durations. I would like to see 100 games gets judged in the leaderboard in 5 minutes.

Suggestions :
Would like to see a 2v2 with Communications
We can even crowdsource the server cost for those 10 days for speed up.

Difficulty : All the bosses were set at the right difficulty keeping the contest duration in mind. I’d expect the bosses to be buffed in multiplayer (something that’s not just if else. Because legend should be given only to real legends - not me :slight_smile: )

2 Likes

I think the idea not too focus on orders but on the needed dishes is exactly the right approach in a co-op oriented contest. Great thinking!

2 Likes

31st (about 15th before I broke my bot, being a tester)

I have a Monte Carlo search, which is quite similar to the one I wrote for Code4Life.
I generate 5 random actions. I hardcoded testing 500 plans (my sim is fast enough to get some more). An action is a USE here, so it can make me move accross the whole map. An action has to be valid, so the options to choose from depend on the item, that my chef currently has (if any).
My chef is not a teamplayer at all. I mostly ignore my partner,

After each of my 5 random actions I score 2 things:

  • preparing the more complex foods (chopped strawberries, croissant, tart)

    double timeWeight = 1 / Math.Log (2 + TravelTime);
    double factor = Math.Sqrt (Math.Min (1, itemCount / itemNeeded)) - 
                    Math.Sqrt (Math.Min (1, previousItemCount / itemNeeded));
    Score += factor * itemCost * timeWeight;
    
  • serving a dish to a customer

    Score += 3 * SearchDepth * customer.Award * timeWeight;
    

This gives less score for finishing a task later (in terms of the real game time, not search depth for my actions - so it’s fine to pick up a dish before a save the tart from burning.
I have a higher motivation to get my 1st croissant, than a 3rd tart.
Taking extra time gives a slightly smaller score.

When I found my best plan, I replace USE by MOVE, if I’m not next to my target already. That’s the only time, that I check the postion of my partner. I try to get close to the next cell in my list in this stage, so that I might save a turn later.

When I place an item on a table, I prefer central tables with many neighbors over corners. That makes it easier to pick up an item again, it’s easier to reach.
image

I ended up with a little less than 1000 lines of code. This is mostly the game logic itself, a little path finding. The actual bot is pretty simple.

14 Likes

We had a performance issue on the server that selects the participants of the battle and then updates the scores at the end. As a consequence, at several occasions, the “execution” servers were idle, waiting for tasks to process. We’re currently working on a technical task that aims at splitting services on dedicated servers, that should help.

However, seeing 100 games in 5 minutes might not be possible for a simple reason: except for the first 10 games, all the next games are computed sequentially because we need your current score to select your next opponents. If each game takes ~10s, you’ll need at least 15 minutes.

We’re of course open to suggestions to improve the computation of the ranking.

5 Likes

I enjoyed the game a lot and wished I had more time to play. Finished 8th in Gold, tried the Legend until the end.

Like @teccles, I tried not to focus on one specific order but rather chose to prepare the closest needed available dessert and drop it on the right plate. My boss would never drop a finished dessert directly on a table (but to empty the oven). Unfortunately, quite a few bots in gold would not try to complete the unfinished orders on tables and instead would stubbornly try to finish one order on their own… leading to very poor performance. And my bot didn’t adapt well to these bots. This + a few bugs and unoptimized decision making regarding basic desserts vs advanced desserts explain why I didn’t get to Legend.

But I had fun! The semi cooperative thing was indeed very interesting despite the frustration of bad cooperation once in a while.

We dropped a lot of things during the creation (I think for the better):

  • there was a bin to empty a plate
  • there was BURNED_FOOD to be removed from the oven until it can be used again (if something burned in it)
  • at some point, we could split a tart into 4 parts at the chopping board
  • there were bananas and waffles
  • you could move in diagonal for less steps than 1 horizontal move + 1 vertical move

Congrats again to the creators csj and Matteh, the graphic designer Bruno (part of the CG team), the testers, the streamers. I wish we didn’t have that annoying issue with last battles (I’ll push to fix this sdk memory issue before next contest). Apart from that, I think it was a really nice event.

See you on the 17th of May for A Code of Ice and Fire. I chose fire :fire: #TeamFire

Oh, I forgot, I tried to keep my code clean from the beginning because

8 Likes

First thanks for the game, loving learning how to code with a game.

Can you increase the batch size from one game at a time during the sequential scoring phase? I certainly agree with many others that it’s hard to test your bot if it takes a long time to find out how well it performs.
If the first 10 games take 10 seconds, then score and play 10 games against similar bots. That’s under 2 mins. Fine tune after that point if needed.

The first 10 games are used to estimate your initial position.

After that, the TrueSkill system require sequential games because each games need the result of the previous game. You can’t do any batch or parallel games.

1 Like

Can we reduce that dependency? For. eg. use the score of first 10 games to determine the next 10 games’ opponents. Then use the score at that time to determine the next 10 games’ opponents and so on… This way more parallelism can be achieved and we get a 10x faster leaderboard.

For the final judging you can have the current judging mechanism. But during contest time I feel we have to reduce the dependency and increase parallelism. Instead of having less matches in a tight dependent ladder I would prefer a low dependency ladder with more matches

I don’t know much about ranking but I guess batching games would increase unreliability of the result thus being an incentive to more “spam push”, no ?

1 Like

I’d have to read up TrueSkill more, but an initial read does not seem to indicate a sequential games requirement. Maybe games should be scored sequentially, not even sure about that.

Seems to me that a tweak could be to have the matchmaker output a batch of “interesting” games, and then they could be played in parallel. Store the results to then be processed sequentially. Sure there is risk that a game that the matchmaker thought would be interested turns out to have been “uninteresting”, but the ranking system should still be able to update the rankings based on it’s result.

What I’m saying, and think I hear from many others is that it takes too long determine your bots results. Maybe the estimate of initial position could be improved?

Maybe we need a contest to see who can create the fastest ranking system that succeeds in ranking bots.

3 Likes

Many thanks to Bruno for the awesome visuals!

1 Like

We could have more leagues, for example one new league every day :slight_smile:
With fewer people in each league we may need fewer matches for one submission ?

1 Like

tutubalin - 14th (Python3)

It’s weird how my opinion about contest changed over time.
When contest just had started I didn’t find it fun enough to even bother about. So first 2-3 days I only discussed ideas with fellows and didn’t even try to code anything. But when I started coding everything changed.
When contest was over I felt like something really important just have ended. During the contest I even saw it in a dream :slight_smile:

As many other people already said, movement along the leaderboard was very surprising. Sometimes after implementing a major improvement my bot dropped 100 places down in a league. Then after changing 1 line it suddenly got promoted to the next league. But it was very emotional, especially when after frustration you get an unexpected success. I think it’s because of cooperative but still concurrent style of the game.

For example, I struggled several hours to get out of bronze. Then I tried to make chopping strawberry more important than baking stuff. BOOM! I am silver.

Getting out of silver was also surprisingly easy. I added just one condition: do not save bakery if chef is already has something in his hands. BOOM! I am in the gold instantly.

Another example: I decided that if both chefs are too into baking, they will block each other. So I decided to assemble orders while delegating baking to a partner. I created a super intelligent system that calculated which items in the kitchen are stackable, what orders can be partially satisfied and how. After submitting I dropped 100 places lower. So I just removed it and switched back to older version, made some minor changes. BOOM! I am legend.

So submitting new code was always a great rist. At some point I stopped coding in web editor and setup git environment - because I needed to track what exactly I change, and how every change affects the position of the bot. My effectiveness greatly increased after that.

Storming top20 was also surprising. In the morning of the last day I was somewhere in #30-40. I made an improvement: prefer more expensive orders. I submitted, and… I am #120

Ok, that was because of bug. I fixed it while preserving improvement, and I am #50-55. Resubmit - still #50-55.
I tried old version - #35-45. Expensive orders are not so good!

So I had no choice other that revering this “improvement” and adding a different one. And suddenly - I am in top10. At this point I decided do not touch it.

A few words about my bot. It is built on “IFs”, or you can call it decision tree probably. The core of the bot contains only 18 ifs.
Most of conditions are simple and straightforward, for example: if bot holds strawberry, he wants to use chop board. If bot holds raw tart, he wants to use oven. Some conditions are more sophisticated. But in total bot code is pretty short and straightforward. I don’t even use MOVE command.

Some ideas that proved to be good:

  1. If you hold ready order - serve it, don’t try to make it more expensive.
  2. Save bakery unless it requires you to go too far.
  3. Do something while baking is in process, do not just stand near oven.
  4. Always go back for you own tart/croissant!!! Do not delegate saving to partner.
  5. If you can make an order - make it!
  6. Put stuff on the tables in the middle - they are easily accessible from both lanes.
  7. Strawberry first. Bake later.
  8. Bake! Bake! Bake! Oven is a bottleneck, it should be working 24/7.
  9. Don’t know what to do - go grab dough.

That’s all guys.
Thanks for great contest! I am waiting for multi to continue my development.

10 Likes

If you haven’t done it yet, please fill the survey: https://codingame.typeform.com/to/DriT4I

2 Likes

First, sorry for the late PM, I stayed up coding and packing the whole last night of the contest, before flying to California the next day :smiley:

The first night I wrote a simple heuristic in Java to get to Bronze. It actually got around #10 the first night, which felt nice. However I knew this was very preliminary and symbolic, and I was sure I would go for a search algorithm.

I went fist for a Monte Carlo simulation, for simplicity while implementing the engine, planning to switch to MCTS later. The alternative heuristics (“if forest”) approach was especially easy and strong in this contest, so it was not easy to get a strong search bot early on, as many people who tried know, needing in particular to spend time crafting a good eval function. But I had something decent after a few days, and I was relatively confident that in the end it would be easier to keep improving, while the if forests would become increasingly hard to maintain. There was still a doubt about which approach would be best though, and I think this is something great about this contest.

The next step was to switch to MCTS. I got something working, but it was not so clearly better, as in some cases it would make worse decisions. I realized that the strength of MCTS, going deeper in promising branches, made reasoning about it harder, because it would find a deep good solution (deliver a finished plate) and not find a shorter action plan (like making a very much needed croissant) that would be better in the long term. It might be possible to fix this by carefully tuning the exploration parameter and the evaluation function, but this seems just too complicated. So I decided to change the expansion criteria to simply the number of visits, to always explore in breadth instead of depth. Essentially this became a BFS search in the state space, implemented in a weird way :). This worked better, and importantly was easier to debug, because I knew all shorter solutions were considered before longer ones.

Regarding the partner, for a long time I just ignored them (though by nature I would happily prepare ingredients that they could use, as I did not focus on a single customer). For a long time I even did not consider that they can block my path, being optimistic that they would just move away in time. That seemed to perform better initially, but eventually I switched to considering that they are static for a few turns, then disappear (since their position is too uncertain after some time). In the last week-end, I added a hard-coded behaviour that they take things out of the oven if they are next to it and they put the ingredient in, which gave a nice improvement.

One small feature I like was to give a small positive value to spreading dough and strawberries at useful locations. That meant for instance than while waiting for my tart to cook here, I picked this dough and place it where it can later be used in the oven or the chopping board:
board

All in all, I think that the search approach indeed allowed me to keep trying new ideas until the end with manageable complexity, and that it payed off, even though it was obviously harder in the beginning. My rank graph from @Adrien’s awesome tool illustrates that:

rank

When legend opened, I was very close but did not make it, so I had to spend Friday evening making sure I promote, which corresponds to the zigzag on March 15th :D. My final rank was #5, which is my best result so far!

Thanks a lot to @csj and @Matteh for a fun and well designed coop game, to the testers, and to all the chat regulars for an awesome community during and between the contests! :slight_smile:

15 Likes

A great thanks to @csj and @Matteh, the creators, and to all of the Codingame team !
What a contest guys ! First cooperative game of the platform, and not the last I hope. Excepted some balance problems in the ranking, I really enjoyed it.
But now it’s over, and I finished #106 in legend with C++.

Here’s a little overview of what I’ve done.

The data structure

After reading the statement and seen the amount of inputs, I started by coding the classes architecture destined to stock these ones. I wanted it clear, but also hyper simple to use, to be able to focus myself on the logic and the strategy of the game. Here’s the result :

  • Coordinates : This class simply represent… coordinates. A x, an y and a simple (Manhattan) method of distance calculation.
  • itemSet : A class destined to represent a set of items (dishes, ingredients and desserts).
  • Order : A class inheriting of itemSet, adding the award to cooking it.
  • Cell : A class representing a cell of the kitchen, walkable or not, and also inheriting of itemSet to represent the disponible elements on it. It also has a "type" attribute, to quickly identify it (I have simply keeped the ascii representation of the input).
  • Kitchen : The representation of the map, containing, of course, an array of Cell.
  • Player : Simply inheriting of Coordinates and itemSet.

The items sets representation

Since an Items set, i.e an Order, a Cell or a Player, can’t contain more than one element of each type, I decided to represent these items sets by bits fields, easier to use than strings. So I started by associate a power of two to each possible item via an enum :
enum
From there, I modified my itemSet class to contain a bits field, and by adding to it a constructor taking a string and using it to fill this bits field.

The toolbox

After that, I employed myself to code all the possibly potentially useful tools which I could imagine :

  • I added a series of methods to my itemSet class allowing to check if it contains an item, contains at least one item in a set, contains an entire set, and so on…
  • I have improved my Kitchen class, notably by modifying the constructor to allow it creating as much data as possible from the input (kitchen in a form of Cell array, in a form of graph, Coordinates of equipment…).
  • I have coded a BFS, and a series of functions based on it. First, several functions to find not walkable cells (closest item not in a dish, closest dish matching an order, closest "evacuation" i.e the dishwasher or the hatch,…), secondly a function to find the closest spot to access a not walkable cell. All of this taking the partner position in consideration.

About Remy

All of this done, it was time to code the decision function. Named Remy for the occasion, it has undergone a lot of modification through the leagues. But here’s in a nutshell the last version’s logic :

  • If Remy don’t have an order in progress, or if the order in progress has already been delivered, he chooses a new order.
  • To choose the order to prepare, Remy consider two things : The award of the order, and if a pending dish somewhere in the kitchen is matching. If a dish match, Remy just add 1000 (Totally arbitrarily) to the award. At the end he chooses the order with the best reward.
  • Then, Remy determine if some complex desserts (Croissant, tart, or chopped strawberries) must be prepared. If yes, he determine what to prepare first this way : If he’s closer to the closest strawberries than the closest dough, he start by the strawberries. If he’s closer to the dough, and the dough is closer to the oven than the chopping board, he starts by the croissant. Otherwise, he starts by the tart.
  • Once a preparation started, Remy finish it step by step. For example, if the tart is the first element to prepare, Remy will pick the dough, then will chop it, then will garnish it, then will cook it, and only after that he will select the next element to prepare.
  • If the oven is not empty, Remy will wait his turn, and eventually get out of the oven what is cooking, if the partner doesn’t (It’s the only case in what Remy take in consideration the partner baking.). During the cook, Remy just wait next to the oven.
  • If Remy is preparing a complex dessert, and the partner put on a table one dessert of this type, he stops his preparation and consider this element ready, except if he’s monitoring a baking, in this case he goes to the end.
  • At any step, excepted the baking, if the needed element has already been prepared and is available on a table, Remy will prefer picks it rather than prepare it.
  • Once all the complex preparations done, Remy composes the dish. First, he takes the closest element of the recipe, dish included. If he has an element in the hands which is not a dish, he goes take a dish. Then he takes the other elements of the recipe, one by one, by order of proximity.
  • When the order is ready, Remy deliver it. But if the partner delivers the same order before him, he put the dish on a table. Same thing during the preparation.
  • If there’s no clean dish, and no in progress dish which match with his current order, Remy take the closest dirty dish and go put it in the dishwasher or in the hatch, according to what is closest.
  • At any moment, if Remy is carrying something and needs his hands to do something, he’s able to put his load on a free table the time to perform this action.
    ratatouille
    That’s all !

Dropped idea

I wasted a lot of time with an idea which seemed good to me, but the results did not agree…
The problem were since the BFS take the partner position in consideration, Remy has some time to do the entire turn of the kitchen to access to an element, whereas perhaps the partner would have moved, next turn.
So I tried this : If the shortest way to access the element is blocked by the partner, Remy move next to him and wait two turns (The best value I tested.), if after that the partner has not moved, Remy take the other path.
By testing this in the IDE, I seen a real improvement of the Remy’s movements. But in the arena, the global results were poor…
Not seeing any bug, I rapidly tested some improves, like reconsidering the path if the partner move during Remy’s kitchen turn. But with no results…
So, I finally dropped the idea…

Ideas of improvements

  • I’m definitely convinced there’s a way to improve in the coordination of the Remy’s moves with the partner’s ones. Remains to find which…
  • There’s clearly better to do with the preparations order. Like doing the closest task whatever what Remy is doing. For example : Put a raw tart on a table the time to chop strawberries, if they are on the way to the oven.
  • Add a fix to avoid the infinite loops like : Remy is carrying chopped strawberries and puts them on a table to prepare another dessert. The partner is carrying a dish which is missing strawberries and see the Remy’s strawberries on the table. So he puts down his dish to come take these ones. But Remy see the partner’s dish and that it matches with his current order, without the strawberries. So, Remy take back the strawberries and go take the dish. But ! Not seeing the strawberries anymore, the partner takes back his dish. Not seeing the dish anymore, Remy puts down the strawberries, and so on…
  • Consider all the orders at the same time, and not only one.
  • Have a more collaborative comportment.

Thanks again everyone !

BlaiseEbuth

3 Likes

Legend 4th.

  • Algorithm

I used simulation algrithm Chokudai Search.
It was invented by Chokudai and has similar features to beam search.
And it has an advantage that it is easier to manage time than Beam Search. Details are described here by Chokudai. (Japanese, but I think sample code is useful for everyone.)

In this contest, I set the search depth to 7 (Player1 → Player2 → Player1 → Player2 → Player1 → Player2 → Player1).
About 10,000 states could be evaluated in one turn.

  • Evaluation Function

I set the weights of the two players equal. I paid attention not to give an evaluation value biased to either player.
I did following 3 steps in the evaluation function.

(1)sort customers

First, prioritize the three customers.
① Give priority to customers who have all the ordered foods on the tables or dishes or players hands.
② If ① is the same, give priority to high Award customers.
Considering serving foods in order of this priority.

(2)Assign players to customers

Checking the customers in the order they were sorted in (1).
If the customers who have all the ordered foods on the tables or dishes or players hands, it is evaluated which the player is likely to be able to complete the customer’s order first.
The player with higher evaluation is assign to the customer, and another player is sent to the next customer.

(3)Not assinged players will cook for not assinged customers

The player who is not assigned to any customers in (2) thinks to cook the foods of the remaining customers order.

After that, when comparing evaluations, it will compared in the following order

  1. Higher game score is better
  2. The more players are assigned to customers, and the dish is closer to the order, the better
  3. Cooking processes have more progress, the better

Thank you for the good contest, I really enjoyed it!

5 Likes