<![CDATA[ rolisz's site - Personal Posts ]]> https://rolisz.ro https://rolisz.ro/favicon.png rolisz's site https://rolisz.ro Sun, 11 Oct 2020 23:37:54 +0300 60 <![CDATA[ Tenet ]]> https://rolisz.ro/2020/09/17/tenet/ 5f63ce5e5ad1bb49f64c7098 Fri, 18 Sep 2020 00:44:09 +0300 After many delays, the movie Tenet has finally come to the cinema. It's amazing. I loved every single bit of it. Almost 2.5 hours of pure awesomeness. #christopernolan4president This is what a good movie is supposed to be like. Go on, go see it. The rest of the post will wait.

At least, that's my opinion. I went to see it at the cinema with 5 friends. One left halfway through the movie. One fell asleep. One admitted he lost track of what was going on. One said it was good movie. The last one is as excited as I am.

This happens after 5 minutes

The movie is a packed action movie. It starts with 10 seconds of logos, the you have actual footage and then 15 seconds later the shooting starts. And it keeps a similar pace for the rest of the movie, sometimes stopping to try and explain what's going on.

It's a philosophical movie. I mean, it's about going forwards and backwards in time. It screws with causality. It explores the grandfather paradox. It does make your mind bend, even more so than previous Nolan movies. Just think about the nested temporal double pincers.

Bullet holes from bullets that haven't been fired yet

The heists/breakins/missions are really well thought out and you actually get to see some of them twice :D

This happened for real

The visuals are stunning. I mean Nolan bought a plane and blew it up for real, rather than doing it with CGI. The sequences where there are both normal and inverted people (so going both forward and backward in time) are... wow. The locations where they filmed are gorgeous.

The soundtrack is brilliant. Ludwig Göransson managed to capture the feel of the movie perfectly. The score fits perfectly to the scenes. He made the melodies by researching retrograde composition, so they would sound (approximately) the same forward and backward.

The acting is great. I don't know where Nolan found the John David Washington, the actor who plays "The Protagonist", but he made a really great choice. I'm looking forward to seeing him in more movies. And it makes me want to watch BlacKkKlansman.

Occasionally the movie slows down to make a joke about suits

Did I mention I love this movie? Is it obvious from my review? No? Well, score: 11/10. There, if it wasn't clear enough so far.

This is a movie that needs to be seen in cinemas. There's big explosions, bullets flying, beautiful landscapes, all of which benefit from a big screen and good sound system. So go watch it in the cinema, so that Christopher Nolan will get money to make more awesome movies.

]]>
<![CDATA[ Visiting Romania: The Black Sea ]]> https://rolisz.ro/2020/09/10/visiting-romania-the-black-sea/ 5f5a69604f71eb12e0abba52 Thu, 10 Sep 2020 23:08:33 +0300 Fun fact about me: until this year, I've never been to the Black Sea for touristy, sun bathing and relaxing purposes. I've been there 3 times before: the first time in an April, for a Math Olympiad, when it was cold, the second time in a February, for a Physics Olympiad, when it was extremely cold and a third time in August, on a tour of historic christian sites, which was fun, but exhausting and I didn't have time to relax.

Last weekend, I finally got the chance to enjoy a couple of lazy days and the shade of an umbrella on the beach. And more excitingly for me: I got to fly again. I so missed it.

We flew from Oradea to Bucharest, rented a car and drove 3 hours to Neptun. Surprise: TAROM is an airline company just like any other. It might have been worse in the past, but the flight was very ok. The airport in Oradea is growing: it now has two terminals. Too bad it doesn't have too many flights 😄

Driving on the not so sunny "Sun Highway"

I rented the car from FMNRent and I was very impressed. It was raining pretty bad when we arrived and I didn't know where their office would be, so I was afraid I'd have to walk quite a bit in rain. Well, they waited for us at the airport, took us to the office by car, I signed the papers quickly and we were on our way. Really awesome customer service!

On our way to Neptun we did a small detour to have lunch at Forest M. The pictures on Google Maps don't do it justice. Really nice location, in the middle of a forest, really good atmosphere and great food as well. I want to go to the sea again so that I can stop and eat there again.

And then we got to the seaside finally. I didn't expect Neptun to be so green. Just 2 minutes away from the beach there are nice parks and forests and lots of places to hide from the sun. I liked it. It also has an interesting mix of older communist style buildings and more modern hotels. It used to be Ceaușescu's seaside retreat, so it has a lot of fancy older villas and that's why there are so many parks.

The sea was quite agitated, with many waves. Two observations: first, it's really fun to jump into waves. I don't know why, but it's just fun. Second: there is a certain mystic/transcendent quality to waves washing up on the shore, only for other ones to come again and again. While I know the theory behind the waves and the tide, I can't help but wonder how marvelous and inspiring it must have been for people living ages ago.

I'm not really sure what it was, but even the garlic was delicious

Before going, I heard from multiple people that the Romanian seaside is expensive and that Bulgaria, Greece or Croatia are cheaper. Well, we found a restaurant 2 minutes away from the beach with great food and normal prices. I don't know where everyone else went.

View from the hotel. Eastern Europe in one picture. 

The hotel was meh. To quote Dyatlov, not great, not terrible. It was 5 minutes from the beach, which was the important part.

And then our short trip ended. We flew back, circled the Oradea airport several times, because of unfavorable air currents, saw our house that is still in construction from the air and went back to work.

]]>
<![CDATA[ Visiting Romania: Șuior ]]> https://rolisz.ro/2020/08/11/visiting-romania-suior/ 5f32dea14f71eb12e0abb808 Tue, 11 Aug 2020 22:37:26 +0300 I have to admit that I haven't seen too much of my home country. I've set foot in at most half of the counties and I've actually visited and done touristy things in even fewer of them. I've visited more countries than that as a tourist.

But after a recent trip to Maramureș, a county in the Northern part of Romania, I was so impressed by how beautiful it looked, that I want to fix this deficiency I have.

Lake Bodi

Last weekend we went to the Șuior area with some friends who had been there before. First we stopped by Lake Bodi to have lunch there. It's a very calm lake, with very nice surroundings, good for sunbathing, swimming or taking a nice stroll.

Then we went to the Șuior peak. It's a short drive from the lake. During winter, it's a ski resort, so they have a chairlift, which is also operating during summer.

