Update: This challenge has ended. Results of this week are on the end. Still, if you want to challenge yourself, feel free to face this task as well. In the end, the goal is to learn and practice.

Our mission is to teach, and we believe that the best way to do it is by sharing knowledge and challenging. This is why in this advent, we challenge every single one of you. At the beginning of each week, we will publish one challenge for you to solve in Kotlin. They can be solved in many different ways and using different Kotlin features.

On the end of each week, we will check all shared solutions and choose the one we like the most according to our code analysis. The author of this solution will be the winner (see “How?” section). We will announce his or her Twitter/Github username here, on mailing and Twitter. We will also show this solution and explain why do we like it the most. We might also include some honorable mentions.

Sounds good? You can start today because this is already week 1! But first, let’s describe some details.

Code analysis

When we judge different solution we will have a few things in mind:

Correctness — the program should work as described, not only pass unit tests.

Coding conventions and idiomatic usage of Kotlin — all attendants of our workshop know that we treat good practices in Kotlin very seriously. We even have a separate workshop dedicated to this subject. Do you use Kotlin idioms? Can you use Kotlin features to make your program clean?

Readability — Is it clear how does this program work?

Timing — We understand that one can copy solution of another and improve it. This is why timing is important for us as well.

Performance — The least important parameter, but highly inefficient solution loose in our eyes.

How?

If you want us to consider your solution, you need to share a link to code (can be GitHub snippet or a link to Kotlin REPL) on Twitter, with #AdventOfKotlin18 tag. Alternatively, you can send an email with your solution to contact@kt.academy email. We post each week challenge on Sunday, and we announce the winner on Sunday next week. We will only accept those snippets sent till 12 pm CET on Saturday.

Week 1 challenge: Mark shortest path on a map

You probably use algorithms for finding the shortest path on your daily basis without even knowing. For instance, you probably use some navigation like Google Maps that helps you find the best route to get somewhere on time.

I started thinking about them for the first time when as a young child I was playing the old game — Age of Empires — and I was wondering how it is possible that when you select unit and you click where do you want it to go, it always chooses the closest path.

This is what we are going to learn today, and the challenge for you is to implement it. On the end, you will do an algorithm which for a map like this one, where S is start, X is the end, and B is a wall:

....................

......X...B.........

..........B.........

........BBB....S....

....................

Finds and marks the optimal way:

.......****.........

......*...B*........

..........B.*.......

........BBB..***....

....................

Supposing that weight of straight step is 1, and of a diagonal step is 1.5.

How to do it?

Dijkstra’s algorithm

The simplest algorithm is Dijkstra’s algorithm. The idea is similar to the Breadth-first search — we develop paths to all directions until we get to the final point. We always develop the closest next point (closest from the start). This way first way to the new point is always optimal. Development of points is presented on the below animation.

This algorithm in simplification can be described as follows:

Set a list of paths possible to develop to all possible ways starting from the starting node. Find the shortest new path in the list of all possible paths. If this path is directing to the final node, finish the algorithm. This path is the optimal path we were looking for. From the list of paths possible to develop, remove all to the chosen node and add all paths starting from this node. If we have an optimal way to the final point, finish. Otherwise, go to step 2.

Detailed description on Wiki. Note that the algorithm described there is slightly different.

A* algorithm

The big problem with Dijkstra’s algorithm is that it searches to all directions instead of concentrating on the most promising ones. Of course, sometimes to get to point on the left, we first need to go right to around an obstacle. This situation is presented on the “Labitynth” test:

BB..B...B...BBBBB...

....B.X.BBB.B...B.B.

..BBB.B.B.B.B.B.B.B.

....B.BBB.B.B.BS..B.

BBB.B...B.B.BBBBBBB.

..B...B.............

Solution:

BB..B...B...BBBBB.*.

....B.*.BBB.B...B*B*

..BBB*B.B.B.B.B.B*B*

....B*BBB.B.B.B**.B*

BBB.B.**B.B.BBBBBBB*

..B...B.***********.

Though in most cases it would be better to try to go to the direction of our goal as soon as possible. The desired behavior is presented in the following animation, and we will achieve it using A* algorithm:

