Infinity Whisky Bottle – Good Idea?

I am not going to explain what an Infinit Bottle is. Use Google.

And I am not ready to just planlessly add my whisky remains to a bottle either. But I will experiment a little.

Motörhead + Storm

Motörhead and Storm are two whiskies in the lower part of my head-to-head ranking. I am almost out of of both of them and I am bored of them, so they could be good candidates for going into an Infinity bottle. Storm is too dry and bodyless. Motörhead is too sweet. Do they taste better if mixed?

Color: quite obviously the mix falls between the pale Storm and the dark Motörhead. I am not goint to argue that the color of the mix is more appealing.

Aroma: Motörhead has a very soft bourbon aroma. Storm has little aroma, a bit chemical. The mix is, quite obviously more balanced. It is much more promising than Storm, and not as in-your-face as Motörhead.

Taste: Being kind, Storm does not taste so bad. For being a blend it has some quality. Over to Motörhead, there are some very sweet dominant flavours that are not entirely nice. How about the 50/50-mix? Well, not too surprisingly it has less of the bad stuff than both the original whiskies. But it also has less of their characters – for good and for bad. In this particular case, my opinion is that it was more good than bad.

Conclusion: MotörStorm is better than both Motörhead and Storm

PHP validation of UTF-8 input

Last weeks I have done some PHP programming (my web hotel where I run wordpress supports PHP, and it is trickier to run Node.js on a simple web hotel). I like to do input validation:

function err($status,$msg) {
  http_response_code($status);
  echo $msg;
}

if ( 1 !== preg_match('/^[a-z_]+$/',$_REQUEST['configval']) ) {
  return err(400,'invalid param value: configval=' . $_REQUEST['configval']);
}

Well, that is good until I wanted a name of something (like Düsseldorf, that becomes D%C3%BCsseldorf when sent from the browser to PHP). It turned out such international characters encoded as Unicode/UTF-8 can not be matched/tested in a nice way with PHP regular expressions.

PHP does not support UTF-8. So ü in this case becomes two characters, neither of them matches [A-Za-z] or [[:alpha:]]. However, PHP can process it as text, use it in array keys, and output valid JSON without corrupting it, so not all is lost. Just validation is hard.

I needed to come up with something good enough for my purposes.

  • I can consider ALL such unicode characters (first byte 128+) valid (even though there may be strange characters, like extra long spaces and stuff, I don’t expect them to cause me problems if anyone bothers to enter them)
  • I don’t need to consider case of Ü/ü and Å/å
  • I don’t need full regexp support
  • It is nice to be able to check length correctly, and international characters like ü and å counts as two bytes in PHP.
  • I don’t need to match specific characters in the ranges A-Z, a-z or 0-9, but when it comes to special characters: .,:,#”!@$, I want to be able to include them explictly

So I wrote a simple (well) validation function in PHP that accepts arguments for

  • minimum length
  • maximum length
  • valid characters for first position (optional)
  • valid characters
  • valid characters for last position (optional)

When it comes to valid characters it is simply a string where characters mean:

  • u: any unicode character
  • 0: any digit 0-9
  • A: any capital A-Z
  • a: any a-z
  • anything else matches only itself

So to match all letters, & and space: “Aau &”.

Some full examples:

utf8validate(2,10,’Aau’,’Aau 0′,”,$str)

This would match $str starting with any letter, containing letters, spaces and digits, and with a length of 2-10. It allows $str to end with space. If you dont like that, you can do.

utf8validate(2,10,’Aau’,’Aau -&0′,’Aau0′,$str)

Now the last character can not be a space anymore, but we have also allowed – and & inside $str.

utf8validate_error

The utf8validate function returns true on success and false on failure. Sometimes you want to know why it failed to match. That is when utf8validate_error can be used instead, returning a string on error, and false on success.

Code

I am not an experienced PHP programmer, but here we go.

function utf8validate($minlen, $maxlen, $first, $middle, $last, $lbl) {
  return false === utf8validate_error($minlen, $maxlen,   
                                      $first, $middle, $last, $lbl);
}

function utf8validate_error($minlen, $maxlen, $first, $middle, $last, $lbl) {
  $lbl_array = unpack('C*', $lbl);
  return utf8validate_a(1, 0, $minlen, $maxlen,
                        $first, $middle, $last, $lbl_array);
}

function utf8validate_utfwidth($pos,$lbl) {
  $w = 0;
  $c = $lbl[$pos];
  if ( 240 <= $c ) $w++;
  if ( 224 <= $c ) $w++;
  if ( 192 <= $c ) $w++;
  if ( count($lbl) < $pos + $w ) return -1;
  for ( $i=1 ;$i<=$w ; $i++ ) {
    $c = $lbl[$pos+$i];
    if ( $c < 128 || 191 < $c ) return -2;
  }
  return $w;
}

function utf8validate_a($pos,$len,$minlen,$maxlen,$first,$middle,$last,$lbl) {
  $rem = 1 + count($lbl) - $pos;
  if ( $rem + $len < $minlen )
    return 'Too short';
  if ( $rem < 0 )
    return 'Rem negative - internal error';
  if ( $rem === 0 )
    return false;
  if ( $maxlen <= $len )
    return 'Too long';

  $type = NULL;
  $utfwidth = utf8validate_utfwidth($pos,$lbl);
  if ( $utfwidth < 0 ) {
    return 'UTF-8 error: ' . $utfwidth;
  } else if ( 0 < $utfwidth ) {
    $type = 'u';
  } else {
    $cv = $lbl[$pos];
    if ( 48 <= $cv && $cv <= 57 ) $type = '0';
    else if ( 65 <= $cv && $cv <= 90 ) $type = 'A';
    else if ( 97 <= $cv && $cv <= 122 ) $type = 'a';
    else $type = pack('C',$cv);
  }

// type is u=unicode, 0=number, a=small, A=capital, or another character

  $validstr = NULL;
  if ( 1 === $pos && '' !== $first ) {
    $validstr = $first;
  } else if ( '' === $last || $pos+$utfwidth < count($lbl) ) {
    $validstr = $middle;
  } else {
    $validstr = $last;
  }

  if ( false === strpos($validstr,$type) ) {
    return 'Pos ' . $pos . ' ('
         . ( 'u'===$type ? 'utf8-char' : pack('C',$lbl[$pos]) )
         . ') not found in [' . $validstr . ']';
  }
  return utf8validate_a(1+$pos+$utfwidth,1+$len,$minlen,$maxlen,
                        $first,$middle,$last,$lbl);
}

That is all.

Tests

I wrote some tests as well.

$err = false;
if (false!==($err=utf8validate_error(1,1,'','a','','g')))
  throw new Exception('g failed: ' . $err);
if (false===($err=utf8validate_error(1,1,'','a','','H'))) 
  throw new Exception('H should have failed');
if (false!==($err=utf8validate_error(3,20,'Aau','Aau -','Aau','Edmund')))
  throw new Exception('Edmund failed: ' . $err);
if (false!==($err=utf8validate_error(3,20,'Aau','Aau -','Aau','Kött')))
  throw new Exception('Kött failed: ' . $err);
if (false!==($err=utf8validate_error(3,20,'Aau','Aau -','Aau','Kött-Jan')))
  throw new Exception('Kött-Jan failed: ' . $err);
if (false!==($err=utf8validate_error(3,3,'A','a0','0','X10')))
  throw new Exception('X10 failed: ' . $err);
if (false!==($err=utf8validate_error(3,3,'A','a0','0','Yx1')))
  throw new Exception('Yx1 failed: ' . $err);
if (false===($err=utf8validate_error(3,3,'A','a0','0','a10')))
  throw new Exception('a10 should have failed');
if (false===($err=utf8validate_error(3,3,'A','a0','0','Aaa')))
  throw new Exception('Aaa should have failed');
if (false===($err=utf8validate_error(3,3,'A','a0','0','Ax10')))
  throw new Exception('Ax10 should have failed');
if (false===($err=utf8validate_error(3,3,'A','a0','0','B0')))
  throw new Exception('B0 should have failed');
if (false!==($err=utf8validate_error(3,3,'u','u','u','äää')))
  throw new Exception('äää failed: ' . $err);
if (false===($err=utf8validate_error(3,3,'','u','','abc'))) 
  throw new Exception('abc should have failed');
if (false!==($err=utf8validate_error(2,5,'Aau','u','Aau','XY')))
  throw new Exception('XY failed: ' . $err);
if (false===($err=utf8validate_error(2,5,'Aau','u','Aau','XxY')))
  throw new Exception('XxY should have failed');
if (false!==($err=utf8validate_error(0,5,'','0','',''))) 
  throw new Exception('"" failed: ' . $err);
if (false!==($err=utf8validate_error(0,5,'','0','','123'))) 
  throw new Exception('123 failed: ' . $err);
if (false===($err=utf8validate_error(0,5,'','0','','123456')))
  throw new Exception('123456 should have failed');
if (false===($err=utf8validate_error(2,3,'','0','','1'))) 
  throw new Exception('1 should have failed');
if (false===($err=utf8validate_error(2,3,'','0','','1234'))) 
  throw new Exception('1234 should have failed');

Conclusions

I think input validation should be taken seriously, also in PHP. And I think limiting input to ASCII is not quite enough 2020.

There are obviously ways to work with regular expressions and UTF8 too, but I do not find it pretty.

My code/strategy above should obviously only be used for labels and names where international characters make sense and where the form of the input is relatively free. For other parameters, use a more accurate validation method.

Air Coolers: Arctic Air & Evapolar

A few weeks every summer it gets uncomfortably hot indoors (where I live). I have no aircondition in my home and I can not easily install one either.

There are devices called air coolers. I have two from Arctic Air and two from EvaPolar. Would I recommend them?

First, lets quickly discuss the concept of creating cooling in a warm room.

A FAN creates an air flow without cooling the air. As long as the air is significantly cooler than your body (37C) you will experience a cooling effect (perhaps even when the air is warmer – I have little experience). If the air is 25C it will tend to make your skin 25C. If a lot of air passes by your skin that effect will be significant, but if the air is completely still it is a much slower process.

An AIRCONDITION works like a refridgerator. It is a machine that takes in air of one temperature and outputs air in two different streams: one cooler and one warmer. The warmer stream must be removed, and the cooler stream is sent into the room (or refridgerator). This costs energy (which is heat), so the warm stream gets more warm than the cold stream gets cold. It is important to understand that a refridgerator (or aircondition) in a closed space heats that space. A refridgerator left open makes your room warmer. An AC that does not send (the) warm air out of your home makes your home warmer. Properly installed and at a high energy cost an AC can truly cool a room or a home.

An AIR COOLER turns water into water vapour. That comes at an energy cost, but that energy is drawn from the air, effectively making the air cooler. To speed up this process an Air Cooler has thin wet membranes (large water-air contact area) and a fan (so the air around the membranes is constantly warm and dry). In theory this is very smart. In practice the effect is limited (but real). Apart from the fan, and air cooler does nothing different from just hanging your wet laundry to let it dry.

Humidity

An air cooler raises the humidity of the air. With my understanding of physics I would say that the energy content of the air is the same, but the temperature is lower and the humidity higher. The human body cools itself by sweating (evaporating liquid on the skin) and this process is more effective in dry air. So when considering (evaporating) air coolers I think it is important to understand that the higher humidity can make it feel warmer.

If you live in a dry place (relative humidity below 50%) you may find that the Air Cooler is good. Dry air is not nice for your skin, nose and throat. If the humidity is already very high (above 75%) the air cooler may be of no use whatsoever.

Typical Effect

It is hard to make exact and scientific experiments in your own home. Some days are warmer, sunnier, more windy or drier. I wish I could tell you that “on the week without air coolers the average temperature was 23C, and on the week with air coolers the average temperature was 21C”, but that would require large scale and controled tests.

However, a typical air cooler (Arctic, or EvaPolar) uses a few liters of water every day (so it needs to be refilled). They have different speeds, which produce different noise and different effect. On medium speed you could expect:

  • Air in: 24C, 60% humidity
  • Air out: 21C, 75% humidity (measured just in front of the machine)