I always enjoy going on chairlifts. It's so quiet up there, as you slowly go up the mountain, with just the trees around you.

Reaching the peak, where there is a weather station, is done by a 40 minute hike, on a 30% slope. As we were going up, we started hearing thunders. Looking around, we see some dark clouds forming. Should we go back or should we press on? Despite me barely catching my breath, we decided to reach the peak, no matter what. I was soaking wet anyway, what's some rain going to do?

Fresh bilberries

We got to the top safely, without only a couple of drops falling on us. We could see in the distance where the rain was flooding everything, but we were safe. We picked some bilberries and then we started going back.

Me walking up barefoot

It rained on us a little bit going down and the grass was more slippery, but we got down safe. When we got there, the chairlift operator told us that the previous group rode the chairlift during the rain and got soaking wet. There was even hail where we left our car. But fortunately, we avoided all of that.

After finishing what the leftovers from lunch, we started the two hour drive back. The other great thing about Maramureș is that even the roads are really scenic. At least the roads we took were really good and often we would go through forests.

All in all, it was a really relaxing day. Thank you our dear "godparents" for taking us out for this fun day and thank you for all the really good talks as well!

I’m publishing this as part of 100 Days To Offload - Day 34.

]]>
<![CDATA[ Boardgames Party: Azul ]]> https://rolisz.ro/2020/08/03/boardgames-party-azul/ 5f248b5b4f71eb12e0abb7b8 Mon, 03 Aug 2020 22:38:54 +0300 Last weekend I played a fun game called Azul, which has won many awards, among which the famed Spiel des Jahres in 2018. The name comes from "azulejos", Portuguese tiles made by the Moors. A portugese king fell in love with them and tasks the players with making the most beautiful decoration in his palace.

The factories making the tiles

There are a number of factories "making" four tiles each. On their turn, players take all the tiles of one color from a factory and put the others in the middle. Using these tiles, they have to prepare to decorate the wall.

Top: scoring area. Left: staging area. Right: the wall to be decorated. Bottom: penalty area

Take too many tiles, which don't fit the staging area, you have to put the surplus ones in the penalty area. Don't gather enough tiles of one color to fill one row in the staging area and you are not able to decorate the wall.

Second row is filled and will decorate the wall. Fourth row is missing one tile, so it won't be used in this turn. 

The game lasts until someone manages to fill one horizontal row on the wall. You get extra points if you fill out vertical rows or if you manage to place all the tiles of one color.

The game is quite simple to explain and it doesn't last long. On our first play, with reading the instructions and understanding the rules, it took one hour, of which the actual game play was half. Despite it's simplicity, there are several interesting strategies to explore, whether to go for the shorter rows first, or for the longer ones, whether to try to get extra points by doing the bonus stuff or whether to try to finish first. It was a nice brain teaser for a Friday night.

Score: nine

The tiles are kept in this really cute bag

I’m publishing this as part of 100 Days To Offload - Day 33.

]]>
<![CDATA[ Note taking with Obsidian ]]> https://rolisz.ro/2020/07/28/obsidian/ 5efb2d5717253e7fe6dd64ca Tue, 28 Jul 2020 10:37:56 +0300 After starting to use spaced repetition more actively and being more consistent with my journaling, now I've tackled improving my note taking skills.

Over the last couple of months, I've read about various note-taking methods, different approaches and diverse goals for them and they helped me change my perspective on collecting ideas.

My previous approaches to taking notes

While I was still in school, I didn't take too good notes. I guess the Romanian educational system is not set up to encourage that kind of critical thinking which leads to taking good notes. In most classes, the teacher would dictate a lesson, I would write that down and then memorize it, without needing to create my own summarized version of the lesson.

Then I took notes in Google Keep. When it came out, I was impressed by how simple and snappy it was, compared to Evernote for example. I've used it for around 6 years, so I have a lot of notes jotted down there. But Keep doesn't offer good ways to organize notes, having only tags and search. But to search, you have to remember something to look for, so it's almost impossible to find notes which you have forgotten.

As I've started moving my notes from Keep to Obsidian, I found a lot of old notes which I had totally forgotten about. Some of those notes were ideas I had, but I never followed up on them. Looking back they were good ideas: I know because in the meantime other people have implemented them.

Goals of a note taking system

A goal of a note taking system should be to store information, so that your brain can do more fun/useful stuff.

But another goal is to help you connect ideas, even from different domains. As you read things in books, blogs, or come up with ideas on your own, eventually some of them will be related and can be combined to make something even better. The note taking system should facilitate creating these connections.

It should also help you process your notes. A note is not a static thing. It's not something you just write down and then never touch again. Almost all ideas will eventually need refining. When you read a book and find something noteworthy, you write some notes, but in time you find that you can reword it better, as you understand things better. For your own ideas, in time you add something; you cut some things, hopefully making them better and then you create something with them.

Lastly, the note organizing system should help you organize and browse your notes. It should encourage serendipitous rediscovery of forgotten notes. It should help visualize them. It should make it easy to see all notes that are related to a certain note or to a certain topic.

Enter Obsidian

Meta: Writing this post in Obsidian. Left: folder pane; middle: raw markdown; right: Rendered note

Obsidian is a new desktop app for creating a "A second brain, for you, forever.". It's quite new, it was released publicly two months ago and it's not even at version 1.0, but development is progressing quite quickly, with a new version coming out almost weekly.

Obsidian is built around Markdown files, which means that the notes are portable and won't get stuck in an old program, should the company go out of business. It enhances the Markdown syntax with the some shortcuts for creating connections between pages using [[name of other page]] syntax. You can see for each page what other pages link back to it. There's also a way to embed one file (or part of a file) into another one.

The graph view

One cool looking feature in Obsidian is the graph view, where you can visualize how your notes are connected. This leads to some interesting looking "constellations".

There are also plugins available. For now, the API is internal, so all the plugins are made by the same company, but they say that once they reach 1.0, they will make the API public.

Because Obsidian is based on Markdown files, you can store them wherever you want and you can sync them across devices with your favorite syncing tool. So far, I've used GitHub.

One disadvantage is that it doesn't have a mobile app so far (or a remote interface in general). On one hand, it's not a big issue for me, because my goals for notes require a big screen and a nice keyboard. In practice, I still like to have access to my notes even on the go, so I just use an Android Git client with a Markdown editor. Maybe one day I'll change this to expose my notes as a static website.

My note taking workflow

My workflow is inspired by (but only inspired, not fully copied from) the Zettelkasten method of Niklas Luhman, which is the trending note taking framework du jour, sprinkled with ideas from Tiago Forte and others. When my note-taking workflow grows up, it wants to be like Andy Matuschak's notes.

I have a loose categorization of my notes into folders. I'm not very strict about them and I don't want to spend much time organizing a hierarchy. Some notes also have tags (simply words prefixed with # and Obsidian is smart enough to start a search for them if you click on one).

Many of my notes don't start their life in Obsidian, but in my bullet journal. When I hear something new, or I read something interesting, often my bullet journal is closer to me, so I jot down the main ideas there. Then, when I get to my computer and I have time I copy it into Obsidian and I flesh out the ideas fully. Then I try to find any other notes that are relevant and add connections to them.

One thing that I try to do is to make my notes my own. This means that if I read an interesting article, I don't just copy paste the interesting parts, but I actually reword them and write them down as I understood them. This helps both comprehension and retention.

For certain topics, I create index maps, where I list all the notes that I have related to that topic. So notes belong to multiple index maps, because they are relevant in different areas.

As I add new notes and create connections between them, I end up revisiting old notes and updating them. Sometimes it's with a negative update (it didn't pan out, it was a wrong idea), but sometimes it's a positive one (a further development or something similar someone else has done).

The most interesting part is when notes from very different areas start to "touch" each other. Because of the graph view, it's easy to see how close notes are to each other. I also use the graph view to see what notes are isolated and then I try to find them a place. As you can see in the above screenshot, I still have a lot of work to do.

So far I have added 160 notes in Obsidian, so it's a small knowledge base. I still have many notes in Google Keep to move over. But I feel like Obsidian has already helped me (I feel on top of my notes) and I hope something nice (and useful) will come out of it.

Top photo by Pogány Péter - Egen Wark.

I’m publishing this as part of 100 Days To Offload - Day 31.

]]>
<![CDATA[ Battlestar Galactica ]]> https://rolisz.ro/2020/07/25/bsg/ 5efb1f3917253e7fe6dd6465 Sat, 25 Jul 2020 21:15:03 +0300 I finally did it: I watched one of the best TV shows made in the last 20 years (at least according to a list made by the New York Times). For some reason, I didn't like the concept back when it aired, but I decided to give it a shot after I finished Travelers. And I was hooked.

Battlerstar Galactica (BSG) is nominally a sci-fi show about a war between humans and the robots they created. But the show actually is more about all kinds of philosophical, political, religious and metaphysical debates.

It's a 15 year old show, but somehow I have avoided spoilers and I had pretty much no idea about anything that would happen. But this post will have spoilers :P

One of the recurring themes in the show is that of a cyclical repetition of history: "All this has happened before, and all this will happen again" is a motto oft repeated in the show. There is a recurring theme of humans creating robots (cylons), cylons rebelling against their makers, cylons almost wiping out the humans and then this would repeat again, several thousand years later. The show tries to end on an optimistic note, that maybe now the cycle might be broken, due to the "law of large numbers".

The eponymous battlestar

BSG also explores how a civilization should be lead. The humans are initially governed as a democracy, with elections, fair trials and so on. But those things tend to get in the way of quick and decisive action, which is needed during war. There are several military coups, rebellions, sham trials and so on. They get pretty close to exploring communism, to get rid of class warfare. They are willing to do genocide against the cylons, initially not being able to consider getting to a peaceful agreement with them. Even though initially the two leaders, Admiral Adama and President Roslin are very likeable, after four seasons, during which they had to make many questionable decisions, they lose a lot of their charisma.

Religion has an important role for the people (and machines) of BSG. Humans start out with a polytheistic religion ("coincidentally", with names from the Roman and Greek pantheon). Cylons have a monotheistic religion, I'd say a bit inspired from Christianity. There are religious writing that make prophecies which (seem) to come true and which guide the humans in their search for a new home. I am a bit conflicted about this part. On one hand, my personal beliefs are somewhat similar to what the ending of the show ("it's part of God's plan"), but at the same time, I like my sci-fi with less religion.

Over the course of 4 seasons, all the characters evolve. As I said before, the leadership gets stained by all the hard decisions they've had to made and by the end of the show they are tired and sick of it all. Apollo is hilarious as he gets fat and lazy and then has to work extra hard to get back in shape (though maybe I shouldn't be laughing at this...). But the character development of Gaius Baltar is a bit too extreme for me. He goes from a completely selfish and narcisistic person to being a guy preaching love and forgiveness and who's ready to give his life for the greater good. I don't know, it feels too fishy. On the other, Starbuck being revealed to be an "angel" or whatever she was... scratches head confused.

A cylon centurion

The cliffhangers in the show are great. I had the privilege of binge watching, but I think 13 years, the midseason breaks and the season finales were brutal. The one at the middle of the 4th season, when they find Earth and it's a radioactive pile of rubble, wow, that gave some really nice twists to everything, especially about the origins of the Final Five cylons.

The technology used in-universe is weird. On one hand, they have very advanced stuff, like faster than light travel, on the other hand, they use really old-school analog scales, you know, the ones where you have to adjust the weights. The computers are not networked, but at least they have a good explanation for that: so that Cylon viruses can't spread from one system to another. But their papers are weird: they have their corners cut off. The process for refining tyllium (their fuel) is extremely manual, almost like coal mining 100 years ago.

The show was made before Netflix came and changed the format of TV shows. While the first season is short (10 episodes), the others have 20 episodes. I have to say, I'm glad TV shows nowadays are shorter. Seasons 2 and 3 have a lot of filler episodes. Season 4 is better, because the writers knew the series was going to end, so they could plan accordingly.

The acting is quite good. In particular, James Callis does a remarkable job with Gaius Baltar, exhibiting over four seasons a comprehensive range of human emotions. Tricia Helfer also has a quite challenging role, having to play the many clones of the Number 6 Cylon, in different places and different postures.

I really liked this show. It is one of the most captivating shows I've watched in the last 2-3 years. It has some flaws, but it's a really great space opera.