How can we achieve it? We need to introduce some naive metric — called heuristic — that will tell us which nodes should we develop first. A most popular one for maps is the distance on the straight line from this point to the end. It can be calculated using the following equilibrium:

So now when we decide which point do we want to develop next, we take into account sum of two values:

the shortest way from the beginning

distance in the straight line to the end

This way we prefer nodes in direction of our end. Is it always going to give us the correct result? Well, it is proven that A* works correctly if the heuristic h satisfies the condition h(x) ≤ d(x, y) + h(y) for every edge (x, y) of the graph, where d denotes the length of that edge. It is true for our heuristic, so it will work fine.

Challenge

Your task is to write an algorithm to find the best way on a map and to return this map with marked best way. The starting point is marked by S , end by X . You can only move on dots . , and you cannot go outside of the map. B is an obstacle. Suppose that weight of straight step is 1, and of diagonal step is 1.5. When you find the best way, you should mark it on the map by replacing all nodes that should be visited with * .

For example, your function for the following map:

...........

.......S...

...........

...........

...........

...........

..X........

Should return the following result:

...........

.......*...

......*....

.....*.....

....*......

...*.......

..*........

In case of obstacles, it should go around:

....................

......X...B.........

..........B.........

........BBB....S....

.................... .......****.........

......*...B*........

..........B.*.......

........BBB..***....

....................

But it should not go outside of the map:

......X...B.........

..........B.........

........BBB....S....

.................... ......*...B.........

.......*..B.........

.......*BBB*****....

........***.........

You can choose any algorithm you want, but performance will be taken into account. Though you don’t need to use any more advanced algorithm then A*. Your algorithm should always give the correct result, and it should not be random. If a way chosen by your algorithm is optimal but different then in a unit test, it is fine. You can adjust this unit test. Though if you decided to implement A*, the chosen way should always be the same as in unit tests.

Unit tests you can use to test are here. You can also implement a solution online on REPL (you can also use the new one)

Good luck :)

Results

I am absolutely amazed by the results of this challenge. It was just a week for this quite complicated task, and I received over 20 correct solutions! One attendee even published an article describing his solution:

Knowing how much work each person had to put into that, I decided to mention them all (on the end).

For now, since the purpose of this challenge was to learn, I would like to give some comments from my site:

I’ve noticed that the biggest trouble was with choosing correct abstraction levels for some concept. This is a complex problem I will explore deeply in my next book, but here I will just touch two extremes.

First of all, it is good to represent map differently than by a list of characters. Additional abstractions can help us a lot by holding additional pieces of information, for instance about the position.

How would you represent a point? It is clear to me that all you need is a simple class:

class Point(x: Int, y: Int)

Though some people are tempted to use Pair<Int, Int> instead. Then they need to name it, and they define typealias:

typealias Point = Pair<Int, Int>

They need to name coordinates as well, and they define extension properties:

val Point.x get() = first

val Point.y get() = second

Is it a good idea? I don’t think so, and I am sure that it is not idiomatic Kotlin way. Kotlin team resigned from tuples intentionally to push people to use classes instead, what is a better practice for a long term.

You can imagine that the solution with typealias can be easily broken by another similar type represented the same way. For instance direction:

typealias Direction = Pair<Int, Int>

val Direction.dx get() = first

val Direction.dy get() = second

If you need named tuple, it is called data class in Kotlin, and you define it this way:

data class Point(x: Int, y: Int)

It is safe and always clear. Classes are cheap in Kotlin, use them!

The other extreme is when we extract too many abstractions in our code. Most projects balanced it very well, but one had way too many abstractions in different files and packages. It reminded me good old FizzBuzz Enterprise Edition :D YAGNI! Keep it simple.

2. I was very happy to see people using a variety of collection processing functions. This problem required a lot of them. Just a few suggestions:

To create Comparator , we can use compareBy which satisfies nearly all cases. To sort using comparator we can use sortedWith .

, we can use which satisfies nearly all cases. To sort using comparator we can use . We can find index of an element by indexOf function.

3. We really enjoyed benchmarks and ReadMe in GitHub projects. Though we enjoyed even more short and clean solutions. We enjoyed comments, but we loved self-explanatory code.

4. We need to add node one by one because otherwise shorter in terms of nodes instead in terms of length path might be chosen. Here is another test that some solutions couldn't pass: