Log in
Seblog.nl

English posts

Introducing dark mode

Like a few others this IndieWebCamp I added a dark mode to my website.

With iOS 13 having a dark mode, which toggles with sunrise and sunset if you want it to, I all of the sudden like to have it on. And yesterday in the train, I noticed that my site felt kind of bright.

So this morning I hacked it together in the train back to Amsterdam. I went the dirty way: just have one media-query for determining dark mode, and then target a lot of elements and classes inside that, and set their color to be something different.

The actual implementation was not that hard (@media (prefers-color-scheme: dark)), but I spent the most time of the traintrip figuring out which darker grayscales should replace the brighter grayscales.

One thing to note here was the tip by Steve Schoger I remembered reading: dark modes are not about just inverting colors. Important elements in your UI should still be brighter than others.

Luckily, I don’t have a very complex UI on my weblog, but it’s worth to note that I took some time to make my month overview pages look nice in both modes. Feel free to compare the two.

Note that I currently don’t have a switch on my site to make you choose, but if your browser / OS tell my site which mode it’s in, my site will adapt to that.

On the topic of scoring in NES Tetris, PAL vs. NTSC

tl;dr: This is nerding out on Tetris scores, and quite frankly, I don't think the conclusion holds. But there are nice tables along the way.

I've been watching Classic Tetris for a few months now, and as a result, I'm also playing it. I am noticeably better after I watched some pro's scoring Tetris after Tetris at level 19, so I keep this pace of watching, playing, watching playing.

There is a problem though: at first I deemed playing at level 18 myself impossible, it's just too fast. Then I noticed how my mind could keep up with the players on the screen, but not with the pieces falling on my screen. Of course, I need more practice, but I have an excuse: I live in Europe.

Turns out old European TVs have a different refresh-rate than Americans. Therefore Tetris on the European PAL systems gets faster in earlier levels than the American NTSC systems. There is a nice article by Tetris Finland about this and other differences between the two versions.

Despite the faster speeds of the PAL version, I found no evidence of the scoring system being different. This means the PAL version is just harder, you just get less points for your hard work. (Yea, I will come back at this.)

I am playing Tetris on a real NES, brought ±15 years ago on a flee market at the local community playground for the insane price of 10 euros. The cartridge of Tetris has cost me more. But to compare myself with the pro's on screen I would have to spend a bit more to import an American NES.

Isn't there a way to calculate the NTSC score based on a PAL score?

Yea, is there a way to calculate NTSC score?

My current, not too impressive high-score on PAL is 174_731 (and yes I will use underscores here). How much is that worth on NTSC?

My first attempt was to look at the maximum scores. Players have been known to 'max out' the game on NTSC. A max out score is a score that is higher than 999_999, at which point the score will freeze on those six nines.

On the PAL version, however, we still have a world record, which is currently set to 758_360 by Joseph Saelee. This video is also a demonstration of how much harder the PAL game gets. (Although Joseph has a talent of making Tetris look easy on all versions.)

With some tweaks in the software you can get Tetris to report past 999_999, so higher scores are known. But for simplicity, let's take the world record and equate it with a NTSC max out score. This gives us ntsc = pal / 758_360 * 999_999, which will give me a NTSC score of 230_406.

But is that accurate?

It's always comparing apples with oranges, but I feel like this formula is a bit too loose. Let's bring in the information of the aforementioned article of Tetris Finland, which gives a nice table that compares the different frame rates, and calculates per level the time it takes, in seconds, to drop 20 spaces. Here is a table with that data:

Level NTSC PAL
9 1.996 sec 1.999 sec
10-12 1.663 sec 1.599 sec
13-15 1.331 sec 1.199 sec
16-18 0.998 sec 0.7998 sec
19-28 0.6655 sec 0.39994 sec
29+ 0.33278 sec as above

Now, the scoring stays the same in both versions, but changes per level. Let's bring that down to a single number per level, let's agree on a perfect score.

The best way of scoring points is to only score tetrises (clear 4 lines at once). Each tetromino is made up of 4 blocks, and the board is 10 blocks wide. A perfect score would be if the Random Number Generator (RNG) gives us only lines and we can stand them up over the full width of the board, resulting in a tetris and an all-clear board each 10 pieces. This is not really achievable, but this would be the best score possible.

According to the Tetris Wiki we get 1200 * (level + 1) for each tetris, and scores are calculated based on the level after the line clear. Let's ignore push-down points. Let's also say that if the drop speed goes below 0.5 sec for 20 spaces the game is over (this makes levels 29 for NTSC and 19 for PAL the kill screens).

Then there is the concept op the start level. When you start at level 1, you go up to level 2 after 10 lines. But when you start at level 18, you get 130 lines before going to level 19, after which you get a new level ever 10 lines again. The Tetris Wiki gives the formula of "(startLevel × 10 + 10) or max(100, (startLevel × 10 - 50)) lines, whatever comes first". This means 100 lines for levels 9-15, then 110, 120 and 130 for 16, 17 and 18.

So I wrote this odd-looking piece of Javascript (because it's my day off and I can do whatever) to list out the various perfect scores for the various level-starts.

const killscreen = 19 // or 29

tetrisScore = level => 1200 * (level + 1)

startlevelLines = level => Math.min(
  (level * 10 + 10),
  Math.max(100, (level * 10 - 50))
)

scoreForLevel = (level, lines) => ({
  score: (Math.floor(lines / 4) * tetrisScore(level)),
  restlines: lines % 4
})

restlevelScore = (level, rest) => {
  if (level >= killscreen) return tetrisScore(level)
  const {score, restlines} = scoreForLevel(level, 10 + rest)
  return score + restlevelScore(level + 1, restlines)
}

perfectScore = level => {
  const {score, restlines} = scoreForLevel(level, startlevelLines(level))
  return score + restlevelScore(level + 1, restlines)
}

const levels = [...Array(killscreen).keys()]
levels.map(level => console.log(`| ${level} | ${perfectScore(level)}`))

I was kind of surprised by the results: as you can see, the world record by Joseph is higher than the perfect score. This is because he had a 100% tetris rate, burned one line in level 18 for 760 points, then tetrised into level 19, and then made a few singles and doubles in level 19. My definition of 'kill screen' seems too strict, but at least it's even on both sides of the table.

Start level NTSC perfect PAL perfect
0 1_332_000 588_000
1 1_334_400 590_400
2 1_340_400 596_400
3 1_348_800 604_800
4 1_360_800 616_800
5 1_375_200 631_200
6 1_393_200 649_200
7 1_413_600 669_600
8 1_437_600 693_600
9 1_464_000 720_000
10 1_478_400 728_400
11 1_454_400 710_400
12 1_462_800 712_800
13 1_432_800 688_800
14 1_435_200 685_200
15 1_399_200 655_200
16 1_429_200 685_200
17 1_461_600 717_600
18 1_497_600 753_600
19 1_536_000
20 1_578_000
21 1_622_400
22 1_670_400
23 1_720_800
24 1_774_800
25 1_831_200
26 1_891_200
27 1_953_600
28 2_019_600

Given that my high score was a level-5-start score of 174_731, I should use ntsc = pal / 631_200 * 1_375_200, giving me a NTSC score of 380_687, a lot more than my previously calculated 230_406. But is this fair now?

Score per second

The table above is not by any means a fair comparison. It does account for the higher scoring potential in NTSC, but it still compares levels regardless of the speed they are played at. Let's combine the previous two tables into one.

So in the PAL version, you get less points, but also less time to think about how to score them. In that regard, the PAL version is harder, and the points should be worth more.

In the next table, I've taken the perfect score per level, and divided it by the thinking time you have for it (the seconds of the earlier table times 240, the number of lines before transition you get when you start in level 29). The result is a series of numbers that say how 'easy' it was to get that number of points.

Start level NTSC score sec / 20 lines modifier
9 1_464_000 1.996 3.0561122244
10 1_478_400 1.663 3.7041491281
11 1_454_400 1.663 3.6440168370
12 1_462_800 1.663 3.6650631389
13 1_432_800 1.331 4.4853493614
14 1_435_200 1.331 4.4928625094
15 1_399_200 1.331 4.3801652893
16 1_429_200 0.998 5.9669338677
17 1_461_600 0.998 6.1022044088
18 1_497_600 0.998 6.2525050100
19 1_536_000 0.665 9.6240601504
20 1_578_000 0.665 9.8872180451
21 1_622_400 0.665 10.1654135338
22 1_670_400 0.665 10.4661654135
23 1_720_800 0.665 10.7819548872
24 1_774_800 0.665 11.1203007519
25 1_831_200 0.665 11.4736842105
26 1_891_200 0.665 11.8496240602
27 1_953_600 0.665 12.2406015038
28 2_019_600 0.665 12.6541353383
Start level PAL score sec / 20 lines modifier
9 720_000 1.999 1.5007503752
10 728_400 1.559 1.9467607441
11 710_400 1.559 1.8986529827
12 712_800 1.559 1.9050673509
13 688_800 1.199 2.3936613845
14 685_200 1.199 2.3811509591
15 655_200 1.199 2.2768974145
16 685_200 0.799 3.5732165207
17 717_600 0.799 3.7421777222
18 753_600 0.799 3.9299123905

This would say an NTSC max-out on a level 18 start is 999_999 / 6.25250501, so has a difficulty of 159_935. The world record on PAL is 758_360 / 3.9299123905, so has a difficulty of 192_971. To me this feels believable, because Joseph has had many beyond-max-out games. We don't know their scores, because the game does not tell us, but their difficulty might also approach 200k.

And my score? Well, as I mentioned somewhere it was a level-5-start score, and as you can see in the first table: there is not much difference between level 9 on PAL and NTSC, and level 5 is not even listed. I should really stop whining and just practice more.

Apples and oranges

If you actually read the article by Tetris Finland you already would know: the speed is not the only difference. There are also differences in the way DAS works in PAL vs NTSC. The article concludes that these games are therefore very different, so different that you might consider them entirely different titles, despite looking the same.

It was fun figuring out these numbers and formula's, but in the end, you really can't compare the two. Gotta fix myself an NTSC somewhere if I want to compare my progress to the pro's.

Note that this difficulty number is a thing I invented in this post, and it is in itself not comparable to Tetris scores. But you can compare it to other difficulty numbers, if you calculate them the same way. Actually, I don't really think this whole thing is reliable at all, but I didn't want to throw a whole morning of calculating and writing out of the window. Some of the tables bear more value than others.

Some other weird things about falsy Javascript:

false == false
0 == false
[] == false
"" == false
"0" == false
"\t" == false
[] == false
["0"] == false
[[["0"]]] == false
[[[1 - 1]]] == false
[[[{}.foobar]]] == false
[[[`\t ${[].length} \t`]]] == false

// BUT

[false] != false

So, yesterday I learned that [] == [] is false in Javascript. Today my adventures with arrays lead me to this:

const coffees = ['espresso', 'latte', 'filter']
console.log(
  'filter' in coffees,
  'cappuchino' in coffees,
  'espresso' in coffees
)

// => true, false, false
als antwoord op Saša Jurić

You need a certain confidence in writing your own authz-code. Part of the value of the package is verfication that it is a proved pattern. I think that would especially apply to auth(n), as we are thaught not to roll our own. But this seems very straightforward once you read it.

als antwoord op Sebastiaan Andeweg

Ah, I think I got myself an answer: the const variant does not allow re-definition of the function, while the function variant does. I still don't think the benefit that brings outweighs the ugliness of it. Can I have a new keyword like fn or something to solve this?

I don't understand why people prefer arrow-functions in Javascript in the following context:

const name = () => stuff();

vs

function name() {
  stuff();
}

... they are ideal for callbacks and map-operations, but when I want to declare a function, I want to see that keyword.

als antwoord op Raj Chauhan

Feels even more useful with reduce():

console.log([
  x => x + 36,
  x => `The answer is ${x}!`,
  x => x.toUpperCase(),
].reduce((x, fn) => fn(x), 6));
// THE ANSWER IS 42!
als antwoord op JohnB

Oh, no I got links to neither... I was not actually planning on sending this in to the contest, it feels too simple. But I might clean it up and send it in, and then those links will come. (Also: what is the licensing model behind Tetris?)

Mobile is hard, btw, because Tetris relies on buttons so much. I can add some buttons to the page, but I'm afraid the game play won't be as good. Most mobile Tetrises I've seen do fancy things with gestures, but it always feels odd to me. It's just not a touch game :)

So, I've been addicted to Tetris for the last week or so. Last night I decided to do something else, and when I came across the Phoenix LiveView contest, I thought: why not try and make Tetris? #myelixirstatus

I was surprised by how far I got. I have watched videos of people coding games before, have been coding myself for years and years, but somehow games felt out of my league. But here, after a (long) evening, I got a game! I should've streamed it myself.

I mean I'm standing of shoulders of giants. I got so much for free from Erlang/Elixir, LiveView to hook it up, @chris_mccord's Snake example gave me the basic idea of using just <div>'s for blocks, the browser is doing the drawing and key-repeats for me.

But it felt magical, once the game got playable. I was hooked again to my own creation. And so many "features" from NES Tetris like sliding, tucking and spinning "just worked" in my first implementation (probably because I was close to their implementation).

It all started with this HTML based board, using flexbox to to the hard works of blocks for me. I later wrapped the board in a Game-struct, and added bindings for the scores.

How to get a game loop in Elixir? Just send yourself a message after, say, 500ms. The LiveView component has a handle_info/2, which queues the next tick and moves the current Tetromino down.

Thanks to the phx-keydown="keydown" in the template, we get messages in handle_event/3 for each key. Just delegating to my Game module. And yes, I use Vim, so I need those H, J and L. (Dropping with K is not implemented.)