Grade: 10

As a side note: I watched this on Amazon Prime and the English subtitles are so bad. Sometimes the subtitles are off by 1 second (I haven't had this issue anywhere else in the last 10 years) and sometimes it seems like the subtitles were written by ear by someone who doesn't have good English.

I’m publishing this as part of 100 Days To Offload - Day 30.

]]>
<![CDATA[ Going camping ]]> https://rolisz.ro/2020/07/11/going-camping/ 5f09cb7651d8dc2b1662a90d Sat, 11 Jul 2020 18:21:03 +0300 I have never been camping in my life. I slept once in a tent in my aunt's backyard, with my cousins, but that doesn't count. This year I've decided it's about time to fill that gap in my life and actually sleep in a tent in the forest.

I actually wanted to do this last year. I even bought a tent, sleeping bags and other necessary items. Unfortunately, my wife and I were quite busy last summer, so we didn't get around to going camping.

But, this weekend, the stars aligned, and the group of guys I go hiking with decided to go camping. We wanted to do Via Ferrata and rafting too, but the water level was too low for rafting :( But at least I got to sleep in a tent in the forest.

Of course, first I tested my tent in my in-laws backyard. I didn't want to  figure out how to assemble it out in the field, while bugs are biting me and there is not much sunlight left. And only then did I dare to install it in the forest.

Our camping spot, before it was cleared out

We had eyed a camping spot about 30 metres from the river, next to a rock cliff. My friends arrived there first and they cleared it out, because it was quite overrun by vegetation. We made a fire, but we forgot to gather enough wood while it was still light out, so we couldn't keep it going for too long. It didn't matter too much, because by midnight all of us were sleepy and we headed to our tents.

And I have to say I loved it. I was warned that I'll be shivering towards the morning - with a proper sleeping bag, that's not a problem at all. I was told it would be uncomfortable and I wouldn't be able to find a good position to sleep in - well, the others in my group did report such problems in the morning, but I had a foam pad and a self inflating pad, so I slept really well, with no aches in the morning, only a fresh feeling.

The narrow path to our spot, almost completely overgrown by vegetation

I'm a man who enjoys comfort, so if I tried to do a hiking + camping trip, where I would have to carry my tent on foot for a long distance, I would probably enjoy things much less. But if I can stuff the car full of things that make it more comfortable, I think camping is really great, no need to go back to the "stone ages".

I can't wait to go camping again, this time with my wife!

I’m publishing this as part of 100 Days To Offload - Day 28.

]]>
<![CDATA[ An update on 100 Days to Offload ]]> https://rolisz.ro/2020/06/30/100-days-to-offload/ 5ef7acb317253e7fe6dd6452 Tue, 30 Jun 2020 15:22:58 +0300 It's been almost two months since I have started the #100DaysToOffload challenge. During this time I have managed to write 25 blog posts, which is more than I wrote last year in total.

But in June I found myself posting less and less often. What's going on?

Part of the reason is probably that the initial excitement wore off. I saw that sometimes I rushed posts in order to publish every day and I'd like to keep the quality high, especially for the tech posts.

But another reason is that the lockdown is pretty much over here in Romania and life started again. All kinds of events are happening, meeting friends, going out to eat and so on. All of these take a considerable amount of time, which squeezes out blogging.

The problem is not only time, but "communication" energy. I'm an introvert, so to recharge my batteries I need alone time. Lockdown was awesome for me, because my batteries didn't get as depleted. On the contrary, the lack of interaction with others started to bother me and I actually missed going out with others. But now that I do finally get to meet up with friends, I find that at the end of the day words don't flow out, even in writing. I guess blogging is similar enough to interacting with people that they tap into the same energy reservoirs.

And last, but not least, work on my house has resumed in (almost) full force and I have to start overseeing that. And it's a consuming job, because each contractor is saying that the previous guy didn't do a good job and they have to fix it. sigh I have to keep in mind what the end result will be and that it will be worth it.

So, in conclusion, I do plan to continue writing, but I won't write as often. My aim is to get around 7-8 posts per month, which should get me to finishing the modified version of the challenge (publishing 100 posts in one year).

I’m publishing this as part of 100 Days To Offload - Day 26.

]]>
<![CDATA[ Pixel 3a ]]> https://rolisz.ro/2020/06/27/pixel-3a/ 5ef5097d17253e7fe6dd63f1 Sat, 27 Jun 2020 23:24:18 +0300 I've been a long-time fan of Google-made/branded phones. Back in 2014, I got a Nexus 5. Then I moved on to a 5X. Then I explored the other side, having an iPhone 6S for a year. I was not impressed by iOS, so then I switched back to Android, this time using a Pixel phone.

I am aware that I often don't handle my phone with enough care, so I always buy a case for it. My Pixel was covered in a Supcase Unicorn Beetle case, which was bulky, but it managed to keep my phone intact, despite numerous drops (and throws). Until I took it out for 10 minutes and of course I cracked the screen. But the phone still worked, so I decided to hang on to it for as long as possible.

I somehow got used to the cracks on the screen. But the Bluetooth became flaky. I don't think it's from the drop, because there are plenty of online reports about others having similar issues. Eventually, it got bad enough that most of the time no combination of device restart/bluetooth restart/turning off wifi got my phone connected to either my earbuds or to my car. This meant that I couldn't listen to sermons while driving or while running, so I decided it's finally time to get a new phone.

The Google Pixel 3a has been receiving glowing reviews. It's cheaper than other similar phones, and it has one of the best cameras on the market (though only one lens, no telephoto or widelens). Some people don't like the fact that the back is made of plastic, but I don't care, because I'm going to keep it in a case anyway.

And it's pretty much what I expected: a better version of my previous phone. It looks almost identical. It doesn't have a notch. It still has a headphone jack. Screen is a bit taller. Supposedly it has a higher PPI, but who can tell at these counts? The CPU is faster, but I don't play games, so I'm not impressed. More storage space. And the camera is even better and this I care about. I haven't gotten the chance to take it out into nature (or even into the city), but backyard testing shows really good results. Both portrait mode and night sight are amazing, even though (or maybe because) they are software only, with no special hardware component for them.