If you use it as a fan – directing the cool air towards you – you feel much cooler than with no fan. If you leave it on 24×7 in a room, I would guess the effect is not insignificant, but it is nothing like an AC.

Arctic Air vs Evapolar

I first bought to Arctic Cooling units. Later I bought two Evapolar units (one evaCHILL and one evaLIGHT).

Evapolar units are more expensive. The build quality feels good, and the evaLIGHT also has thermometers for in/out air and a few other features.

What I can say is that Evapolar units are significantly more quiet. So whenever noise is a problem they give you more cooling (like when you sleep or don’t want a loud fan noise).

Evapolar indicates on their web page that they use some hightech membrane materials that gives advantages. My impression is that the Evapolar units have a stronger air flow and drink more water during a day (at similar power consumption ~5VA).

So even though an Evapolar costs 2-5x more than the (cheap) competition, if noise matters to you, I can imagine you can get 2-5x more cooling from it on a typical day.

Power Consumtion

All units I have run on USB 5V and use from 0.3A to a little more than 1A depending on speed. This means you CAN run them from a PowerBank, a computer, or most any USB charger.

My Arctic Air units came without power supplies. evaCHILL comes with a USB-C-connector (but it runs on 5V, not 20V as is standard for USB-C), I doubt the USB-C-standard allows this. So don’t connect your evaCHILL to a 20V laptop charger.

Replacing membranes

The aircooler has membranes that absorb water which is evaporated. Only the water evaporates and inpurities remain in the membranes. So you will need to replace the membranes (at some cost) eventually.

In my case these units will be in use only a few weeks per year so I expect them to last a few years without changing membranes. I also have access to very pure tap water.

Conclusion

If aircondition is not a possibility and humidity is reasonaby low, and Air Cooler is probably the best you can do. If you prefer a more silent unit go for quality (Evapolar) rather than the much cheaper alternatives.

If all you want is a cool experience at your workplace (desk) a fan might be good enough – and in that case it will be cheaper, more silent and requre less maintenance. However an air cooler will give you a slightly cooler air flow.

Goodbye One.com – Hi Inleed.se

I have been running this site on One.com for a few years. Yesterday I moved it to Inleed.se. Details about the moving of wordpress are found here.

On One.com

One.com has served me well. For a hobby non-profit site with a couple of dozen visitors per day the smallest package possible was fine (at about 50 Euros per year, including a domain).

A few weeks ago One.com informed me about new packages, and my little plan was being converted to the new little plan. At first it seemed fine: same price and more storage space (I use very little).

However, the day after the upgrade, I could not SSH into the server to edit a few files. It turned out SSH was no longer included in the most basic plans anymore. In a market economy, One.com can package their services the way they want. But

  • I had a feature (SSH) since years, it was removed from me
  • To get this feature back, my costs were doubled
  • No long term fix for an old customer was offered
  • SSH costs nothing to offer, SFTP was still available, so One.com had put effort into making my little plan less useful.

I use SSH (and linux shells) for everything. Production, test, development, professional servers, hobby servers, workstations, laptops, macOS, Linux, Windows, configuration, programming, and other work. It is just unproductive to not use SSH to

  • edit text files (.html, .js, .php, .htaccess, and so on)
  • check and fix file permissions
  • pack/unpack files
  • manage folders and files

So, as a customer, after expressing my dissatisfaction and getting no long term solution, I vote with my wallet and find another hosting company.

On Inleed.se

I have been a customer of Inleed.se for a few days so I can’t really write a review. But what I immediately notice is that compared to One.com

  • cheapest package is half price
  • …and includes much less storage
  • I get a cgi-bin folder (I don’t think One.com offered that, but not completely sure)
  • I get more than one database if I need
  • I can use SSH 🙂
  • … and there some kind of Node.js support: very interesting, I need to look into it!

So far so good!

Move WordPress to new Domain

I gave up my old web hotel (one.com) and moved to a new one (inleed.se) (read more about why here). As a wordpress blog owner, not being very familiar with MariaDB, Apache and PHP this can seem a bit scary.

However, it was quite fine. With a new web hotel and a database ready, this was basically the tools/steps required.

  1. Use WordPress Plugin Duplicator to produce a complete backup (a downloadable zip file) and a downloadable installer (installer.php)
  2. Configure .htaccess on new server to forward requests to a wordpress folder
  3. Upload backup-zip-file and installer.php to wordpress folder on new server
  4. Run installer (go to http://newsite/installer.php), follow instructions
  5. Use WordPress Plugin Velvet Blues Update Urls to make sure all my links point to newsite rather than oldsite.
  6. Create a .htaccess on old server to permanently forward traffic to new domain

I ended up doing this thing twice, learning the first time and perfecting it the second time.

.htaccess on new server

The purpose of this is to place wordpress in its own directory, while still not needing to expose that directory in the URLs.

<IfModule mod_rewrite.c>
RewriteEngine on
RewriteCond %{HTTP_HOST} ^(www.)?techfindings.net$ 
RewriteCond %{REQUEST_URI} !^/techfindings/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /techfindings/$1
RewriteCond %{HTTP_HOST} ^(www.)?techfindings.net$ 
RewriteRule ^(/)?$ techfindings/index.php [L]
</IfModule>

So, wordpress is entirely installed in a directory named techfindings, but behaving like it was in my root. Any other page is served normally.

.htaccess on old server

I don’t want people to access the old site when the new site is up. This was a pretty simple and effective .htaccess file:

RedirectPermanent / https://techfindings.net/

This will be in place as long as my old domain is valid and hosted on the old web hotel.

Conclusion

Moving wordpress from one domain and server to another domain and server is perfectly possible with a good result.

Chrome OS: Bad time in Linux container

I have an Acer R13 Chromebook running Chrome OS 83.0, and most actual work I do on this computer is development work inside the Linux environment.

I realized the other day that the time in the container was bad. Not just a little off, but several days behind. It seems when the computer sleeps/hibernates (which it does a lot) the Linux time “stops” and it does not leap forward to current time when the computer awakes.

I made a few attempts with ntp inside Linux but I don’t think it is meant to be used that way. I have also failed doing “$ sudo date -s 11:00:00”.

So, what make the time right in Linux again is:

  1. Restart the computer (doh), or
  2. Restart the container
    1. CTRL-ALT-T
    2. > vmc stop termina

In both cases your current work is interupted. So preventing your Chromebook from sleeping, or just shutting it off when not using it may be the most straight forward fix.

I changed the power settings so it does not sleep by itself.

Bästa versionen av Dungeons & Dragons?

Den bästa versionen av Dungeons & Dragons är den jag just nu spelar med mina vänner.

Den bästa versionen av Dungeons & Dragons är också den omåttligt populära och aktuella versionen (5e). Och så resonerade även jag när jag skulle köpa D&D för ett tag sedan. Men, vad skulle inte vara bäst med 5e?

  • 5e finns inte på svenska
  • 5e är inte den enklaste versionen att lära sig och att spela
  • 5e kostar ca 1500kr för Players Handbook, Dungeon Masters Guide och Monsters Manual (det finns billigare starlådor, men om du vill fortsätta spela så räcker de inte)
  • Smaken är som baken: olika spelare gillar olika versioner olika mycket, så det kan hända att just du och din grupp faktiskt skulle föredra en annan version.

Låt mig ge en förenklad bild av olika versioner av D&D (se wikipedia för detaljer).

1974-1977-2000-2008-2014-
Original D&DClassic D&D
Advanced D&D (1e, 2e)
D&D 3eD&D 4eD&D 5e

Om man frågar sig vilket spel som är enkast men också genomarbetat och bra, så blir svaret Classic D&D. Det heter såklart bara Dungeons & Dragons, men det brukar kallas Classic eller Basic för att skilja ut det.

En rolig sak med Classic är att det släpptes på svenska i slutet av 1980-talet av Titan Games.

Dessa två boxar täcker spel för rollpersoner med graderna 1-14. På engelska finns ytterligare två boxar (Companion och Master Rules för graderna 15-36). Alla dessa fyra boxar finns samlade i en enda bok som heter Rules Cyclopedia som släpptes 1991.

I mångas ögon var och är detta den bästa versionen av D&D. Men produktionen lades ner i början av 1990-talet och självklart blev produkterna allt svårare att få tag i. Därför uppstod en rad retro-kloner, spel som inte heter Dungeons & Dragons, men som i väldigt stor utsträckning är samma spel – och som är kompatibla när det gäller äventyr och annat. Så för dig som vill spela “Classic D&D” i Sverige idag 2020 finns flera bra alternativ:

  • Rules Cyclopedia (print-on-demand eller PDF från dmsguild.com)
  • Monster & Magi (en svensk retroklon som finns på Bokus och Adlibris eller gratis nedladdning)
  • Svärd & Svartkonst (en svensk retroklon som är ännu enklare och inte så nära originalet)
  • Basregler och Expertregler från Titan Games (begagnat)
  • …eller någon av en stor mängd retrokloner på engelska, både i tryck eller som (gratis) nedladdning

Tvärt emot vad det först kan verka, så är detta inte på något vis en återvändsgränd för mycket nöje med D&D.

  • dmsguild.com säljer alla gamla D&D-äventyr som PDF (på engelska)
  • Monster & Magi erbjuder flera äventyr
  • Svärd & Svartkonst erbjuder flera äventyr

Är Classic bättre?

Jag har precis börjat spela Classic igen (Titan Games D&D) efter att ha spelat 5e ett tag, med blandade känslor. Det återstår att se om gräset är grönare på andra sidan. Det som talar för Classic är att det är ett enklare och snabbare spel. Om jag skulle spela med unga människor i Sverige så skulle jag absolut välja D&D Classic på svenska.

Det finns också en trend som heter OSR (Old School Revival) som handlar om att hitta tillbaka till rollspelens ursprung. Utan att gå in på detaljer så är min mening att rollspelen blev mer och mer avancerade under 80-talet och 90-talet – det fanns såklart en efterfrågan från spelare, och en marknad att tjäna pengar på. Sedan försvann intresset för bordsrollspel mycket på 2000-talet, och jag tror en bidragande orsak var dataspel (Diablo, World of Warcraft). Ett dataspel är alltid överlägset ett rollspel när det gäller möjligheten att hantera taktisk realism och djup på ett snabbt sätt. Men till slut ersätter inte dataspelen helt rollspelen – det är en annorlunda upplevelse. Och kärnan i vad som gör rollspelen unika fångas helt även i ett mycket enkelt klassiskt rollspel. Jag tycker på samma sätt att man kan ana med nyare versioner av D&D (inte minst 5e) att de är designade med dataspel i åtanke (också).

Jag har skrivet mer om OSR (länk).

En annan sak som kan upplevas som en fördel eller nackdel med Classic är att eftersom det är så enkelt, så är det också väldigt enkelt att hitta på egna husregler som passar din grupp.

Vilken retro-klon eller Classic-D&D är bäst?

Jag är inte alls i stånd avgöra vilken retroklon som är bäst, eller om den är bättre än Bas/Expert från Titan Games.

Men retroklonerna uppstod därför att originalet blev svårt att få tag i. Sedan några år finns Rules Cyclopedia att köpa tillsammans med allt gammalt som publicerats till classic (på dmsguild.com). Om allt det funnits tillgängligt 1995-2015, så hade det troligtvis inte funnit så många retrokloner.

Med det sagt så är självklart inte Rules Cyclopedia från 1991 det perfekta D&D i alla avseenden. Olika retrokloner har ändrat vissa saker i avsikt att förbättra. Så för den som jämför retro-kloner finns några huvudsakliga skillnader:

  • Ras som klass: I D&D är alv, halvling och dvärg egna klasser. Människor kan vara krigare, magiker, präster och tjuvar. Men det finns inga alv-tjuvar eller dvärg-präster. Många retrokloner gör annorlunda.
  • Räddningsslag: I D&D finns 5 olika typer av räddningsslag. I vissa retrokloner finns bara 1 räddningsslag, eventuellt modifierat av grundegenskap (som 5e), eller andra lösningar.
  • Pansarklass: I D&D har man PK9 utan rustning och PK3 med plåtrustning – lägre är bättre. I många retrokloner har man vänt detta (precis som i 5e) så att högre PK är bättre.
  • En del retrokloner är mer T20-baserade (medan D&D ofta använder 1T6 eller 2T6 för att avgöra saker).

Styrkan i Rules Cyclopedia är mer avancerade saker som ofta saknas i retroklonerna, och att det är omfattande och komplett

  • Regler ända till grad 36
  • Färdigheter och “Weapon Mastery”-regler
  • Många formler, monster, magiska föremål, osv
  • Regler för fältslag, mm

Min egen avsikt är nog att använda Titan Games D&D, men med Rules Cyclopedia som referens i bakgrunden (och i den händelse jag behöver hantera karaktärer över grad 14) eftersom det är precis samma spel.

För den som vill börja spela D&D på svenska så skulle jag föreslå Monster & Magi. Det finns regler upp till grad 20, och skulle man vilja plocka in saker från Rules Cyclopedia så tror jag det skulle gå bra.

Även om Svärd & Svartkonst känns väl minimalistiskt för mig, så finns det mycket intressant att läsa i den snygga regelboken! Det finns lite valfria regler för strid, perspektiv på erfarenhetspoäng och hur exempelvis vandöda (levande döda) hanteras som helt klart är bra nytänkande!

Jag har svårt att se att jag skulle rekommendera någon att leta efter D&D Titan Games begagnat när M&M och S&S finns.

För den som föredrar ett engelskt spel så tycker jag utgångspunkten är att originalet (Rules Cyclopedia) är det bästa alternativet. Men det finns många bra retroclones på engelska. De jag skulle titta på först är:

  • Basic Fantasy Roleplaying Game (Mentzer / d20)
  • Castles and Crusades (1e / 3e)
  • Dark Dungeons (Moldvay / BECMI / Rules Cyclopedia)
  • Dungeon Crawl Classics (d20 / D&D)
  • Five Torches Deep (5e simplified)
  • Labyrith Lord (Moldvay / BECMI)
  • OSE / Old School Essential (B/X)
  • Sword & Sorcery (Original D&D)

Slutsats

Det här med att välja D&D-version som passar för dig och din grupp är inte helt enkelt. För att verkligen kunna ha en riktig uppfattning måste man såklart provspela. Men om man vet vad som finns så är det nog lite lättare att göra ett klokt val när man köper ett spel.

Jag hade nog inte köpt D&D 5e förra året ifall jag kände till Rules Cyclopedia och Monster & Magi då.

D&D Ability Scores: How exceptional is your character?

D&D (and many other RPGs) have ability scores. You are supposed to roll 3d6 for each of them, but most often players want better scores and DMs allow methods that gives better scores.

Each ability has a value in the range 3-18 and it gives a bonus in the range -4 to +4 (for Classic D&D: -3 to +3).

Lets forget about child mortality and assume 3d6 is for those who reach adulthood (without racial ability modifiers). This would give that the average citizen of a fantasy world has a total ability value of 63 (6 x 10.5) and a bonus of 0. It makes sense that the player characters – adventurers – are better than average. Not everyone becomes an adventurer.

If we imagine our D&D town or village not to be so very different from how children grow up today, I think this is a reasonable way to think of it (approximately)

  • 1/4: most talented among siblings
  • 1/10: most talented kid on the street
  • 1/30: most talented in class
  • 1/200: most talented in school
  • 1/1000: most talented person in a village
  • 1/10000: most talented person in town
  • … and beyond that, exceptional and rare talent

How does this match ability scores and bonuses?

Ability Score

6 abilitiesBetter than
6347%
6453%
6558%
6663%
6768%
6873%Most talented sibling
6977%
7081%
7185%
7288%
73Best of 10Most talented kid on street
74Best of 14
75Best of 18
76Best of 24
77Best of 32
78Best of 45
79Best of 64
80Best of 92
81Best of 136
82Best of 205Most talented kid in school
83Best of 316
84Best of 499
85Best of 808
86Best of 1341Most talented person in village
87Best of 2287
88Best of 4009
89Best of 7237
90Best of 13472Most talented person in town

So, a character with an average score of 15 would have a total of 90. On one hand that is a person who would probably have received some attention for her talents when growing up. On the other hand, it is far from unrealistic that such a person exist.

Bonuses

So what about total bonuses?

Total Bonus
6 Abilities
D&D 5e
Better than
Best of
D&D Classic
Better than
Best of
045%41%
+155%59%
+266%75%
+375%87%
+483%/17
+589%/45
+6/15/135
+7/26/473
+8/50/1 924
+9/103/9 075
+10/229/49 840
+11/554/320 497
+12/1 476/2 435 080
+13/4 344/22 166 682
+14/14 251/246 959 710
+15/52 658/3 481 658 707
+16/221 926/65 692 122 012
+17/1 083 583/1 846 464 029 852
+18/6 251 574/101 983 687 214 006

What I find interesting with this is that it can somehow be connected to the background story of the character. If the character is just an unknown nobody who left her village for adventuring a total bonus of +6 (or +4 for Classic) is on the high side (a best-in-class-character would probably have recieved som attention). If, on the other hand, it is understood that the character is somehow distinguished, priviliged or meant for great deeds, it is not at all unreasonable with a total bonus of +12 (+8 in classic).

Even higher bonuses than that are very possible as long as it is understood that the characters are extraordinarily talented individuals in their society.

Other Rolling Methods

What if the DM allows other methods of rolling (Classic in parenthesis)?

~1/10 bad rollAverage roll~1/10 good roll
3d6-5 (-2)+0 (0)+5 (+2)Average person
2d6+6+4 (+2)+8 (+5)+12 (+8)Best in large class (~50)
4d6 drop worst+1 (+1)+6 (+4)+10 (+7)Best kid on street (~15)
5d6 drop worst 2+5 (+3)+9 (+6)+13 (+8)Best in small school (~100)
5e: [15, 14, 13, 12, 10, 8]+5 (+2)Best sibling
5e: My House Rule+6Best kid on street (~15)

What this table says, roughly, is that if the player rolls once using this method it is likely she will get the average outcome. Only about 1/10 she will get the bad result from the bad column, or the good result from the good column. It is not at all likely that any of your players will roll a +14 (+9) character (which would correspond to best one of 10 000, or most talented in town) with any of the methods.

Conclusion

From this little experiment, two things came to me.

First, when it comes to NPC, villains and heroes (Conan and Robin Hood if you like) sometimes the stats of such figures are published and they can seem ridiculously good. But that is quite fine. They may be one-in-a-million type of humans who really made a footprint in their time.

Second, most established methods for generating abilities will produce characters that are the most talented among their siblings or perhaps the most talented in their school. For a player to roll anything significantly better there are some ways:

  • Very much luck
  • Rolling very many times
  • Cheating
  • Inventing a very powerful method

I think, more than just focusing on the rolling and the methods, it makes sense to talk to they players and ask: who do you reasonably think your character is. If they are the son of Conan, or the only one girl selected by the archmage in the country to study magic because of her extraordinary talents, then very high ability scores are perfectly reasonable.

Source Code

The source code for this is written in JavaScript and runs with Node.js on your computer. It is not quite written to be published, but here it is anyway.

Parameters are hard coded in the top of main().

const objcopy = (obj) => {
  return JSON.parse(JSON.stringify(obj));
};

const num2prop = (n) => {
  if ( n < 0 ) return '' + n;
  if ( 0 < n ) return '+' + n;
  return ' 0';
};

const numcmp = (a,b) => {
  return +a - +b;
};

const abilities = () => {
  const ret = {};
  let i;
  for ( i=3 ; i<=18 ; i++ ) ret[i] = 0;
  return ret;
};

const dice3d6 = () => {
  const ret = abilities();
  let i, j, k;
  for ( i=1;i<=6;i++ ) for ( j=1;j<=6;j++ ) for ( k=1;k<=6;k++ ) ret[i+j+k]++;
  return ret;
};

const dice2d6p6 = () => {
  const ret = abilities();
  let i, j;
  for ( i=1;i<=6;i++ ) for ( j=1;j<=6;j++ ) ret[i+j+6]++;
  return ret;
};

const dice4d6drop = () => {
  const ret = abilities();
  let i, j, k, l;
  for ( i=1;i<=6;i++ )
    for ( j=1;j<=6;j++ )
      for ( k=1;k<=6;k++ )
        for ( l=1;l<=6;l++ ) {
          const a = [i,j,k,l];
          a.sort(numcmp);
          ret[a[1]+a[2]+a[3]]++;
        }
  return ret;
};

const dice5d6drop = () => {
  const ret = abilities();
  let i, j, k, l, m;
  for ( i=1;i<=6;i++ )
    for ( j=1;j<=6;j++ )
      for ( k=1;k<=6;k++ )
        for ( l=1;l<=6;l++ )
          for ( m=1;m<=6;m++ ) {
            const a = [i,j,k,l,m];
            a.sort(numcmp);
            ret[a[2]+a[3]+a[4]]++;
          }
  return ret;
};

const multiplestats = (dist,n) => {
  let ret = objcopy(dist);
  let i;
  for ( i=1 ; i<n ; i++ ) {
    ret = multiplestats_2(ret,dist);
  }
  return ret;
};

const multiplestats_2 = (total,one) => {
  let ret = {};
  let t, o, r;
  for ( t in total ) for ( o in one ) {
    r = num2prop(+t + +o);
    if ( !ret[r] ) ret[r] = 0;
    ret[r] += (total[t] * one[o]);
  }
  return ret;
};

const bonuses = {
  'Classic' : () => {
    return {
      '-3' : 0,
      '-2' : 0,
      '-1' : 0,
      ' 0' : 0,
      '+1' : 0,
      '+2' : 0,
      '+3' : 0
    };
  },
  '5e' : () => {
    return {
      '-4' : 0,
      '-3' : 0,
      '-2' : 0,
      '-1' : 0,
      ' 0' : 0,
      '+1' : 0,
      '+2' : 0,
      '+3' : 0,
      '+4' : 0
    };
  }
};

const ability2bonus = {
  'Classic' : (a) => {
    switch (a) {
    case 3: return '-3';
    case 4: case 5: return '-2';
    case 6: case 7: case 8: return '-1';
    case 9: case 10: case 11: case 12: return ' 0';
    case 13: case 14: case 15: return '+1';
    case 16: case 17: return '+2';
    case 18: return '+3';
    };
  },
  '5e' : (a) => {
    switch (a) {
    case 3: return '-4';
    case 4: case 5: return '-3';
    case 6: case 7: return '-2';
    case 8: case 9: return '-1';
    case 10: case 11: return ' 0';
    case 12: case 13: return '+1';
    case 14: case 15: return '+2';
    case 16: case 17: return '+3';
    case 18: return '+4';
    };
  }
};

const dist_ability2bonus = (distDice,version) => {
  const distBonus = bonuses[version]();
  const getBonus = ability2bonus[version];
  let a;
  for ( a=3 ; a<=18 ; a++ ) distBonus[getBonus(a)] += distDice[a];
  return distBonus;
};

const toPercent = (dist) => {
  const ret = {};
  const keys = Object.keys(dist).sort(numcmp);
  let total = 0;
  let i,k;
  for ( i=0 ; i<keys.length ; i++ ) {
    k = keys[i];
    total += dist[k];
  };
  total = 100 / total;
  for ( i=0 ; i<keys.length ; i++ ) {
    k = keys[i];
    ret[k] = dist[k] * total;
  };
  return ret;
};

const accumulate = (dist) => {
  const ret = {};
  const keys = Object.keys(dist).sort(numcmp);
  let acc = 0;
  let i,k;
  for ( i=0 ; i<keys.length ; i++ ) {
    k = keys[i];
    ret[k] = acc;
    acc += dist[k];
  }
  return ret;
};

const rounddecimals = (dist) => {
  let k,v;
  for ( k in dist ) {
  v = dist[k];
  if ( !Number.isInteger(v) ) {
    if ( v < 10 )
      dist[k] = '1/' + (100/v).toFixed(0);
    else if ( 90 < v )
      dist[k] = '1/' + (100/(100-v)).toFixed(0);
    else
      dist[k] = v.toFixed(1);
    }
  }
};

const main = () => {
//const rolldist = dice3d6();
//const rolldist = dice2d6p6();
//const rolldist = dice4d6drop();
  const rolldist = dice5d6drop();
//const bonus = '5e';
  const bonus = 'Classic';
  const rollcount = 6;
  const percent = true;
  const accumulated = true;
  const round = true;
  const printall = true;
  let workdata = [{ dist:rolldist , lbl:'Roll Dist'}];
  const last = () => { return workdata[workdata.length-1].dist; };
  if ( bonus ) workdata.push({
    dist : dist_ability2bonus(last(),bonus),
     lbl : 'Bonus: ' + bonus
  });
  if ( 1 < rollcount ) workdata.push({
    dist : multiplestats(last(),rollcount),
     lbl : '' + rollcount + ' stats'
  });
  if ( percent ) workdata.push({
    dist : toPercent(last()),
     lbl : 'Percent'
  });
  if ( accumulated ) workdata.push({
    dist : accumulate(last()),
     lbl : 'Accumulated'
  });
  if ( round ) workdata.forEach((w) => { rounddecimals(w.dist) });
  if ( printall )
    console.log(JSON.stringify(workdata,null,4));
  else
    console.log(JSON.stringify(last,null,4));
};

main();

Rollformulär Titan Games D&D

Här följer rollformulär för svenska Dungeons & Dragons av Titan Games.

Rollformular-TitanGames.pdfOriginal Spelarens Handbok, (C) Titan Games
Rollformular-DnD.pdfMitt eget rollformulär, 2 sidor, anpassade för mina husregler
Rollformular-DnD-2on1.pdfMitt eget rollformulär, båda sidorna bredvid varandra på en A4-sida

Simple Password Hashing with Node & Argon2

When you build a service backend you should keep your users’ passwords safe. That is not so easy anymore. You should

  1. hash and salt (md5)
  2. but rather use strong hash (sha)
  3. but rather use a very expensive hash (pbkdf2, bcrypt)
  4. but rather use a hash that is very expensive on GPUs and cryptominers (argon2)

Argon2 seems to be the best choice (read elsewhere about it)!

node-argon2

Argon2 is very easy to use on Node.js. You basically just:

$ npm install argon2

Then your code is:

/* To hash a password */
hash = await argon2.hash('password');

/* To test a password */
if ( await argon2.verify(hash,'password') )
  console.log('OK');
else
  console.log('Not OK');

Great! What is not to like about that?

$ du -sh node_modules/*
 20K  node_modules/abbrev
 20K  node_modules/ansi-regex
 20K  node_modules/aproba
 44K  node_modules/are-we-there-yet
348K  node_modules/argon2
 24K  node_modules/balanced-match
 24K  node_modules/brace-expansion
 24K  node_modules/chownr
 20K  node_modules/code-point-at
 40K  node_modules/concat-map
 32K  node_modules/console-control-strings
 44K  node_modules/core-util-is
120K  node_modules/debug
 36K  node_modules/deep-extend
 40K  node_modules/delegates
 44K  node_modules/detect-libc
 28K  node_modules/fs-minipass
 32K  node_modules/fs.realpath
104K  node_modules/gauge
 72K  node_modules/glob
 20K  node_modules/has-unicode
412K  node_modules/iconv-lite
 24K  node_modules/ignore-walk
 20K  node_modules/inflight
 24K  node_modules/inherits
 24K  node_modules/ini
 36K  node_modules/isarray
 20K  node_modules/is-fullwidth-code-point
 48K  node_modules/minimatch
108K  node_modules/minimist
 52K  node_modules/minipass
 32K  node_modules/minizlib
 32K  node_modules/mkdirp
 20K  node_modules/ms
332K  node_modules/needle
956K  node_modules/node-addon-api
240K  node_modules/node-pre-gyp
 48K  node_modules/nopt
 24K  node_modules/npm-bundled
 36K  node_modules/npmlog
172K  node_modules/npm-normalize-package-bin
 28K  node_modules/npm-packlist
 20K  node_modules/number-is-nan
 20K  node_modules/object-assign
 20K  node_modules/once
 20K  node_modules/osenv
 20K  node_modules/os-homedir
 20K  node_modules/os-tmpdir
 20K  node_modules/path-is-absolute
 32K  node_modules/@phc
 20K  node_modules/process-nextick-args
 64K  node_modules/rc
224K  node_modules/readable-stream
 32K  node_modules/rimraf
 48K  node_modules/safe-buffer
 64K  node_modules/safer-buffer
 72K  node_modules/sax
 88K  node_modules/semver
 24K  node_modules/set-blocking
 32K  node_modules/signal-exit
 88K  node_modules/string_decoder
 20K  node_modules/string-width
 20K  node_modules/strip-ansi
 20K  node_modules/strip-json-comments
196K  node_modules/tar
 28K  node_modules/util-deprecate
 20K  node_modules/wide-align
 20K  node_modules/wrappy
 36K  node_modules/yallist

That is 69 node modules of 5.1MB. If you think that is cool for your backend password encyption code (in order to provide two functions: encrypt and verify) you can stop reading here.

I am NOT fine with it, because:

  • it will cause me trouble, one day, when I run npm install, and something is not exactly as I expected, perhaps in production
  • how safe is this? it is the password encryption code we are talking about – what if any of these libraries are compromised?
  • it is outright ugly and wasteful

Well, argon2 has a reference implementation written in C (link). If you download it you can compile, run test and try it like:

$ make
$ make test
$ ./argon2 -h
Usage: ./argon2-linux-x64 [-h] salt [-i|-d|-id] [-t iterations] [-m log2(memory in KiB) | -k memory in KiB] [-p parallelism] [-l hash length] [-e|-r] [-v (10|13)]
Password is read from stdin
Parameters:
 salt The salt to use, at least 8 characters
 -i   Use Argon2i (this is the default)
 -d   Use Argon2d instead of Argon2i
 -id  Use Argon2id instead of Argon2i
 -t N Sets the number of iterations to N (default = 3)
 -m N Sets the memory usage of 2^N KiB (default 12)
 -k N Sets the memory usage of N KiB (default 4096)
 -p N Sets parallelism to N threads (default 1)
 -l N Sets hash output length to N bytes (default 32)
 -e   Output only encoded hash
- r   Output only the raw bytes of the hash
 -v (10|13) Argon2 version (defaults to the most recent version, currently 13)
 -h   Print ./argon2-linux-x64 usage

It builds to a single binary (mine is 280kb on linux-x64). It does most everything you need. How many lines of code do you think you need to write for node.js to use that binary instead of the 69 npm packages? The answer is less than 69. Here comes some notes and all the code (implementing argon2.hash and argon2.verify as used above):

  1. you can make binaries for different platforms and name them accordingly (argon2-linux-x64, argon2-darwin-x64 and so on), so you can move your code (and binaries) between different computers with no hazzle (as JavaScript should be)
  2. if you want to change argon2-parameters you can do it here, and if you want to pass an option-object to the hash function that is an easy fix
  3. options are parsed from hash (just as the node-argon2 package) when verifying, so you don’t need to “remember” what parameters you used when hashing to be able to verify
/* argon2-wrapper.js */

const nodeCrypto = require('crypto');
const nodeOs    = require('os');
const nodeSpawn = require('child_process').spawn;
/* NOTE 1 */
const binary    = './argon2';
// st binary    = './argon2-' + nodeOs.platform() + '-' + nodeOs.arch();

const run = (args,pass,callback) => {
  const proc = nodeSpawn(binary,args);
  let hash = '';
  let err = '';
  let inerr = false;
  proc.stdout.on('data', (data) => { hash += data; });
  proc.stderr.on('data', (data) => { err += data; });
  proc.stdin.on('error', () => { inerr = true; });
  proc.on('exit', (code) => {
    if ( err ) callback(err);
    else if ( inerr ) callback('I/O error');
    else if ( 0 !== code ) callback('Nonzero exit code ' + code);
    else if ( !hash ) callback('No hash');
    else callback(null,hash.trim());
  });
  proc.stdin.end(pass);
};

exports.hash = (pass) => {
  return new Promise((resolve,reject) => {
    nodeCrypto.randomBytes(12,(e,b) => {
      if ( e ) return reject(e);
      const salt = b.toString('base64');
      const args = [salt,'-id','-e'];
/* NOTE 2 */
//    const args = [salt,'-d','-v','13','-m','12','-t','3','-p','1','-e'];
      run(args,pass,(e,h) => {
        if ( e ) reject(e);
        else resolve(h);
      });
    });
  });
};

exports.verify = (hash,pass) => {
  return new Promise((resolve,reject) => {
    const hashfields = hash.split('$');
    const perffields = hashfields[3].split(',');
/* NOTE 3 */
    const args = [
        Buffer.from(hashfields[4],'base64').toString()
      , '-' + hashfields[1].substring(6) // -i, -d, -id
      , '-v', (+hashfields[2].split('=')[1]).toString(16)
      , '-k', perffields[0].split('=')[1]
      , '-t', perffields[1].split('=')[1]
      , '-p', perffields[2].split('=')[1]
      , '-e'
    ];
    run(args,pass,(e,h) => {
      if ( e ) reject(e);
      else resolve(h===hash);
    });
  });
};

And for those of you who want to test it, here is a little test program that you can run. It requires

  • npm install argon2
  • argon2 reference implementation binary
const argon2package = require('argon2');
const argon2wrapper = require('./argon2-wrapper.js');

const bench = async (n,argon2) => {
  const passwords = [];
  const hashes = [];
  const start = Date.now();
  let errors = 0;

  for ( let i=0 ; i<n ; i++ ) {
    let pw = 'password-' + i;
    passwords.push(pw);
    hashes.push(await argon2.hash(pw));
  }
  const half = Date.now();
  console.log('Hashed ' + n + ' passwords in ' + (half-start) + ' ms');

  for ( let i=0 ; i<n ; i++ ) {
    // first try wrong password
    if ( await argon2.verify(hashes[i],'password-ill-typed') ) {
      console.log('ERROR: wrong password was verified as correct');
      errors++;
    }
    if ( !(await argon2.verify(hashes[i],passwords[i]) ) ) {
      console.log('ERROR: correct password failed to verify');
      errors++;
    }
  }
  const end = Date.now();
  console.log('Verified 2x' + n + ' passwords in ' + (end-half) + ' ms');
  console.log('Error count: ' + errors);
  console.log('Hash example:\n' + hashes[0]);
};

const main = async (n) => {
  console.log('Testing with package');
  await bench(n,argon2package);
  console.log('\n\n');

  console.log('Testing with binary wrapper');
  await bench(n,argon2wrapper);
  console.log('\n\n');
}
main(100);

Give it a try!

Performance

I find that in Linux x64, wrapping the binary is slightly faster than using the node-package. That is weird. But perhaps those 69 dependencies don’t come for free after all.

Problems?

I see one problem. The node-argon2 package generates binary hashes random salts and sends to the hash algorithm. Those binary salts come out base64-encoded in the hash. However, a binary value (a byte array using 0-255) is not very easy to pass on the command line to the reference implementation (as first parameter). My wrapper-implementation also generate a random salt, but it base64-encodes it before it passes it to argon2 as salt (and argon2 then base64-encodes it again in the hash string).

So if you already use the node-package the reference c-implementation is not immediately compatible with the hashes you already have produced. The other way around is fine: “my” hashes are easily consumed by the node package.

If this is a real problem for you that you want to solve I see two solutions:

  1. make a minor modification to the C-program so it expects a salt in hex format (it will be twice as long on the command line)
  2. start supplying your own compatible hashes using the option-object now, and don’t switch to the wrapper+c until the passwords have been updated

Conclusion

There are bindings between languages and node-packages for stuff. But unix already comes with an API for programs written in different languages to use: process forking and pipes.

In Linux it is extremely cheap. It is quite easy to use and test, since you easily have access to the command line. And the spawn in node is easy to use.

Argon2 is nice and easy to use! Use it! Forget about bcrypt.

The best thing you can do without any dependencies is pbkdf2 which comes with node.js and is accessible in its crypto module. It is standardized/certified, that is why it is included.