My Game.move/2 got a bit complicated, some refactoring is in place. But I started with a Tetromino struct, with :x and :y keys and a :color. To move it down, you paint the current :x and :y :white, and then paint :y + 1 to the :color. Code below reduces for all four points:

To obtain all the points I got this wonderful Tetromino.points/1 function. It's ugly and was a pain to write out and get right, but it works like a charm.

Colors and rotations work similar, with Tetromino.rotate/1. (color_for_type/1 gets called in new/1, and it never changes once it is created.)

Then the messy Game.move/1: can you move down? Move down. Otherwise, call Board.clear_lines/1 to remove full lines and obtain the score, get a new random Tetromino and put it on the board. The game-over handling is a bit buggy still, I had only one evening.

Clearing the board is simple: reject rows of which all cells are not empty and use counts to add new rows to the top. The Board.color_at/2 gives the color for that particular cell. The guards against negative numbers prevent List.pop_at/2 to get items from the back of the list :)

I think that's mostly it! Again, I wish I had streamed it, it was very weird watching myself doing it, but also very rewarding. Keep coding, people, you can do this too!

als antwoord op Caleb Porzio

Hah, I thought you made an app to digitally manage the stickers on your laptop. That widget you got is so close. Plz provide a way to import stickers ;)

als antwoord op Sebastiaan Andeweg

The only downside I see now is that you are binding your components to your GraphQL schema, but since that has reasonable deprecation management I think I'm okay with that.

als antwoord op Chris Toomey