One weird thing about the Pixel 3a is the "flexible" sides. When you squeeze the phone on the sides, it activates the Google Assistant. I found it too sensitive and sometimes I trigger it just by picking up the phone from the table. I don't use the Google Assistant anyway, so I'll just disable that feature.

I'm such a huge fan of the Unicorn Beetle cases, that I ordered another one for the Pixel 3a. Unfortunately, I couldn't find it in Romania, so I had to order from Amazon UK and it hasn't arrived yet :(

All in all, I am very happy with my new phone. I'm glad Google kept the winning formula and didn't introduce new designs just for the sake of having something new. I hope there will still be a Pixel 5a in 2-3 years, when I'll replace this one :D

I’m publishing this as part of 100 Days To Offload - Day 25.

]]>
<![CDATA[ How Much Does It Cost To Run This Blog? ]]> https://rolisz.ro/2020/06/15/how-much-does-it-cost-to-run-this-blog/ 5ee7b28417253e7fe6dd638a Mon, 15 Jun 2020 21:19:45 +0300 The current meme/trend/fad blog post topic in my blogroll is about how much it costs to run a personal blog.

So here are my costs per month:

Service Monthly Costs
Domain name 1€
VPS hosting 5.95$
DigitalOcean backup 1.19$
Total ~7.31€

Initially, I bought the domain name from a Romanian registrar. Back then it was for life. I payed something like 30 euros for it. But 3 years ago, the powers that be decided to bring the .ro TLD to more "modern" standards and to have you pay yearly. I paid in advance for 10 years, 12 euros per year.

I have the smallest VPS from DigitalOcean to run my blog. I have another droplet which runs some other things, but I don't count that here. I also use their DNS hosting, which is free.

I also use their droplet backup service, which is 1 euro + VAT. It's the simplest form of backup and I should probably set up some higher level exports from Ghost to back up my posts.

I see that both Jan-Lukas and Kevin use a CDN. I haven't thought about using one so far, because I've been happy with performance so far. Serving static sites is really cheap. However, if I land on HackerNews again, I might need it...

Otherwise I use free tools to write my blog. Because I enjoy blogging, I also don't consider the countless hours sunk into this as a waste :)

I’m publishing this as part of 100 Days To Offload - Day 24.

]]>
<![CDATA[ Playing Codenames with Rust ]]> https://rolisz.ro/2020/06/10/playing-codenames-with-rust/ 5ed5526217253e7fe6dd5eb5 Wed, 10 Jun 2020 19:53:21 +0300 I love playing board games. I love machine learning. I love Rust. So why not combine them and do something really fun? So I ran a word vector analogy query and I got back:

machine learning + board games - Rust = Codenames 

So let's implement Codenames in Rust. It's a simple game. There are two teams, each having one Spymaster and several field operatives. There is a shared 5x5 board, which represents a map to the field operatives, which are hidden. Each spot on the map has a word on it. The spymasters must give clues to the operatives so that they can find the enemy operatives and take them out. A more detailed description and pictures can be found in my previous post.

In this post, we will implement a simple model for the game, using dummy agents. In a future post, we will implement some smarter agents, with word vectors.

Modeling the map

First, let's start modeling the map. Each cell on the map can be a red agent, a blue agent, an assassin (black) or a neutral character (gray). There is one word on each cell and we need to track if the cell has been overturned or not. All this will go in a file called map.rs:

#[derive(Debug, Copy, Clone, PartialEq)]
pub enum State {
    Gray,
    Red,
    Blue,
    Black
}

#[derive(Debug)]
pub struct Cell<'a> {
    pub color: State,
    pub word: &'a str,
    pub revealed: bool
}

Cells have a lifetime parameter which is needed for the word field. Using the 'a lifetime parameter we tell the compiler that the str in that field will live at least as long as the cell that contains it.

While the derived Debug provides a way to print out the debug version of both the State and the Cell, when printing out in the CLI the actual game we'll want to customize what we print out. For this, we will have to implement the  Display trait, which is then used by formatters to create a string representation. For states, we will print only the first letters, for cells we will print the state if the cell has been overturned (so it's visible) and its word otherwise.

use core::fmt;
use crate::map::State::{Gray, Red, Blue, Black};

impl fmt::Display for State {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let char = match self {
            Gray => 'N',
            Red => 'R',
            Blue => 'B',
            Black => 'X'
        };
        write!(f, "{}", char)
    }
}

impl fmt::Display for Cell<'_> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if self.revealed {
            f.pad(&format!("{}", self.color))
        } else {
            f.pad(self.word)
        }
    }
}

Now let's make a struct for the map. The map will have just a vector for the cells. We also want to provide a constructor, which in Rust is by convention called new, which takes a list of words, chooses 25 words randomly, and assigns each cell a color. Initially, all cells will have revealed flag set to false.

use rand::prelude::*;

pub struct Map<'a> {
    cells: Vec<Cell<'a>>
}

impl Map<'_> {
    pub fn new<'a>(words: &[&'a str]) -> Map<'a> {
        let mut colors = vec![Gray; 7];
        colors.append(&mut vec![Red; 9]);
        colors.append(&mut vec![Blue; 8]);
        colors.push(Black);

        let mut rng = thread_rng();
        colors.shuffle(&mut rng);
        let words: Vec<&&str> = words.into_iter().choose_multiple(&mut rng, 25);

        let mut cells = Vec::with_capacity(25);
        for i in 0..25 {
            cells.push(Cell { color: colors[i], word: words[i], revealed: false });
        }
        Map{cells}
    }
}

We have to have a lifetime even for map, because the cells need it. For the constructor, because we have two input lifetimes, we have to explicitly specify to which one is the output lifetime related: to the words, because the map will contain some of them.

For all the random stuff we use the Rand module. We import everything from it and then we instantiate a thread_rng. I find the name a bit misleading, because it has nothing to do with threads per se, it's just that it's not thread safe. Our program doesn't use threads, so we don't care about that. The Rand module then provides a trait for slices which we can use to shuffle the color list and to choose the 25 random words.

Let's also add a Display implementation, which will just show each cell, using the Display for Cell. To make sure things show up nicely in a grid, we calculate the longest word first and then we pad all the words to that size.

impl fmt::Display for Map<'_> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let max_len = self.cells.iter().map(|x| x.word.len()).max().unwrap();
        for i in 0..5 {
            for j in 0..5 {
                write!(f, "{:width$} ", self.cells[i*5+j], width=max_len)?;
            }
            write!(f, "\n")?;
        }
        Ok(())
    }
}

The formatter knows to use the Display for Cell, so it will show words for unrevealed cells and colors for revealed cells.

Let's put everything together and test it out. Let's write a simple main.rs:

mod map;

use crate::map::{Color, Cell, Map};


fn main() {
    let words = vec!["RAY", "REVOLUTION", "RING", "ROBIN", "ROBOT", "ROCK",
"ROME", "ROOT", "ROSE", "ROULETTE", "ROUND", "ROW", "RULER", "SATELLITE", "SATURN",
"SCALE", "SCHOOL", "SCIENTIST", "SCORPION", "SCREEN", "SCUBA DIVER", "SEAL",
"SERVER", "SHADOW", "SHAKESPEARE", "SHARK", "SHIP", "SHOE", "SHOP", "SHOT", "SINK"];
    let map = Map::new(words);
    println!("{}", map);
}

I hard coded there a list of words for now, we'll get rid of that soon. For now, this should output something similar to the following:

RAY         SHOT        RING        ROBIN       ROBOT       
ROCK        ROME        ROOT        SHARK       ROULETTE    
ROUND       ROW         RULER       SATELLITE   SATURN      
SCALE       SCHOOL      SCIENTIST   SCORPION    SHOE        
SCUBA DIVER SEAL        SERVER      SHADOW      SHAKESPEARE 

Everything is nicely aligned.

The player agents

We have a map. Now let's start implementing the players. There will be two player types: a spymaster and a field operative. The spymaster has to give hints based on the map and the field operative has to guess words, based on the hint. In Codenames hints consist of a word (a single word) and a number, which is the number of words on the board that are related to the hint word. This goes into a players.rs file.

use crate::map::Map;

#[derive(Debug)]
pub struct Hint {
    word: String,
    count: usize,
}

pub trait Spymaster {
    fn give_hint(&mut self, map: &Map) -> Hint;
}

pub trait FieldOperatives {
    fn choose_words<'a>(&mut self, hint: &Hint, words: &[&'a str]) -> Vec<&'a str>;
}

The Hint word is a String and not a &str because in some cases we might have to return owned hints and not borrowed ones (such as when reading from the CLI).

Both the Spymaster and the FieldOperative must borrow self mutably, because they might need to change some state internally (such as a random number generator) to give their corresponding answers.

For now, we will implement two kinds of players: a random player, that outputs random clues and chooses words at random and a human player, that reads from the outputs from the keyboard.

use rand::prelude::*;

pub struct RandomSpyMaster<'a> {
    rng: ThreadRng,
    clues: &'a [&'a str]
}

pub struct RandomFieldOperatives {
    rng: ThreadRng,
}

impl RandomSpyMaster {
    pub fn new() -> RandomSpyMaster {
        let rng = thread_rng();
        RandomSpyMaster{rng}
    }
}

impl RandomFieldOperatives {
    pub fn new() -> RandomFieldOperatives {
        let rng = thread_rng();
        RandomFieldOperatives{rng}
    }
}

The random field operative has only a random number generator, while the spymaster also has a list of words from which it can choose clues.

impl Spymaster for RandomSpyMaster<'_> {
    fn give_hint(&mut self, _map: &Map) -> Hint {
        let word = (*self.clues.choose(&mut self.rng).unwrap()).to_string();
        let count = self.rng.gen_range(1, 5);
        Hint { word, count }
    }
}

impl FieldOperative for RandomFieldOperative {
    fn  choose_words<'a>(&mut self, hint: &Hint, words: &[&'a str]) -> Vec<&'a str> {
        let nr_found_words = self.rng.gen_range(1, hint.count+1) as usize;
        words.choose_multiple(&mut self.rng, nr_found_words).copied().collect()
    }
}

We do a dereferencing when getting the word in the spymaster, because Clippy says it's faster that way.

Let's test them out, by adding them in our main function:

    let mut sp = RandomSpyMaster::new(&words);
    let mut fo = RandomFieldOperative::new();

    let hint = sp.give_hint(&map);
    println!("{:?}", &hint);
    println!("{:?}", fo.choose_words(&hint, &words));

And we will have as output:

Hint { word: "SHIP", count: 4 }
["ROSE", "SERVER", "SHIP"]

It ain't much, but it's random work. Let's implement the CLI players, which are even simpler:

pub struct HumanCliSpymaster {}

pub struct HumanCliFieldOperative {}

They don't have any fields or internal state, because everything comes from the brain of the player.

The spymaster player should give the hint in the following format: 2 rust. If we can't parse it, we'll ask them to give the input again.

impl Spymaster for HumanCliSpymaster {
    fn give_hint(&mut self, map: &Map) -> Hint {
        println!("Give a hint for this map in count, word format: \n{} ", map);
        loop {
            let mut input = String::new();
            io::stdin().read_line(&mut input).unwrap();
            let results = input.split_ascii_whitespace().collect::<Vec<&str>>();
            match results[0].parse::<usize>() {
                Ok(count) => return Hint { count, word: results[0].to_string() },
                Err(_e) => println!("Give hint in count, word format!"),
            };
        }
    }
}

When reading from stdin, we apply unwrap. I can't even really imagine under what conditions that read might fail or what you could even do in that case, so I think it's fine.

The field operative has to give a list of as many words as the clue said, each on separate line. All the words they give must be from the words on the map.

impl FieldOperative for HumanCliFieldOperative {
   fn choose_words<'a>(&mut self, hint: &Hint, words: &[&'a str]) -> Vec<&'a str> {
       let mut chosen_words = vec![];
       println!("Choose {} words from {:?}", hint.count, words);
       let mut counts = hint.count;
       while counts > 0 {
           let mut input = String::new();
           io::stdin().read_line(&mut input).unwrap();
           if input.trim() == "" {
               return chosen_words;
           }
           match words.iter().position(|&x| {
               x.to_lowercase() == input.trim()
           }) {
               Some(c) => {
                   chosen_words.push(words[c]);
                   counts -= 1;
               },
               None => println!("Choose a word from the given list")
           }
       }
       chosen_words
   }
}