Thanks! I'm considering introducing it to a project. It uses JS and Vue, but I think it can still be a match. A lot of props get passed around through many components, and this could be a way to just pass the whole object but still make sure all the fields are present.

Surf the web at lightning speed

I am at an airport, and I just saw a booth where I could access the web for free if I had the right card, or for a certain amount of euros per time unit. I saw the Microsoft Internet Explorer logo. All the computers where available, except for one. I assumed the girl that was using it worked there. Why else would you use a place like that?

It made me realise how much the web has changed. In another era, there were publishers, who put information on paper. These papers could then be brought by those who were interested in the information. It was a publishers world. The story has been told many times: the internet changed that, information became free, anyone could publish. But the web has evolved once more.

Why would I not want to use a computer in that booth even if it were free? Because I have no idea what to do on the web. Do I go to some news sites? Do I look up the time of my flight again? It would only be to kill time. Most of the public information is boring, the real fun is on Twitter, Facebook or maybe even my e-mail, where the information is tailored to my tastes. And no, I would not feel okay with entering my password into one of those things.

I should probably not call the social media part of the internet ‘the web’. It is partly not accessable without logging in, not all parts have clear URLs, it depends on a few giant websites. But there’s more fun on those silo’s, or at least: there is more of that promised content, published by anyone, everyone, especially your friends. And the best way of viewing it, is by authenticating myself as me. Luckily this is not a problem: I have a device in my pocket that is connected and authenticated 24/7. Compared to that, the web is a dull place, with unpersonal information.

Will the web make place for these giant silo’s? Is the time of publishing on your own site over? I hope not. I’m experimenting with this on this website, which is my personal website. There is a link in the upper-right corner which says ‘log in’. That is not a link for me, that is a link for you. You can log in using various methods (ok, only IndieAuth and Twitter are supported at the time of writing). After you are logged in you will see more posts, like my checkin into the airport. If I know you and shared a post especially with you, you will see those as well.

We still need some work on staying authenticated, preferably to one app that collects private posts for you, as you. But there is more web in this approach than we have in social media. And that is nice. I like more web. The web is exciting. Let’s not let it stay boring with only public, general information. Let’s share the personal here too. And let’s create a way to do that in a more private way, where you control who sees your posts. See you on the IndieWeb!

GraphQL Stitching versus Federation

Recently, I started working on a project that uses GraphQL Stitching, to bring multiple GraphQL Schema's from backend servers into one central Schema to consume for clients. Sadly, I cannot say I'm a fan of it. In researching how it all works, I found out that it was actually deprecated, and by digging a bit deeper, I found out that that happened last Thursday. I happily investigated the proposed alternative of Apollo Federation.

What's wrong with Stitching?

Let's first dive into what I don't like about Schema Stitching. I must admit that I'm no expert on the subject: I am still trying to grasp what's possible. And it seems like a lot is possible. One can grab fields from one service, enhance them with fields from other services, leave fields out, all kinds of things.

For example. If I have a backend service that does books, it could have a Schema like the following:

type Query {
  books: [Book]
  book(id: ID!): Book
}

type Book {
  id: ID!
  isbn: String!
  title: String
}

If you then have a backend service that does reviews of books, it could have a Schema like the following:

type Query {
  reviewsForIsbn(isbn: String!): [Review]
}

type Review {
  text: String
  stars: Integer
}

The thing you can do with Stitching, is that you can add a reviews field to the Book type, by using the isbn field on Book and using reviewsForIsbn(isbn:) to grab it. And this is already the part where I should leave out the details of how to do it: I have no idea, but I know it's possible, and even in weirder setups than this one.

The nasty thing about it is that it creates a dependency on both the isbn and reviewsForIsbn fields. This dependency is not visible from either backend service, only in the code of the stitching service. And since GraphQL is all about callbacks and resolver functions, that code in the stitching service can become difficult to read, especially if you do a lot of stitching.