If the player submits a blank line, we return only the list of words we have found so far, even if they are fewer than what hint said, because they probably are out of ideas. If the player submits a word that is not on the map, we ask again.

To find the index of an item, the Rust idiom is to do vector.iter().position(|x| x == my_value), which returns an Option. I find it too verbose. Why isn't there a convenience indexOf method that directly tests for equality? I'm sure this is a very common use case.

The game logic

Now, let's put all of this together and make our game logic, in a file called game.rs. First we define a function that will do all the player interaction: getting hints and choosing words.

pub fn get_actions(spymaster: &mut dyn Spymaster, field_op: &mut dyn FieldOperative, map: &Map) -> Vec<String>{
    let hint = spymaster.give_hint(map);
    field_op.choose_words(&hint, &map.remaining_words()).iter().map(|&x| x.to_string()).collect()
}

The dyn keyword is necessary to make it explicit that we are using a trait object, which has some performance implications.

Next, we need to check the results. What happened in the last turn? If the player chose the Black cell, instant game over, they lost. If the player chose one of their own field operatives or a neutral one, their round is over and the remaining guesses are discarded.

fn opposite_player(color: Color) -> Color {
    match color {
        Color::Red => Color::Blue,
        Color::Blue => Color::Red,
        _ => panic!("Impossible player type!")
    }
}

pub fn check_if_lost(current_player: Color, guesses: &[String], map: &mut Map) -> bool {
    for word in guesses {
        let color = map.reveal_cell(word);
        if color == Color::Black {
            return true;
        }
        if color != opposite_player(current_player) {
            return false;
        }
    }
    false
}

We have also added a helper function to get the color of the other player. Now we need to reveal the cells in Map.

impl Map<'_> {
    pub fn reveal_cell(&mut self, word: &str) -> Color {
        let cell = self.cells.iter_mut().find(|x| x.word == word).unwrap();
        cell.revealed = true;
        cell.color
    }
}

To reveal a cell, we simply iterate over all the cells until we find the one where the word matches. We know the word comes from the cells, so it's safe to unwrap. We set the revealed bit to true and then we return the color of the cell, because that's all we need.

And now let's loop until the game is over.

pub fn game(red_spymaster: &mut dyn Spymaster, red_field_op: &mut dyn FieldOperative,
            blue_spymaster: &mut dyn Spymaster, blue_field_op: &mut dyn FieldOperative,
            map: &mut Map) -> Color {
    let mut current_color = Color::Red;
    loop {
        println!("The turn of player {}", current_color);
        let guesses;
        if current_color == Color::Red {
            guesses = get_actions(red_spymaster, red_field_op, &map);
        } else {
            guesses = get_actions(blue_spymaster, blue_field_op, &map);
        };
        println!("{:?}", &guesses);
        if check_if_lost(Color::Blue, &guesses, map) {
            return opposite_player(current_color);
        }
        println!("The map is now: {}", map);
        if map.is_over() {
            return current_color;
        }
        current_color = opposite_player(current_color);
    }
}

If the player lost (because they found the Black cell), we return the other player as the winner. If the game is over, we return the current player as the winner. Otherwise, we swap colors and it's the turn of the other player.

When is the game over? When there are no more cells belonging to red or blue that are unturned. To count the number of cells of a certain color that are unturned, we simply iterate over all of them and filter out the ones that have been revealed and the ones that are another color:

impl Map<'_> {
    pub fn is_over(&self) -> bool {
        if self.unturned_cells_of_color(Red) == 0 {
            return true
        }
        if self.unturned_cells_of_color(Blue) == 0 {
            return true
        }
        return false
    }

    fn unturned_cells_of_color(&self, color: Color) -> usize {
        self.cells.iter().filter(|x| !x.revealed)
                         .filter(|x| x.color == color).count()
    }
}

Our main function becomes as follows:

use crate::map::{Color, Cell, Map};
use crate::players::{RandomFieldOperative, RandomSpyMaster, Spymaster, FieldOperative, HumanCliSpymaster, HumanCliFieldOperative};
use crate::game::game;
use std::fs::File;
use std::io::Read;

fn main() {
    let mut file = File::open("resources/wordlist").unwrap();
    let mut contents = String::new();
    file.read_to_string(&mut contents).unwrap();
    let words = contents.lines().collect::<Vec<&str>>();
    
    let mut map = Map::new(&words);

    let result = game(&mut RandomSpyMaster::new(&words), &mut RandomFieldOperative::new(),
           &mut RandomSpyMaster::new(&words), &mut RandomFieldOperative::new(), &mut map);

    println!("The winner is {}", result)
}

I've also moved the list of words to an external file, so that they are not hardcoded in the binary. You can find a list of all the words in the official game with a quick DuckDuckGo search :D

If you want, you can try to put a HumanCliSpymaster or a HumanCliFieldOperative and see if you can beat the random player. It's quite easy :)

The whole code put together can be seen on GitHub (on the blog branch).

Now, we've got the basic skeleton for our Rust implementation for Codenames. Next time, we will get to the fun part: creating a smart agent that actually knows how to give hints and how to make guesses, using word vectors.

I’m publishing this as part of 100 Days To Offload - Day 22.

]]>
<![CDATA[ Boardgames Party: Codenames ]]> https://rolisz.ro/2020/06/08/boardgames-party-codenames/ 5ede7a0a17253e7fe6dd61e4 Tue, 09 Jun 2020 00:05:00 +0300 Lockdown is over, time to play some games that work well in larger groups, such as Codenames, which is a very fun game to play, but it requires language skills. On the box it says it can be played by 4-8 players, but I think you can easily have more people.

The map as seen by the spymasters

There are two teams, one red and one blue. Each team has a spymaster and several field operatives. There is a 5x5 map, where each cell can be a red agent, a blue agent, a neutral bystander or an assassin. The goal of each team is to locate all the enemy agents. Only the spymasters see the map with the color coded cells.

The map with words

The players see only the map with words overlayed on cells. In the map above, the top left cell is the assassin (black card) and has the word worm on it, while the bottom left cell has a red agent and the word box.

The spymaster must give out clues so that the field operative can figure out where are the enemy agents. The clues must consist of a single word and how many agents can be found with that clue. Obviously, you cannot use any words that are on the board, or that are plural or singular versions. For example, for the above board, a valid clue from the blue spymaster might be iron 3. If the field operatives guess correctly, the board might look like this afterward:

After guessing that mine, hook and nail are related to iron

Now it's the other team's turn and the red spy master could give a clue such as restaurant 3. The field operatives guess correctly 2 of the 3, and find a neutral bystander too.

If while guessing, a team flips an agent of their own or a neutral bystander, their turn is over and they forfeit their remaining guesses.

This keeps going until either one team finds all the enemy agents, in which case they win, or until they find the assassin, in which case they lose.

Codenames it's a really fun game, even though it's simple to explain the concept. As a spymaster, it's really frustrating to see that the clue you spent so much time to find is being completely misinterpreted by your team and you listen in horror how their speculations get closer and closer to the assassin. As a field operative, it's really fun to argue with the others what could the spymaster have meant. Maybe box is related to restaurant, because some restaurants have bento boxes.

One slight problem with the game is if you play it in a game which is not everyone's native tongue. Then some problems might arise because some people don't know the meaning of the word. But the game has already been translated to several languages and there is even a picture version of it.

Score: 10

I’m publishing this as part of 100 Days To Offload - Day 21.

]]>
<![CDATA[ Happy decennial anniversary! ]]> https://rolisz.ro/2020/06/08/happy-decennial/ 5eddf7aa17253e7fe6dd617e Mon, 08 Jun 2020 13:26:34 +0300 Well, this has happened too: ten years of blogging. I feel like it's only yesterday that I wrote the 5 year anniversary post. Some things have changed in the meantime, some haven't. I still enjoy writing, but my goals about my blog have changed.

10 years ago I don't think I knew what I wanted from my blog. I posted lots of content, some well thought out, some not at all. Five years ago, my blog was mostly a way to keep in touch with friends and family. Now, I want to expand my reach again. I've made it to the front page of Hacker News this year, with the Moving away from GMail post. That got me 30 thousand page views in three days, more than I had last year in total. The Rust Web Crawler post was also quite successful, getting to more than 1000 views.

This year I have already posted more than in the last six years (this would be the 33rd post in 2020), mostly thanks to joining the 100 Days to Offload and I hope that at least for some of those posts, the quality is much higher than before. In particular, I pay a lot more attention to the order of ideas, I try to group them well, to add headers and so on. For longer pieces, I also ask for feedback from some friends.

As always, I continue having big ideas for my blog. I hope you'll continue reading it, for the next ten years as well!

I’m publishing this as part of 100 Days To Offload - Day 20.

]]>
<![CDATA[ New desk setup ]]> https://rolisz.ro/2020/06/07/new-desk-setup/ 5edc88c317253e7fe6dd610f Sun, 07 Jun 2020 10:34:54 +0300 Last week we were at my in-laws for a couple of days. We got back home late on Thursday. As I went upstairs, I took my backpack containing my work laptop to drop it off in my home office. I didn't bother turning on the light in the office, because I wanted to only put down the backpack and then go to bed. I dropped it off and I wanted to leave the room when I stopped in my tracks. Something was off.

My desk had been a darker cream color, but now there was a white desk in its place. I pivot and take another look, while still in the dark. I turn around and look at my wife who was following close by: she's recording me and she starts laughing.

Turns out she and her family pulled off the best gift surprise for me ever. For legacy reasons (read: was too lazy/cheap), my old desk was actually an IKEA kitchen table, that I had brought back with me from Switzerland. I had bought a big office chair here in Romania, but I never liked it too much. Knowing this, my wife and my in-laws ordered me a new BEKANT desk and a JÄRVFJÄLLET chair from IKEA. They arrived while we were at my in-laws, so she asked a neighbor to pick them up and place them in our house. My brothers-in-law came over one evening and assembled them, taking care to preserve the mess that is on my desk and not cleaning it up. Well organized, my dear wife!

After 2 days of using, I can say I really like my new desk and chair :D It was about time I upgraded to a desk whose height I can adjust.

And I'll end by paraphrasing Homer Simpson:

To many more gifts like these!

I’m publishing this as part of 100 Days To Offload - Day 19.

]]>
<![CDATA[ Quarantine boardgames: Pandemic ]]> https://rolisz.ro/2020/06/02/quarantine-boardgames-pandemic/ 5eb3ab9181fef554fabb4208 Tue, 02 Jun 2020 22:24:43 +0300 The pandemic is "over" in Romania (or at least, the most severe lockdown is over), so it means we won the game of Pandemic :)

Pandemic is a game were there are four diseases ravaging through the world. The players must cooperate to prevent outbreaks and to find cures for the infections.

The little cubes represent diseases

To find a cure, one player must gather five city cards of the disease's color and then travel to a research station in order to prepare the cure. Each player receives some city card at the beginning of their turn, but that's rarely enough, so they have to trade cards among each other. Cards can be traded only in their corresponding cities, so a lot of travel is involved. If the city cards run out, the game is over.

Epidemic cards give you anxiety

On each turn, more and more cities get infected. If a city has three diseases cubes and another one should be added, there is an outbreak there. The disease spreads to all the surrounding cities. If 7 outbreaks happen, the game is over.

If there are not enough disease cubes to place on the map, the game is over.

The little vials represent the cures for diseases

There are many ways to lose the game of Pandemic and only one to win: you must find the cure for all the diseases, but not necessarily eradicate them. The first several times I played we lost the game. The last time I played the game was when the COVID 19 pandemic was already starting, and even though I played with Catalin and we drew diagrams and tried to optimize the last 10 actions, we still lost.

Each player has a different card. You can be a scientist and then you need only four city cards to research a cure. You can be a quarantine specialist and then you prevent the placement of disease cubes around you. You can be a medic and then you can remove all disease cubes with a single action.

The game is a cooperative game, so you share victory and defeat. It's one of the first well known coop game, so it has the problem of quarterbacking: some players who are more assertive easily end up telling everyone what to do, so it requires restrain on their side.

The game is a great way to learn geography: this is how me and my wife now know the biggest cities in Asia and Africa. How else would you know where Kinshasha is?

Pandemic is a really fun game. And rumour has it that the legacy versions are among the best board games ever. But I haven't found yet the board game crew to play with!

Grade: 9

I’m publishing this as part of 100 Days To Offload - Day 18.

]]>