I think the term stitching is excelent: if you keep stitching Schemas together, you will get a giant stitchwork full of yarn and everything will be connected to everything. And that's a bad thing, because it ties you down: you can't move a thing, if you pull one cord out, the whole thing might become undone.

With great power comes great responsibility. I think stitching is very powerful, but it's also very easy to shoot yourself in the foot with. As said: I was happily surprised to find out I wasn't alone and that it has been deprecated by Apollo. (Who, by the way, are the only GraphQL library that supported it.)

The new fancyness: Apollo Federation

How to bind multiple GraphQL Schemas together if you can't stitch them? Apollo's new answer is Apollo Federation, which is a pattern of defining Schemas, and a service called Apollo Gateway. Short summary: the Gateway reads the federated Schemas, and based on the information they provide, it stitches it all together, without you having to write any code in the stitching layer.

This information is given in the form of type anotations and some callbacks, that are described in a specification. For Javascript, you can just use the @apollo/federation package. For libraries (such as Absinthe for Elixir) you have some work to do.

But let's review the previous two services again, but now using Federation. Federation makes use of a few directives, which are designed to give more information to the query executor. They begin with an @ sign and can have parameters.

Consider the Books service again:

expand type Query {
  books: [Book]
  book(id: ID!): Book
}

type Book {
  id: ID!
  isbn: String!
  title: String
}

Not much changed here, other than the expand keyword in front of the Query type. The Books service, in this simple example, does not need any external data. The Reviews service, however, does:

expand type Query {
  reviewsForIsbn(isbn: String!): [Review]
}

type Review {
  text: String
  stars: Integer
}

expand type Book @key(fields: "isbn") {
  reviews: [Review] @external
}

There is some stitching here. The Reviews service knows about Books, but that is fine: a review would not makes sense without an object to review, in this case, books. Notice how, in the Schema, it is defined that, in order for the Reviews service to resolve the Book type, it needs an isbn. Seen from the existing Book type, the Review is then an @external type, which is provided by the Review service. Note that we could drop the reviewsForIsbn entirely now, but still maintain reviews on Book.

Some questions I think I have answers for but am not sure about:

  • Why add a Book type to the Reviews service? Because someone has to know, and I prefer it to be the service that has the other entity next to it's domain.
  • How does the Gateway know to call reviewsForIsbn? Well it doesn't: given the type and the requested @key(fields:), and some underwater endpoint, the Reviews service is now responsible to find reviews that match that type (Book) and key, and it can have a resolver for that.
  • Why can't the Books service just provide the full Book type? Because then knowledge about the link between Books and Reviews is in both services.

I don't pretend to understand all the implications of both approaches, but intu¬tively, this second approach seems better to me. There is no code that we have to write in the Gateway anymore, and the Schema is split up between the services, that only know their own part about the shared entities.

Downsides: gotta bring it to Absinthe

Unfortunately, like with Stitching, Apollo for Javascript is the only library that supports this thing at the moment. As mentioned, there is a spec describing what to do to support this elsewhere (still using the Apollo Gateway), but it is very early days.

I tried some stuff with Absinthe, and was happy to see macros for directives. After some exploration, however, I came to the conclusion that the directives you can define with these, are only useable in the Schema you put out, and thus in your queries. Since the Schema itself is defined with macros in a DSL, I cannot use the directives there. I will have to set up my own macros for it, and that is it's own rabbit hole.

Moreover: in the current release of Absinthe, there is not yet a way to get your parsed Schema out. This Schema, however, has to be send to the Gateway, for it to work. I tried working around it by defining the Schema twice (by hand and with the DSL), but there are still a lot of rabbit holes in the union _Entity (which consists of all types that use the @key directive) and scalar _Any (which maps to all @external types).

With Stitching, all the work is done in the external service, so your backend services do not have to know about it. With Federation, all your backend services have to be aware that they are part of something bigger in order to participate in that bigger picture.

Still, I really like the idea, and I think it will hold better than stitching all the things manually.

Meer laden